Drupal 7 Services List and Entity Reference Field Issues
Published:
Heads up! This content is more than six months old. Take some time to verify everything still works as expected.
I've been working on an angularjs project that features a Drupal services backend. As I'm new to angular (or was), I thought I had to have been doing something wrong posting data back to Drupal to save my user accounts. The response from the services backend would be a 406, with the messages:
406 (Not Acceptable : An illegal choice has been detected. Please contact the site administrator.)
Actually, you'll have multiple copies of "An illegal choice has been detected. Please contact the site administrator." depending on the number of list and entity reference fields you are posting. The Drupal logs on the backend will contain multiple copies as well, with one of two additions per.
Warning: Illegal offset type in isset or empty in list_field_validate()
(line 394 of modules/field/modules/list/list.module)
Warning: Illegal offset type in isset or empty in _form_validate()
(line 1368 of includes/form.inc).
It took a while to track down what was happening. It made no sense that I could not post back the user object I had just received via api. After an hour googling, I came across this little hint: http://drupal.stackexchange.com/questions/61124/drupal-7-services-3-inse....
The author explains that he was experiencing the same issue, but
found his own solution. He modified the data being posted to
services to represent the format of data found the node form
array, not in the end structure resolved. I'm not sure he was aware
that was what he was doing, but it lead me to further searching.
I came across a bug report against the core for Drupal that
explained why the data format the previous author found worked.
It seems services is designed to use a somewhat disused part of
Drupal for its save operations. It uses version of
drupal_form_submit
to allow validation in a generic fashion.
This lets them get the errors seen above easily, without knowing
what modules are actually at work. The issue in doing that is the
data structure is not the real node, it's the form, which loops
back to the bug.
From what I can tell, both services, and the core seem responsible for this bug. Not really sure which should fix it, arguments could be made for both. The list module is part of core, services is its own animal, and entity reference is yet another responsible party. Honestly, the chance of this being fixed before my project needed to be done is minimal, and likely for yours as well if you're reading this. So, I worked out my own solution, perhaps services, and entity reference could add it to their code base, I'll link them here for reference. But for you and I, we can use services to fix services!
There's a lovely api hook in services that allows us to modify
arguments before they are passed to controllers:
hook_services_request_preprocess_alter
.
So, here's some experimental code I've produced that fixes both list
module select, and entity reference select fields. It does leave a
note in the logs if it rewrites a field so make sure check them
if you find any odd field based behaviors.
/** * Allow to alter arguments before they are passed to service callback. * * @param $controller * Controller definition * @param $args * Array of arguments * @param $options * * @see services_controller_execute() * @see services.runtime.inc */ function custom_services_request_preprocess_alter($controller, &$args, $options) { // Look for node and user callbacks to correct the argument format automatically // @see http://drupal.stackexchange.com/questions/61124/drupal-7-services-3-inse... // @see https://www.drupal.org/node/1195306 // No callback? No work if( empty($controller['callback']) ){ return; } // Not one of ours? Skip it if( !in_array($controller['callback'], array( '_user_resource_create', '_user_resource_update', '_node_resource_create', '_node_resource_update', ))){ return; } // Determine the entity type if( $controller['callback'] == '_user_resource_update' || $controller['callback'] == '_node_resource_update' ){ $entity =& $args[1]; } else { $entity =& $args[0]; } $language = !empty($entity['language'])? $entity['language'] : LANGUAGE_NONE; // Get a field dump of properties associated with this entity type $fields = field_info_fields(); // Modify the input of all incoming data for fields of type entity_reference // or using the select widget foreach( $fields as $field_name => $field_settings ){ // Skip non-problematic fields if( !in_array($field_settings['module'], array('list', 'entityreference')) ){ continue; } // Skip fields that are not present in the posted data if( !array_key_exists($field_name, $entity) ){ continue; } switch($field_settings['module']){ case 'list': if( empty($entity[$field_name][$language][0]) ){ continue; } $values = array(); foreach( $entity[$field_name][$language] as $field_item ){ $values[] = $field_item['value']; } $entity[$field_name][$language] = $values; watchdog( 'services' ,t('Entity %field_name argument rewritten by custom programming. <pre>!data</pre>') ,array( '%field_name' => $field_name, '!data' => print_r($entity[$field_name], TRUE), ) ,WATCHDOG_INFO ); break; case 'entityreference': if( empty($entity[$field_name][$language][0]['target_id']) ){ continue; } $values = array(); foreach( $entity[$field_name][$language] as $field_item ){ $values[] = $field_item['target_id']; } $entity[$field_name][$language] = $values; watchdog( 'services' ,t('Entity %field_name argument rewritten by custom programming. <pre>!data</pre>') ,array( '%field_name' => $field_name, '!data' => print_r($entity[$field_name], TRUE), ) ,WATCHDOG_INFO ); break; } } }