You are here

Entity reference - customizability at its best

CZÖVEK András's picture

The Entity reference module is one of the most used Drupal modules. At the moment of writing it is 51st on the usage list of Drupal projects. Those who have already made a Drupal site probably have knows it. At the same time, not many know what a powerful tool it can be even in special cases.

The quality of a Drupal module is quite well characterized by its customizability. If a module produces markup for instance we expect to be able to alter that markup through the theme layer of Drupal. The Entity reference is an excellently written module in this aspect (too). But what should be customized in the Entity reference module?

In one of the sites recently produced by us one part of the content comes from an external source. These contents are digitalized books stored on the Drupal site as simple nodes with "book" content type. The books are not submitted via the Drupal "Create Content" form but instead an XML file with the book text is imported. However, the XML file is updated from time to time: when this happens we remove the book nodes from the Drupal site and reimport the XML file.

An eternal problem is the handling of the references pointing to reimported (or migrated) content. The Entity reference module stores the node nid as default and on reimport this nid changes. We could of course update the book nodes instead of deleting and reimporting them but we chose another way.

Each book content got a unique string id that came from the XML file and that we store in a (field_id) field on the Drupal level. This id is the id that does not change after reimporting. The problem to be solved was reduced to get the Entity reference module store this unique id instead of the nid. And that's where Entity reference selection and behavior plugins enter the picture.

These are ctools plugins serving the purpose of customizing every bit of the entityreference field to our liking. Selection plugins modify the behaviour of the widget used for the field. Behavior plugins regulate the access, loading, saving and storing of the field value and the views integration.

Let us see, how the implementation looks like! (Some custom functions are not attached, we only describe their functionality.)

As with every ctools plugin, first of all we need a hook_ctools_plugin_directory().

<?php
 
/**
 * Implements hook_ctools_plugin_directory().
 */
function MYMODULE_ctools_plugin_directory($module, $plugin) {
  if ($module == 'entityreference') {
    return 'plugins/entityreference/' . $plugin;
  }
}

To write the behavior plugin, we create a plugins/entityreference/behavior/mymodule_behavior.inc file in the MYMODULE module:

<?php
/**
 * @file
 * CTools plugin declaration for book reference behavior.
 */
 
$plugin = array(
  'title' => t('Book ref'),
  'description' => t('Book references entityreference behavior'),
  'class' => 'EntityReferenceBehavior_BookRef',
  'behavior type' => 'field',
);

Further explanations are in the code comments. The file is plugins/entityreference/behavior/EntityReferenceBehavior_BookRef.class.php:

<?php
 
/**
 * @file
 * Entityreference plugin class for the Book reference behavior.
 */
 
/**
 * An entityreference field behavior plugin to handle the id strings.
 *
 * We extend EntityReference_BehaviorHandler_Abstract. For further
 * info on the methods see entityreference/plugins/behavior/abstract.inc.
 */
class EntityReferenceBehavior_BookRef extends EntityReference_BehaviorHandler_Abstract {
 
  /**
   * Implements EntityReference_BehaviorHandler_Abstract::schema_alter().
   *
   * This method gets called from entityreference_field_schema() which
   * is a hook_field_schema() implementation.
   *
   * First of all we need to modify the default entityreference field schema
   * that accepts only integer values to prepare it for our varchar ids.
   */
  public function schema_alter(&$schema, $field) {
    $schema['columns']['target_id']['type'] = 'varchar';
    $schema['columns']['target_id']['length'] = 255;
    $schema['columns']['target_id']['default'] = '';
    // varchar cannot be unsigned so we unset this.
    unset($schema['columns']['target_id']['unsigned']);
  }
 
  /**
   * Implements EntityReference_BehaviorHandler_Abstract::insert().
   *
   * This method gets called from entityreference_field_insert() which
   * is a hook_field_insert() implementation.
   *
   * We want to store the string id in the database, so we convert
   * the nid into it when inserting a new field value.
   */
  public function insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
    $this->mymodule_transform_items($items);
  }
 
  /**
   * Implements EntityReference_BehaviorHandler_Abstract::update().
   *
   * This is the same as the previous method only that this gets called on
   * field update.
   */
  public function update($entity_type, $entity, $field, $instance, $langcode, &$items) {
    $this->mymodule_transform_items($items);
  }
 
  /**
   * Implements EntityReference_BehaviorHandler_Abstract::load().
   *
   * This method gets called from entityreference_field_load() which
   * is a hook_field_load() implementation.
   *
   * This method runs when a field is loaded (and is not fetched from
   * the cache). So this is the time to turn our custom string book
   * id into nid.
   */
  public function load($entity_type, $entities, $field, $instances, $langcode, &$items) {
    foreach ($entities as $entity) {
      $ids = field_get_items('node', $entity, $field['field_name']);
      if ($ids) {
        foreach ($ids as $id) {
          // You won't find mymodule_get_nid_by_id() in this article but
          // believe me: it simply converts a book string id into nid.
          $items[$entity->nid][]['target_id'] = mymodule_get_nid_by_id($id);
        }
      }
    }
  }
 
  /**
   * Helper function: Transform field items from nid to field_id values.
   */
  protected function mymodule_transform_items(&$items) {
    foreach ($items as $key => &$item) {
      // You won't find mymodule_get_id_by_nid() in this article but
      // believe me: it simply converts a book string nid into id.
      $item['target_id'] = mymodule_get_id_by_nid($item['target_id']);
    }
  }
 
  /**
   * Implements EntityReference_BehaviorHandler_Abstract::views_data_alter().
   *
   * This method gets called from entityreference_field_views_data() which
   * is a hook_field_views_data() implementation.
   *
   * To use views relationships in the usual way we need to use a custom
   * relationship handler (mymodule_views_handler_relationship_book_ref)
   * that joins the node, the book id and the entityreference field tables.
   * This views relationship handler is out of the scope of this article.
   *
   * For the reverse relationship a separate hook_views_data_alter()
   * implementation is needed.
   */
  public function views_data_alter(&$data, $field) {
    // We need to join in the field_data_field_id table in the middle.
    $data['field_data_field_text_refs']['field_text_refs_target_id']['relationship']['middle_table'] = 'field_data_field_id';
    $data['field_data_field_text_refs']['field_text_refs_target_id']['relationship']['middle_table_left_field'] = 'field_id_value';
    $data['field_data_field_text_refs']['field_text_refs_target_id']['relationship']['middle_table_right_field'] = 'entity_id';
    $data['field_data_field_text_refs']['field_text_refs_target_id']['relationship']['handler'] = 'mymodule_views_handler_relationship_book_ref';
 
    // To make the story complete we should alter the revision relationships too
    // but since we don't need them we won't.
  }
}

