Entity reference – Testreszabhatóság felsőfokon

czigor képe

CsatolmányMéret

MYMODULE.tar.gz10 KB

Az Entity reference modul az egyik leginkább használt Drupal modul. Az írás pillanatában az 51. helyen áll a projektek népszerűségi sorrendjében. Aki készített már Drupal oldalt, valószínűleg ismeri. Mégis kevesen tudják, milyen hatékony eszköz tud lenni még különleges esetekben is.

Egy drupal modul minőségét elég jól jellemzi, hogy mennyire lehet testreszabni. Ha például egy modul valamilyen HTML-t gyárt, elvárjuk, hogy ezt a HTML-t a drupal theme rendszerén keresztül meg tudjuk változtatni. Az Entity reference modul ilyen szempontból (is) egy kiválóan megírt modul. Node mit kellene tudni testreszabni az Entity reference modulban?

Az általunk a közelmúltban készített Drupal oldalon a tartalmak egy része külső forrásból származik. Ezek a tartalmak digitalizált könyvek, amiket a Drupal oldalon egyszerű book tartalomtípusú node-okként tárolunk. A könyveket nem a Drupal „Tartalom létrehozása” űrlapjain keresztül küldjük be, hanem egy, a könyveket hordozó XML fájlt importálunk. Az XML fájl azonban időről időre frissül, ilyenkor a Drupal oldalon letöröljük a könyveket és újraimportáljuk az XML fájlt.

Örök probléma azonban, hogy hogyan kezeljük az újraimportált tartalmakra mutató hivatkozásokat. Az Entity reference modul alapértelmezetten nid-et tárol, ami ilyenkor megváltozik. Lehetne persze törlés és újralétrehozás helyett frissíteni is a könyvet, de mi más megoldás mellett döntöttünk.

Minden könyv tartalom kapott egy egyedi azonosítót, ami már az XML fájlban szerepel és amit Drupal szinten egy mezőben (field_id) tárolunk. Ez az azonosító az, ami nem változik újraimportálás során sem. Azt kellett megoldani, hogy az Entity reference modul a nidek helyett ezt az egyedi azonosítót használja. És itt jöttek be a képbe az Entity reference selection és behavior pluginjai.

Ezek ctools pluginok, amik pont azt a célt szolgálják, hogy az entity reference mező minden porcikáját tetszőlegesen testreszabhassuk. A selection pluginok a mezőhöz használt widget viselkedését módosítják, a behavior pluginok pedig a mező értékének betöltését, mentését, adatbázisban történő tárolását, views integrációját és hozzáférését szabályozzák.

Lássuk, hogy nézett ki ennek konkrét implementálása! (Néhány saját függvényt inkább nem csatolunk, csak leírjuk, mit csinálnak.)

Mint minden ctools plugin esetében, itt is egy hook_ctools_plugin_directory()-val kezdünk.

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

A behavior plugin elkészítéséhez a MYMODULE modulban létrehozzuk plugins/entityreference/behavior/mymodule_behavior.inc fájlt:

<?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',
);

A további magyarázatot ld. a kódhoz írt (angol) megjegyzésekben. A file a 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.
  }
}

A selection plugin hasonlóan működik. Először a MYMODULE modulban létrehozzuk plugins/entityreference/selection/mymodule_selection.inc fájlt:

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

Majd a plugins/entityreference/selection/BookTextEntityReference_SelectionHandler.class.php-t. Beszéljen helyettem a kód.

<?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 '';
  }
 
}

Összefoglalva: az entityreference mező minden olyan részét sikerült átalakítanunk, ami nem felelt meg a mi szükségleteinknek. Ráadásul átlátható módon, mindössze két osztály kiterjesztésével. Így az összetartozó funkcionalitás a kódban is garantáltan egy helyen marad. Mindezzel az Entity reference modul számunkra igazán kellemes meglepetést okozott.

(A csatolt MYMODULE.tar.gz a cikkben megjelenített kódot tartalmazza.)

Technológia: entityreferencectoolsDrupal 7