Entity Field Queries

  • Published by Alan
  • Filed under Drupal

Entity Field Queries are a new feature introduced alongside the new field system in Drupal 7. These allow you to construct queries about entities and get back entity studs, partial entity objects, which contain the id, and if applicable, the revision id and bundle key. They are a powerful but often underutilised tool in Drupal.

Usage in core

For example, the following lists the limited usage within Drupal core:

  • Field module uses this to query fields to discover if they have any data in field_has_data() and in a couple of other places when creating if a field has data.
  • File module uses this to find file references in the function file_get_file_references().
  • Taxonomy module does not use this API directly, it does uses hook_entity_query_alter() that is used to bridge the Entity systems usage of the bundle key and the Taxonomy systems usage of the vocabulary id that is used when storing taxonomy terms within the database.

Usage in contrib

Within contrib, there are more examples, although not extensive by any means. Within one recent project usage was only detected in three modules, albeit two of these are two of the most important Entity system contrib modules that provide the underlying framework for many other modules:

  • Entity API uses it to get lists of entities and uses this within the default Entity listings via EntityDefaultUIController class.
  • EntityReference () uses it within its base case as the main entity query method.
  • LinkIt () uses it to fetch lists of Entities for it’s autocomplete widget.

Extending EntityFieldQuery

Without going into the details of the basics, it is possible to easy extend the base class for custom use cases. This allows developers to develop robust and reusable custom classes to handle common use cases.

Since we mostly handle Node entities, it makes sense to create a custom EntityFieldQuery class to handle base node operations. The first step is to tell Drupal autoclass loading registry where you are going to create this new class in the modules info file.

files[] = classes/GloNodeFieldQuery.php

Then define the class stub within this file

class GloNodeFieldQuery extends EntityFieldQuery {
}

We then create a constructor for our class. There is no need to call parent::__construct() here as the base class does not define any constructor.

  public function __construct($bundle = NULL, $order = 'title', $direction = 'ASC') {
    $this->entityCondition('entity_type', 'node');
    if (isset($bundle)) {
      // This will accept either singular values or arrays.
      $this->propertyCondition('type', $bundle);
    }
    // Default order.
    if (isset($order)) {
      $this->propertyOrderBy($order, $direction);
    } 
  }

While we generally prefer to use chaining methods to set important class instance options, it makes sense to set a few globally shared ones here to cover 95% of use cases as the main use of the class is almost always to return a list of nodes with their titles.

Excluding nodes from a query

A common use case is to exclude one or more existing nodes from the query. For example during validation of unique values, you would want to exclude the node being saved from the unique value check. This also shows the basis behind the method chaining idiom where you return the current object when there is nothing else to return.

  /**
   * Excludes nodes from the results.
   *
   * Pass in NULL to used the active menu object to guess the active node to
   * exclude.
   */
  public function excludeNodes($nids = array()) {
    if (!is_array($nids)) {
      $nids = array($nids);
    }
    $this->propertyCondition('nid', $nids, 'NOT IN');
    return $this;
  }

Increased performance when generating Entity option lists

A common usage of the EntityFieldQuery is to get a simple list of node titles and goes something like this:

$efq = new EntityFieldQuery();
$result = $efq->execute();
$options = array();
If (isset($result[‘node’]) {
  foreach (node_load_multiple(array_keys($result[‘node’])) as $nid => $node) {
    $options[$nid] = $node->title;
  }
}
return $options;

So along with the entity field query itself, the system also does full node loads, which can fatally consume system resources. The overhead may be minimal with 10 or so nodes, but that quickly escalate if you are trying to load 100's or 1000's of nodes.

So we override EntityFieldQuery::finishQuery() to set up the ability to return the node title from the query itself. This avoids the node_load_multiple() thus avoiding any potential WSOD (white screen of death).

  function finishQuery($select_query, $id_key = 'entity_id') {
    foreach ($this->tags as $tag) {
      $select_query->addTag($tag);
    }
    foreach ($this->metaData as $key => $object) {
      $select_query->addMetaData($key, $object);
    }
    $select_query->addMetaData('entity_field_query', $this);
    if ($this->range) {
      $select_query->range($this->range['start'], $this->range['length']);
    }
    if ($this->count) {
      return $select_query->countQuery()->execute()->fetchField();
    }

    // All this just to add...
    $select_query->addField('node', 'title', 'entity_label');

    $return = array();
    foreach ($select_query->execute() as $partial_entity) {
      $bundle = isset($partial_entity->bundle) ? $partial_entity->bundle : NULL;
      $entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $bundle));
      $entity->title = $partial_entity->entity_label;
      $return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity;
      $this->ordered_results[] = $partial_entity;
    }
    return $return;
  }