Selection plugins work in a similar way. First we create the plugins/entityreference/selection/mymodule_selection.inc file in the MYMODULE module:

<?php
 
$plugin = array(
  'title' => t('Select Book texts'),
  'class' => 'BookTextEntityReference_SelectionHandler',
  'weight' => -100,
);

Then comes plugins/entityreference/selection/BookTextEntityReference_SelectionHandler.class.php. Let the code speak instead of me:

<?php
 
/**
 * A Book Entity reference selection handler.
 *
 * This handles the entityreference field widget.
 *
 * There are some more methods that can be implemented, e.g. in order
 * to use an autocomplete widget. For examples and further description see
 * entityreference/plugins/selection/ and the OG module.
 *
 */
class BookTextEntityReference_SelectionHandler extends EntityReference_SelectionHandler_Generic {
 
  /**
   * Overrides EntityReference_SelectionHandler_node::getInstance().
   *
   * This does not do much but is needed.
   */
  public static function getInstance($field, $instance = NULL, $entity_type = NULL, $entity = NULL) {
    return new BookTextEntityReference_SelectionHandler($field, $instance, $entity_type, $entity);
  }
 
  /**
   * Overrrides EntityReference_SelectionHandler_Generic::settingsForm().
   *
   * We don't want any settings on our settings form, not even the
   * default entity and bundle selection.
   */
  public static function settingsForm($field, $instance) {
    return array();
  }
 
  /**
   * Overrides EntityReference_SelectionHandler_Generic::getReferencableEntities().
   *
   * Get the options for our widget. The keys are the book node nids,
   * the values are the book titles.
   *
   * To keep it simple, the $match, $match_operator and $limit
   * arguments are not used.
   *
   * Normally, Entity reference uses an EFQ here but that can be very resource
   * intensive as to get the title each entity must be loaded. So we just use
   * a db_select() inside a custom mymodule_book_get_ref_titles() function.
   *
   */
  public function getReferencableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
    /// We store the book chapters in a 'book' content type.
    $bundles = array('book');
    $return = array();
    foreach ($bundles as $bundle) {
      // mymodule_book_get_ref_titles() just returns an array with all
      // referencable book node nids as keys and the node titles as values.
      $return[$bundle] =  mymodule_book_get_ref_titles($bundle);
    }
    return $return;
  }
 
  /**
   * Overrides EntityReference_SelectionHandler_Generic::countReferencableEntities().
   *
   * Surprise: it returns the number of referencable entities.
   */
  public function countReferencableEntities($match = NULL, $match_operator = 'CONTAINS') {
    // mymodule_book_get_ref_titles() just returns an array with all
    // referencable book node nids as keys and the node titles as values.
    $referencable_entities = mymodule_book_get_ref_titles();
    return count($referencable_entities);
  }
 
  /**
   * Overrides EntityReference_SelectionHandler_Generic::validateReferencableEntities().
   *
   * This method is called from entityreference_field_validate, which
   * is a hook_field_validate() implementation. This method must
   * return the accepted entity ids.
   *
   * validateReferencableEntities() might be different from
   * getReferencableEntities() in that the latter does not necessarily
   * return the entity ids (though in our case it does.)
   */
  public function validateReferencableEntities(array $ids) {
    if ($ids) {
      // mymodule_book_get_ref_titles() just returns an array with all
      // referencable book node nids as keys and the node titles as values.
      $referencable_entities = mymodule_book_get_ref_titles(array('book'));
      return array_keys($referencable_entities);
    }
    return array();
  }
 
  /**
   * Overrides EntityReference_SelectionHandler_Generic::getLabel().
   *
   * When the widget displays the selected entities this method is called.
   * We display the field_title field of the node instead of its title
   * property.
   */
  public function getLabel($entity) {
    $label = field_get_items('node', $entity, 'field_title');
    if ($label) {
      return $label[0]['value'];
    }
    else return '';
  }
 
}

To sum up: we succeeded to customize all those parts of the entityreference field that did not fit our needs. On top this was done in a transparent way, extending only 2 classes. This way the cohesive functionality stays in the same place in code as well. All this was made possible by the Entity reference module and to our most pleasant surprise.

(The attached MYMODULE.tar.gz contains the code displayed in the article.)

AttachmentSize
Binary Data MYMODULE.tar.gz10 KB

Comments

Hi, is it possible to get the full demo module? I tried to install the attached module, but there are some functions missing (like mymodule_get_id_by_nid). As I am not a programmer, it would be very helpful for me.
Kind regards

Kai

Hi Kai,
Sorry, but there's no full demo module. You will definitely need developer skills to use this method anyway. Functions like mymodule_get_id_by_nid() should be quite straightforward to implement so they were not included for the sake of brevity.