2

I have a custom auto-complete widget that I use to create a custom field that is used to create a custom content type in a custom module.

I have hook_field_widget_info

function MODULE_field_widget_info()
{
    return array(
        "MODULE_taxonomy_autocomplete" => array(
            "label"         => t("MODULE Autocomplete term widget (tagging)"),
            "field types"   => array("taxonomy_term_reference"),
            "settings"      => array(
                "size"              => 60,
                "autocomplete_path" => "taxonomy/autocomplete",
            ),
            "behaviors"     => array(
                "multiple values"   => FIELD_CARDINALITY_UNLIMITED,
            ),
        ),
    );
}

I have hook_field_widget_form

function MODULE_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element)
{
    $tags = array();
    foreach ($items as $item)
    {
        $tags[$item["tid"]] = isset($item["taxonomy_term"]) ? $item["taxonomy_term"] : taxonomy_term_load($item["tid"]);
    }

    $element += array(
        "#type"                 => "textfield",
        "#default_value"        => taxonomy_implode_tags($tags),
        "#autocomplete_path"    => $instance["widget"]["settings"]["autocomplete_path"] . "/" . $field["field_name"],
        "#size"                 => $instance["widget"]["settings"]["size"],
        "#maxlength"            => 1024,
    );

    return $element;
}

I have hook_field_widget_error

function MODULE_field_widget_error($element, $error, $form, &$form_state)
{
    form_error($element, $error["message"]);
}

Here I am creating the field of the content type

field_create_field(
    array(
        "field_name"    => "field_MODULE_tags",
        "type"          => "taxonomy_term_reference",
        "bundles"       => array(
            "node" => array(
                "CONTENT_TYPE",
            ),
        ),
        "cardinality"   => "-1",
        "settings"      => array(
            "allowed_values" => array(
                array(
                    "vocabulary" => "TAXONOMY_LIST",
                    "parent"     => 0,
                ),
            ),
        ),
    )
);

field_create_instance(
    array(
        "field_name"    => "field_MODULE_tags",
        "entity_type"   => "node",
        "label"         => "Tags",
        "description"   => "",
        "bundle"        => "CONTENT_TYPE",
        "widget"        => array(
            "weight"    => 1,
            "type"      => "MODULE_taxonomy_autocomplete",
            "settings"  => array(
                "size"              => 60,
                "autocomplete_path" => "taxonomy/autocomplete",
            ),
        ),
    )
);

All of this appears to work fine, the content type shows the correct field, the auto complete works great, but when I try to save a new node of the custom type, I get the following error:

PHP Fatal error:  Cannot unset string offsets in /modules/field/field.default.inc on line 41

If I look at field.default.inc file I see

function field_default_extract_form_values($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) {
  $path = array_merge($form['#parents'], array($field['field_name'], $langcode));
  $key_exists = NULL;
  $values = drupal_array_get_nested_value($form_state['values'], $path, $key_exists);
  if ($key_exists) {
    // Remove the 'value' of the 'add more' button.
    unset($values['add_more']);
    $items = $values;
  }
}

where line 41 is unset($values['add_more']);

If I output $values, I get a string where unset is expecting an array.

How can I make my value an array?

I've tried using hook_form_alter to add a custom submit function so I can manipulate the value but the custom submit never gets fired (which is weird).

2

hook_node_validate was how I was able to fix this issue.

function MODULE_node_validate($node, $form, &$form_state)
{
    foreach ($form as $field => $field_data)
    {
        $field_info    = field_info_field($field);
        $instance_info = field_info_instance("node", $field, "CONTENT_TYPE");

        // since this field returns a string when an array is required, we need to correct
        // the data here before it goes any further.
        if ($instance_info["widget"]["type"] == "MODULE_taxonomy_autocomplete")
        {
            // Recieved: string
            // Expecting: array(array("tid" => TID),)
            $tag_names = explode(",", $form_state["values"][$field][$node->language]);
            $form_state["values"][$field][$node->language] = array();
            foreach ($tag_names as $tag)
            {
                $term = taxonomy_get_term_by_name($tag, $field_info["settings"]["allowed_values"][0]["vocabulary"]);
                $term = array_shift($term);

                $form_state["values"][$field][$node->language][] = array("tid" => $term->tid);
            }
        }
    }
}
1

This error happens in field_default_extract_form_values which is trying to load default values from the field, therefore to make it array, you need to make sure #default_value is in the right format.

This error usually happens when #default_value is NULL, but array is expected. So you can always cast it into array something like:

"#default_value"        => (array)taxonomy_implode_tags($tags),

Below is code example for radios/checkboxes-like widget with array() and with field element validation handler as per @Scott suggestion (which seems to make the difference as well):

/**
 * Implements hook_field_widget_form().
 */
function foo_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {

  $value_key = key($field['columns']);
  $has_value = isset($items[0][$value_key]);
  $required = $element['#required'];
  $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;

  switch ($instance['widget']['type']) {
    case 'foo':
      $options = array(0 => t('Foo 1'), 1 => t('Bar'));
      $properties = _options_properties($instance['widget']['type'], $multiple, $required, $has_value);
      $default_value = _options_storage_to_form($items, $options, $value_key, $properties);
      if (!$multiple) {
        // If this is a single-value field, take the first default value.
        $default_value = $default_value ? reset($default_value) : array(); // <-- default array() here.
      }
      $element += array(
        '#type' => $multiple ? 'checkboxes' : 'radios',
        '#default_value' => $default_value,
        '#options' => $options,
        '#properties' => $properties,
      );
      break;
  }

  $element += array(
    '#value_key' => $value_key,
    '#element_validate' => array('foo_field_widget_validate'),
  );

  return $element;
}

/**
 * Form element validation handler for options element.
 */
function foo_field_widget_validate($element, &$form_state) {
  if ($element['#required'] && $element['#value'] == '_none') {
    form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
  }
  // Transpose selections from field => delta to delta => field, turning
  // multiple selected options into multiple parent elements.
  $items = _options_form_to_storage($element);
  form_set_value($element, $items, $form_state);
}

In rare cases the field.default.inc error can be related to corrupted structure of your form state. This can either by caused by invalid implementation of hook_field_widget_form or modifying/replacing $form/$form_state in hook_form_alter by reference.


Debugging

Debugging of invalid form state can be challenging. This can be achieved by PHP debugger (such as XDebug) and analysing structure of $values in field_default_extract_form_values() when the error happens (which suppose to be an array, not a string).

This can also be achieved by dumping the field instance which failing, for example:

--- a/modules/field/field.default.inc
+++ b/modules/field/field.default.inc
@@ -37,6 +37,9 @@ function field_default_extract_form_values($entity_type, $entity, $field, $insta
   $key_exists = NULL;
   $values = drupal_array_get_nested_value($form_state['values'], $path, $key_exists);
   if ($key_exists) {
+    if (!is_array($values)) {
+      var_dump($values, $field, $instance); die();
+    }
     // Remove the 'value' of the 'add more' button.
     unset($values['add_more']);

Or if Devel module is present, using dsm() or dd() is more convenient. For example can use dd($form_state['values']); to dump the whole structure of values into the file and compare the output between working and non-working scenarios.

For example (in shell) you can try:

> /tmp/drupal_debug.txt # Empty Devel debug file.
# Now test the working scenario to generate the dump.
cp -v /tmp/drupal_debug.txt working.txt
> /tmp/drupal_debug.txt # Empty the file again.
# Now test non-working scenario.
cp -v /tmp/drupal_debug.txt non-working.txt
diff -u working.txt non-working.txt # Or use GUI like: diffmerge, opendiff or meld

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.