This is almost a clone of the core EntityFieldQuery::finishQuery() method, with the addition of these two lines of code.

$select_query->addField('node', 'title', 'entity_label');
$entity->title = $partial_entity->entity_label;

So the corresponding usage is now:

$efq = new GloNodeFieldQuery();
$result = $efq->execute();
$options = array();
If (isset($result[‘node’]) {
  foreach ($result[‘node’] as $nid => $node_stub) {
    $options[$nid] = $node_stub ->title;
  }
}
return $options;

Integrate with the Weight module

You could integrate with the weight module here to provide additional weight based ordering. This is a simple implementation that defines a single order by weight setter and the corresponding changes to GloNodeFieldQuery::finishQuery() to implement this sort.

class GloNodeFieldQuery extends EntityFieldQuery {

  /**
   * Integrates with the weight module to order by weight.
   */
  public function orderByWeight() {
    $this->orderByWeight = TRUE;
    return $this;
  }

  function finishQuery($select_query, $id_key = 'entity_id') {
    ...
    // Handle the join here rather than a new hook_query_alter().
    $weight_alias = 'weight';
    if ($this->orderByWeight && module_exists('weight')) {
      $join_alias = $select_query->leftJoin('weight_weights', 'w', 'node.nid = %alias.entity_id');
      $weight_alias = $select_query->addExpression('COALESCE(' . $join_alias . '.weight, 0)', $weight_alias);
      $select_query->orderBy($weight_alias, 'ASC');
    }

    $return = array();
    foreach ($select_query->execute() as $partial_entity) {
      $bundle = isset($partial_entity->bundle) ? $partial_entity->bundle : NULL;
      $entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $bundle));
      $entity->title = $partial_entity->entity_label;
      if ($this->orderByWeight) {
        $entity->weight = $partial_entity->{$weight_alias};
      }
      $return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity;
      $this->ordered_results[] = $partial_entity;
    }
    return $return;
  }

Filter by a date field

Filtering by year or month on content types that have a Date field is sometimes required, and can be another useful addition to your overridden EntityFieldQuery class. The following is based on a Date field that is stored in an ISO format, which is a varchar in most systems. This is an example of the field condition before including the new method into your class.

// The year comes from multiple sources in long (2013) and short (13) formats:
if (drupal_strlen($year) == 2) {
  $year = '20' . $year;
}
$efq = new EntityFieldQuery();
$efq->fieldCondition('field_end_date', 'value', db_like("{$year}-") . '%', 'LIKE');
$result = $efq->execute();

The logic is not overly hard, but it reduces code duplication and improves code readability to encapsulated this into the main GloNodeFieldQuery class.

class GloNodeFieldQuery extends EntityFieldQuery {
  /**
   * Filters the query by year as defined in the field field_end_date.
   */
  public function filterByYear($year) {
    if (drupal_strlen($year) == 2) {
      $year = '20' . $year;
    }
    $this->fieldCondition('field_end_date', 'value', db_like("{$year}-") . '%', 'LIKE');
    return $this;
  }

Now the usage would be:

$efq = new GloNodeFieldQuery();
$efq->filterByYear($year);
$result = $efq->execute();

Far easier to read and reuse!

There are many other great uses for this to handle specific projects, too many to list here, but I hope this has opened your eyes to the power of Entity Field Queries and the potential usefulness of extending these for custom work. Contact us if you see any great examples in the wild that you would like to share.

Comments

Mike's picture

Entity Field Query, and Drupal/PHP in general, is driving me bananas with time zones. Can you get a date from a content type with EFQ in a specific format or is the database always going to spit out UTC dates? My site was running along fine until we went into BST (1 hour ahead of UTC/GMT) and then I realised a lot of my code was broken as it didn't take into account the timezone info. I then modified the code using something like $closure_timezone = $closure_node->field_calendar_date[LANGUAGE_NONE][0]['timezone']; and then format_date but still cannot get it to work. Do you know of any good tutorials out there for working with EFQ and timezones?

Thanks

Add new comment