fredag den 7. november 2014

Creating field collections programmatically

I had the challenge of creating a new node containing multivalue field collections within multivalue field collections, etc.

This posed no problem as such, except for perhaps performance. Whenever a field collection is saved, the host entity is saved also. In my case it caused a save of the node 26 times, which is perhaps a bit overkill for a new node.

The FieldCollectionItem entity has an internal parameter ($skip_host_save) on the save() method. However, this is not availabe via entity_save() or via the entity metadata wrapper, the latter which I use heavily.

The class below will allow you to call an entity object's save() method with arguments via an entity metadata wrapper object.

/**
/**
 * Class EntityDrupalWrapperSaveArguments
 *
 * Hax0r class for saving field collection without saving the host entity.
 *
 * When saving a new field collection, the host entity will be saved as well.
 * This can result in several of unnecessary (1) saves of the host entity,
 * especially if creating a new node with many field collection fields.
 *
 * The FieldCollectionItem::save() supports an internal option for only saving
 * the field collection entity, but there's no way to send this option via
 * entity_save() or EntityDrupalWrapper::save(). Only through Entity::save() is
 * this possible. However, if we use Entity::save() directly, we loose all the
 * benefits of the metadata wrapper.
 *
 * Instead we implement a "pseudo" class, which has access to the entity
 * metadata wrapper object's protected variables.
 *
 * (1) They SEEM unnecessary.
 */
class EntityDrupalWrapperSaveArguments extends EntityDrupalWrapper {

  /**
   * Re-implementation of EntityDrupalWrapper->save().
   *
   * When saving a new entity, the wrapper object's id must be updated.
   * Since this is a protected variable, we implement this method in a class
   * "pretending" to be an EntityDrupalWrapper class. Thereby we gain access to
   * protected variables in other objects of the same type.
   *
   * @see EntityDrupalWrapper::save().
   */
  static public function saveArguments() {
    $args = func_get_args();
    $wrapper = array_shift($args);
    if ($wrapper->data) {
      if (!entity_type_supports($wrapper->type, 'save')) {
        throw new EntityMetadataWrapperException("There is no information about how to save entities of type " . check_plain($wrapper->type) . '.');
      }
      self::entity_save_arguments($wrapper->type, $wrapper->data, $args);
      // On insert, update the identifier afterwards.
      if (!$wrapper->id) {
        list($wrapper->id, , ) = entity_extract_ids($wrapper->type, $wrapper->data);
      }
    }
    // If the entity hasn't been loaded yet, don't bother saving it.
    return $wrapper;
  }

  /**
   * Re-implementation of entity_save().
   *
   * In order to send arguments to the entity's save() method, we need to
   * re-implement the logic from entity_save().
   *
   * This function takes an extra arguments ($args) compared to entity_save().
   * $args contains an array of the arguments passed to $entity->save().
   *
   * Note: ^ There's a difference between entity_save() and $entity->save().
   *
   * @see entity_save().
   * @see Entity::save().
   */
  static public function entity_save_arguments($entity_type, $entity, $args) {
    $info = entity_get_info($entity_type);
    if (method_exists($entity, 'save')) {
      return call_user_func_array(array($entity, 'save'), $args);
    }
    elseif (isset($info['save callback'])) {
      $info['save callback']($entity);
    }
    elseif (in_array('EntityAPIControllerInterface', class_implements($info['controller class']))) {
      return entity_get_controller($entity_type)->save($entity);
    }
    else {
      return FALSE;
    }
  }
}

// Create an Entity and populate it
$entity = entity_create('node', array('type' => 'article'));
$entity->uid = 1;
$entity->title = 'test';

$wrapper = entity_metadata_wrapper('node', $entity);

$fc_entity = entity_create('field_collection_item', array('field_name' => 'field_my_field_collection_field'));
$fc_entity->setHostEntity('node', $entity);

$fc_wrapper = entity_metadata_wrapper('field_collection_item', $fc_entity);
$fc_wrapper->field_my_field_inside_the_field_collection->set('some value');

// Old style save.
// $fc_wrapper->save();

// New style save.
// Equivalent to $fc_entity->save(TRUE), but retains the functionality of the metadata wrapper.
EntityDrupalWrapperSaveArguments::saveArguments($fc_wrapper, TRUE);




Disclaimer: I'm not responsible if you hurt yourself with this code. And be aware, that using this code will also bypass some presave/update/insert handlers. Which could hurt you. Big time. And I'm not responsible.