Exposing reverse entity reference fields in Drupal

Exposing reverse entity reference fields in Drupal

Difficulty: 
Let's Rock

Entity references in Drupal is the mechanism used to do some "proper" (sorry for the quotes but what you can achieve with Drupal is years behind a real ORM such as the Entity Framework in terms of usability, reliability, flexibility and overal quality) data modeling without having to write everything from scratch including queries, widgets and storage. 

Drupal 8 supports entity reference fields natively, while in Drupal 7 you had to resort to the Entity Reference or References modules (and some others...).

In this post I will show you how to automatically populate reverse reference fields for all your entity reference fields so that you can leverage EntityMetadataWrapper to navigate the relationship in both ways.

Imagine I have two entities: User and Order. Order has an entity reference field to User called 'field_owner'.

If you have a metadata wrapper on the Order Entity you can easily access the User entity reference by 'field_owner' like this:

// Load order #25
$order = UtilsEntity::entity_metadata_wrapper('order', 25);
// Retrieve the name of the owner.
$owner_name = $order->field_owner->name->value();

But what if I you want to navigate this relationship in a reverse way, that is, list all the orders for a given user entity?

Not a big deal, but you would have to either write a plain query or use EntityFieldQuery.

It would be much easier and straightforward if - like in many ORM's - the reverse of the relationship was exposed as a field itself, so that you could do something like this:

// Load user uid = 1
$user = UtilsEntity::entity_metadata_wrapper('user', 1);
foreach ($user->reverse_order_field_owner as $order) {
  // Do somthing with this order.
}

Maybe this example is not the best use case because a user might have hunders of orders and this approach simply won't scale. ORM's that expose reverse relationships in one way or another also provide the tools needed to filter the results at the database level.

This can easily be achieved by implementing a hook and a callback method for the new virtual properties that expose the reverse references. This is a sample code that will work with both the Node Reference and the Entity Reference Drupal 7 modules.

namespace Drupal\fdf\EntityReverseRelationship;

use Drupal\fdf\Utilities\UtilsFdf;
use Drupal\fdf\FdfCore;
use Drupal\fdf\Utilities\UtilsEntity;

class Hooks {

  /**
   * Implements hook_entity_property_info_alter.
   */
  public static function hook_entity_property_info_alter(&$info) {

    // Para cada entidad crearemos una propiedad inversa para cada referencia
    // que la tenga a ella como destino. Hay 2 tipos de campo para ello: entityreference y node_reference.
    $fields = field_info_fields();

    foreach ($fields as $field) {
      switch ($field['type']) {
        case 'entityreference':
          $field_name = $field['field_name'];
          $entity_type = $field['settings']['target_type'];

          $bundles = array();
          if (!empty($instance['settings']['handler_settings']['target_bundles'])) {
            $bundles = $instance['settings']['handler_settings']['target_bundles'];
          }
          else {
            $available_bundles = isset($info[$entity_type]['bundles']) ? $info[$entity_type]['bundles'] : FALSE;
            if($available_bundles && isset($available_bundles[$entity_type])){
              $bundles = array($entity_type);
            }
          }

          foreach ($bundles as $bundle) {
            if (isset($info[$entity_type]['bundles'][$bundle])) {
              $info[$entity_type]['bundles'][$bundle]['properties']["reverse_{$field_name}"] = array(
                'label' => 'Reverse reference entity from field ' . $field_name,
                'description' => 'Reverse reference entity from field ' . $field_name,
                'type' => 'list<' . $entity_type . '>',
                'getter callback' => FdfCore::RegisterGlobalFunctionMap(UtilsFdf::GetStaticMethodCallback(static::class, 'GetReverseNodeEntity'), 'fdf_reverse_relationship', FALSE),
                'computed' => TRUE,
                // Para construir los listados de referencias necesitamos saber qué entidades y bundles apuntana  nosotros.
                'fdf_back_reference' => array('bundles' => $field['bundles'], 'field' => $field_name),
              );
            }
          }
          break;
        case 'node_reference':
          $types = $field['settings']['referenceable_types'];
          foreach ($types as $type) {
            if ($type == FALSE) {
              continue;
            }
            if (isset($info['node']['bundles'][$type])) {
              $field_name = $field['field_name'];
              $info['node']['bundles'][$type]['properties']["reverse_{$field_name}"] = array(
                'label' => 'Reverse reference node from field ' . $field_name,
                'description' => 'Reverse reference node from field ' . $field_name,
                'type' => 'list<node>',
                'getter callback' => FdfCore::RegisterGlobalFunctionMap(UtilsFdf::GetStaticMethodCallback(static::class, 'GetReverseNodeEntity'), 'fdf_reverse_relationship', FALSE),
                'computed' => TRUE,
                // Para construir los listados de referencias necesitamos saber qué entidades y bundles apuntana  nosotros.
                'fdf_back_reference' => array('bundles' => $field['bundles'], 'field' => $field_name),
              );    
            }
          }
          break;
      }
    }
  }

  /**
   * Summary of GetReverseNodeEntity
   * @param mixed $entity 
   * @param array $options 
   * @param mixed $name 
   * @param mixed $entity_type 
   * @param mixed $property_info 
   * @return array
   */
  public static function GetReverseNodeEntity($entity, array $options, $name, $entity_type, $property_info) {

    // Need the entity type the flag applies to.
    $remote_field = $property_info['fdf_back_reference']['field'];
    $remote_bundles = $property_info['fdf_back_reference']['bundles'];

    if (count($remote_bundles) > 1) {
      // D8 does not allow sharing fields between entities.
      watchdog('fdf reverse fields', 'Cannot inspect reverse reference field when it appears in two different entities.');
      return array();
    }

    $wrapper = UtilsEntity::entity_metadata_wrapper($entity_type, $entity);

    // Solo debería haber uno de estos...
    $remote_entity = array_keys($remote_bundles)[0];

    $query = new \EntityFieldQuery($remote_bundles);

    $query->entityCondition('entity_type', $remote_entity)
      ->fieldCondition($remote_field, 'target_id', $wrapper->getIdentifier(), '=')
      ->addMetaData('account', user_load(1)); // Run the query as user 1.

    $result = $query->execute();

    if (isset($result[$remote_entity])) {
      return array_keys($result[$remote_entity]);
    }
    
    return array();
  }
}

Now that Drupal 8 has entity references integrated into core, it would be nice to have something similar in both the MetadataWrapper and in EntityQuery so that we can get a straightforward and managed way of navigating and querying increasingly complex data models without having to manually write SQL queries (that can be quite a mess thanks Drupal's over engineered storage model).

Of course this is still extremely limited but will do the job for the scope of what a Drupal based project usually is.

 

Add new comment

By: root Monday, November 16, 2015 - 13:55