Oct 09 2014
Oct 09

Field collections is a nice contributed module that extends the default Drupal entity functionality by creating a new entity field that can be composed by other fields. With this module we solve problems like creating complex entities where we want to store multiple different values into one single field. This works because Drupal lets us assign unlimited values for a field, which links to the field collection entity where we have multiple fields, thats how we "grouped" multiple fields into one field.

At a basic usage, field collections is not difficult to add to our entity, but when we want to do some advanced stuff, this module can be very complex. Here I will show how to do some magic with it, programatically.

Obtain the values of the child fields

How to access the field collection values of an entity? Field collections store field collection items, which are the values of this kind of field. The field collection items use an ID, and with this ID we can find other fields. It also defines a new entity type called “field_collection_item”.

  1. Access to all values of a field collection on an existing entity, using Field API // Get the items of a field collection, from a node
    $field_node_example_values = field_get_items('node', $node, 'field_node_example');

    // Use a field collection helper function to get all the field collection item ids
    $field_node_example_ids = field_collection_field_item_to_ids($field_node_example_values);
    // By default, Drupal don't load the field collection items
    $field_node_example_fc_items = field_collection_item_load_multiple($field_node_example_ids);

    // Loop over every field collection item and get the values for each field
    foreach ( field_node_example_fc_items as $item) {
        $fc_field_values = field_get_items('field_collection_item', $item, 'field_inside_fc');
    }

  2. Access to all values of a field collection on an existing entity, using an entity wrapper

    $node_wrapper = entity_metadata_wrapper('node', $nid);
    // Loop over the collections until we find which we have to modify
    foreach ($node_wrapper->field_example->value() as $field_example_value) {
       // Wrap it with Entity API
       $fc_wrapper = entity_metadata_wrapper('field_collection_item', $field_example_value);
       $fc_field_value = $fc_wrapper->field_example_child->value();
    }

Alter the child fields

Now let's see how we can modify or remove items from an existing field collection of an entity. To do things easily, I prefer to use here an entity wrapper.

  1. Alter the value of one of the fields of a concrete field collection item $fc_item = field_collection_item_load($item_id);

    // Check there's no problem with the item
    if (!$fc_item) {
      return;
    }

    // Wrap it before modifying
    $fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $fc_item);
    $field_value = $fc_item_wrapper->field_example->value();
       ... change $field_value ...
    $fc_item_wrapper->field_example->set($field_value);
    $fc_item_wrapper->save();

  2. Delete a field collection item

    $fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $item_id);
    $fc_item_wrapper->delete();

Create a field_collection_item

Field collection items use a “entity host”, which is the entity to whom the field collection is attached. To add a new field collection item to an existing field collection field, we have to create it and set its entity host, then we can assing values to every single field of the field collection item and save it.

// Create a field_collection_item entity
$fc_item = entity_create('field_collection_item', array('field_name' => 'field_example_is_a_field_collection'));

// Attach it to the node
$fc_item->setHostEntity('node', $node);

// Check there's no problem with the item
if (!$fc_item) {
  return;
}

// Wrap it with Entity API
$fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $fc_item);

// Assign values to its fields
foreach($fc_item_values as $field_name => $field_value){
  $fc_item_wrapper->$field_name->set($field_value);
}

$fc_item_wrapper->save();


Alter a field_collection field into a form

When we are creating or editing a node (or any other entity) we use a form, and to modify it usually the hook_form_alter is used. Here are some interesting examples of how to alter the field collection widget.

  1. Change the options for a select box on all the field collection items and don't display the remove button for the first item. $field_name = 'field_collection_example';
    $fp_langcode = $form[$field_name]['#language'];
    $options = array( … );
    foreach (element_children($form[$field_name][$fp_langcode]) as $child) {
      // All the unlimited values fields have the “Add more” button as a child, but it's not a field collection item
      if (is_numeric($child)) {
        $form[$field_name][$fp_langcode][$child]['field_selectable'][$fp_langcode]['#options'] = $options;

        // Don't display the remove button for the first field collection item
        if ($child == 0) {
          unset($form[$field_name][$fp_langcode][$child]['remove_button']);
        }
      }
    }

  2. The field collection field stores some state information about it on $form_state. With this information we can know, realtime, how many items we have on the form and take decisions about it. For example, we have an unlimited values field collection field, but we want to limit the number of items depending on some certain circunstances. With this example, we can control a field to not display more than 3 field collection items on the form. $field_name = 'field_collection_example';
    $fp_langcode = $form[$field_name]['#language'];

    // Get state information about this field
    $fp_parents = $form[$field_name][$fp_langcode]['#field_parents'];
    $field_state = field_form_get_state($fp_parents, $field_name, $fp_langcode, $form_state);

    // The field state will change everytime we click on “Add more”, after the ajax call is triggered and the form is re-rendered to display the changes.

    // The user can see a maximum of 3 field collection items
    $items_limit = 3;
    if ($field_state['items_count'] < $items_limit) {
      // I want to change the “Add more” button title
      $form[$field_name][$fp_langcode]['add_more']['#value'] = t('Add another');
    }
    else {
      unset($form[$field_name][$fp_langcode]['add_more']);
    }

Clone field_collections from an existing node to a new node

Finally, here is an example of how to assign some default values to a field collection on a node creation form. As you can suspect, this is tricky if the value to clone is only one field collection item, but if we want to clone more than one, it's a problem because Drupal by default will only provide the form widget to enter the first item values (and wait for you to click on “Add more”).

The technique is to reproduce what Drupal does when the Field API attaches the fields to the node form. Using this way, Drupal will provide all the form structure we need to see all the values that come from the node we want to use as origin, but this has a problem and is that doing it this way we are assigning the same field collection items to two different nodes and if one of the nodes changes the values of one of the items, this will be reflected on the other node. The solution is to reset the item_id and revision_id values for the items on the new node, so we get the default values we want and they will be stored as new field collection items attached to the new node.

    $original_node = node_load(123);
    $node_type = 'my_custom_type';
   
    // Get the fields defined for this node type;
    $node_fields =  field_info_instances('node', $node_type);

    // Re-create the fields for the original node, like when editing it
    $tmpform = array();
    $tmpform['#node'] = $original_node;
    $tmpform['type']['#value'] = $node_type;
    $tmpform_state = array( 'build_info' => array('form_id' => $form['#form_id']) );
    field_attach_form('node', $original_node, $tmpform, $tmpform_state, entity_language('node', $original_node));

    // Here we have on $tmpform the form structure we need and with the default values.
    // We can choose what fields to clone, but in this example we will loop over all the node fields and clone all of them
    foreach($node_fields as $field_name => $field_settings) {
      // Copy the form structure
      $form[$field_name] = $tmpform[$field_name];
      // Copy state information about this field
      $form_state['field'][$field_name] = $tmpform_state['field'][$field_name];

      // When copying the field_collection structure, reset the id of the entities and
      // they will be created again with a new id.
      $langcode = field_language('node', $original_node, $field_name);
      if ($form_state['field'][$field_name][$langcode]['field']['type'] == 'field_collection') {
        $field_childs = &$form_state['field'][$field_name][$langcode]['entity'];
        foreach(element_children($field_childs) as $idx => $fc_entity) {
          $field_childs[$idx]->item_id = NULL;
          $field_childs[$idx]->revision_id = NULL;
        }
      }
   }

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web