EntityFieldQuery: Let Drupal Do The Heavy Lifting (Pt 2)

In the first part of this introduction to EntityFieldQuery, we looked at how simple it was to quickly construct a query with only a few lines of code. In this second part of the series, we'll take a look at some more practical ways to put EntityFieldQuery to use.

Tim Cosgrove, Senior Developer
#Development | Posted

In the first part of this introduction to EntityFieldQuery, we looked at how simple it was to quickly construct a query with only a few lines of code. In this second part of the series, we'll take a look at some more practical ways to put EntityFieldQuery to use.

Our Example Module

For the purposes of this series, I've created a simple module, EntityFieldQuery Example, that demonstrates some uses of EntityFieldQuery. It's hosted on Github; you can grab the source code if you'd like to see the code in more detail or if you'd like to install it yourself.

Some quick notes about what the module contains: in addition to the callbacks and block code described below, the module installs three node types, efq_article, efq_page, and efq_photo. These are very simple node types: efq_article and efq_page each contain a body field for text, while efq_photo contains a single image field. In addition, all three content types contain a US States field, which we'll be using to construct our example queries.

I recommend using Devel content generate to quickly generate content for your examples, although you can of course create it manually if you like. For the purposes of my examples, I created 200 nodes using the following command:

drush genc --types="efq_page,efq_article,efq_photo" 200

When you uninstall this module, it will clean up after itself, removing all created content. I'm thinking of teaching this technique to my cats.

Extending EntityFieldQuery

One of the first things to to remember about EntityFieldQuery is that it is a proper PHP class. The means that you can extend its behavior to suit your needs. For example, when using EntityFieldQuery, you might find that you are often trying to find nodes. If you are trying to find content that you will be listing in public pages and blocks, you probably want it to all be published. Additionally, lists of content tend to be published with the most recent content first.

Let's create our own version of EntityFieldQuery, NodeEntityFieldQuery, with these properties already set:

  1. class NodeEntityFieldQuery extends EntityFieldQuery {
  2.   // define some defaults for the class
  3.   public function __construct() {
  4.     // now we don't need to define these over and over anymore
  5.     $this
  6.       ->entityCondition('entity_type', 'node')
  7.       ->propertyCondition('status', 1)
  8.       ->propertyOrderBy('created', 'DESC');
  9.     // define a pager
  10.     $this->pager();
  11.   }
  12.   public function excludeNode($nid) {
  13.     // code snip; we'll come back to this.
  14.   }
  16. }

This is fairly straightforward. Each time we create a new NodeEntityFieldQuery, a number of things will be already pre-set for us, namely that we're searching for published nodes and we want them returned in a reverse chronological order. Additionally, we instantiated a pager, since we'll most likely want one. If we find ourselves using NodeEntityFieldQuery over and over again, we will save ourselves some time by having all these values pre-set.

Also note that we can extend the class with our own methods. We'll come back to that method later.

A Simple Listing Page

Let's start by creating a simple listing page. This is going to list all nodes of our three example content types in reverse chronological order, as well as providing a pager. First, we'll need to define a menu item and callback for our page:

  1. /**
  2.  * Implements hook_menu().
  3.  */
  4. function efq_example_menu() {
  5.   $items['efq'] = array(
  6.     'title' => 'EntityFieldQuery example: recently published content',
  7.     'page callback' => 'efq_example_listing',
  8.     'access arguments' => array('access content'),
  9.   );
  10.   return $items;
  11. }

Now we can start our callback to provide the listing for us. We're going to keep this very basic to start with: we're just listing all the nodes of our three types, that are published, in reverse chronological order.

  1. function efq_example_listing() {
  2.   // instantiate the query using our extended query object
  3.   $query = new NodeEntityFieldQuery();
  4.   // let our query know which node types we want
  5.   $query
  6.     ->entityCondition('bundle', array('efq_article', 'efq_page', 'efq_photo'));
  7.   // execute the query
  8.   $result = $query->execute();
  9.   $output = array();
  10.   // return the nodes as teasers
  11.   if (!empty($result['node'])) {
  12.     $output['nodes'] = node_view_multiple(node_load_multiple(array_keys($result['node'])), 'teaser');
  13.     $output['pager']['#markup'] = theme('pager', $query->pager);
  14.   }
  15.   else {
  16.     $output['status']['#markup'] = t('No results were returned.');
  17.   }
  18.   return $output;
  19. }

That's all we need to do to provide us with a listing page with a pager. It's not exactly Drupal For Dummies, but it really isn't terribly difficult, and in particular it's very few lines of code to get that result.

Adding Arguments

Of course, it would be more interesting if we could filter this list some. Let's add some arguments to our callback. Our current URL is at efq. Let's set it up so that we can use URLs in the form efq/%state/%node_type as well. We'll have to set up our callback such that both state and node_type are optional, and to be aware of the arguments if they're present.

Our new callback isn't substantially different:

  1. function efq_example_listing($state = NULL, $node_types = array('efq_article', 'efq_page', 'efq_photo')) {
  2.   // instantiate the query using our extended query object
  3.   $query = new NodeEntityFieldQuery();
  4.   // set up some basic parameters
  5.   $query
  6.     ->entityCondition('bundle', $node_types);
  7.   // if $state is defined, add that fieldCondition to the query
  8.   if (!empty($state)) {
  9.     $query->fieldCondition('field_us_state', 'value', $state);
  10.   }
  11.   // execute the query
  12.   $result = $query->execute();
  13.   $output = array();
  14.   // return the nodes as teasers
  15.   if (!empty($result['node'])) {
  16.     $output['nodes'] = node_view_multiple(node_load_multiple(array_keys($result['node'])), 'teaser');
  17.     $output['pager']['#markup'] = theme('pager', $query->pager);
  18.   }
  19.   else {
  20.     $output['status']['#markup'] = t('No results were returned.');
  21.   }
  22.   return $output;
  23. }

In our new callback, we look for a value for $state, and if it is present, we add a fieldCondition to our query. Additionally, we set up $node_types with a default value of an array of all of our content types, and look for a changed value. Note that the condition code is flexible:

  1.   $query
  2.     ->entityCondition('bundle', $node_types);


can be an array, as is the default, or it can be a string, which is how it will come in from the URL. That's fine; the condition will adjust to the type of argument you pass.

Building a Content-sensitive Block

So now we have a listing of our content, and it can accept arguments. It would be nice also if, on our individual nodes, we could find other content for our given state. Let's construct a block that shows the five most recent items from that node's state. Additionally, we should probably provide some links back to the listing pages, again based on state.

Constructing the framework for the block itself is basic Drupal:

  1. /**
  2.  * Implements hook_block_info().
  3.  */
  4. function efq_example_block_info() {
  5.   $blocks['content_by_state'] = array(
  6.     'info' => t('Other Content In This State'),
  7.     'cache' => DRUPAL_NO_CACHE
  8.   );
  9.   return $blocks;
  10. }
  11. /**
  12.  * Implements hook_block_view().
  13.  */
  14. function efq_example_block_view($delta = '') {
  15.   $block = array();
  16.   switch ($delta) {
  17.     case 'content_by_state':
  18.       $block = efq_example_content_by_state();
  19.       break;
  20.   }
  21.   return $block;
  22. }

Now of course we need to populate that block. Here's the function that will do that for us (don't worry, we're going to break this down):

  1. /**
  2.  * Produces content for a block, based on the state of the host node.
  3.  */
  4. function efq_example_content_by_state() {
  5.   $block = array();
  6.   // if we don't have a node to draw from, in our current setup, we won't have a state and can't continue
  7.   if ($node = menu_get_object()) {
  8.     // get the state value
  9.     $field_name = "field_us_state";
  10.     if (!empty($node->$field_name)) {
  11.       $items = field_get_items('node', $node, $field_name);
  12.       $state = $items[0]['value'];
  13.     }
  14.     // only continue if we have a state value
  15.     if ($state) {
  16.       // instantiate the query
  17.       $query = new NodeEntityFieldQuery;
  18.       // limit the query to our established node types
  19.       $node_types = array('efq_article', 'efq_page', 'efq_photo');
  20.       $query->entityCondition('bundle', $node_types);
  21.       // add the state value
  22.       $query->fieldCondition($field_name, 'value', $state);
  23.       // add a small range
  24.       $query->range(0, 5);
  25.       // remove the current node from the query
  26.       $query->excludeNode();
  28.       // execute the query
  29.       $result = $query->execute();
  30.       $output = array();
  31.       $block['subject'] = t('Other Content for @state', array('@state' => $state));
  33.       if (!empty($result['node'])) {
  34.         // return the nodes as teasers
  35.         $nodes = node_view_multiple(node_load_multiple(array_keys($result['node'])), 'teaser');
  36.         $block['content']['nodes'] = $nodes;
  37.         // let's include links to the content listing for convenience
  38.         $node_types = array(
  39.           'efq_article' => 'Articles',
  40.           'efq_page' => 'Pages',
  41.           'efq_photo' => 'Photos',
  42.         );
  43.         $links = array(l(t('All Content for @state', array('@state' => $state)), "efq/$state"));
  44.         foreach ($node_types as $node_type => $node_type_name) {
  45.           $links[] = array(l(t('All @node_type_name for @state', array('@node_type_name' => $node_type_name, '@state' => $state)), "efq/$state/${node_type}"));
  46.         }
  47.         $item_list = array(
  48.           '#items' => $links,
  49.           '#type' => 'ul',
  50.           '#theme' => 'item_list',
  51.         );
  52.         $block['content']['links'] = $item_list;
  53.       }
  54.       else {
  55.         $block['content']['status']['#markup'] = t('No results.');
  56.       }
  57.     }
  58.   }
  59.   return $block;
  60. }

Let's break this down a bit.

  1.   // if we don't have a node to draw from, in our current setup, we won't have a state and can't continue
  3.   if ($node = menu_get_object()) {
  4.     // get the state value
  5.     $field_name = "field_us_state";
  6.     if (!empty($node->$field_name)) {
  7.       $items = field_get_items('node', $node, $field_name);
  8.       $state = $items[0]['value'];
  9.     }
  10.     // only continue if we have a state value
  11.     if ($state) {
  13.     // ....

The beginning of this is quite straightforward. If we don't have a node, there's no point in continuing. Also, we need the node to have a state value, or else there isn't any point in looking for state content for our block.

  1.       // instantiate the query
  2.       $query = new NodeEntityFieldQuery;
  3.       // limit the query to our established node types
  4.       $node_types = array('efq_article', 'efq_page', 'efq_photo');
  5.       $query->entityCondition('bundle', $node_types);
  6.       // add the state value
  7.       $query->fieldCondition($field_name, 'value', $state);
  8.       // add a small range
  9.       $query->range(0, 5);

This looks very similar to our page listing query. We define a new query via NodeEntityFieldQuery, let the query know what kinds of nodes we're looking for, and give it the value for the state field. Additionally, we're limiting the number of results returned to the 5 most recent, since this is a block.

One thing you might want to consider is that you may not want the results listed in the block to include the node you're currently on. This makes sense: if you see the same page you're already reading listed in a sidebar, it can feel sloppy, or else like you're having your time wasted.

Luckily, it is not hard to do this. Remember when we extended EntityFieldQuery to create NodeEntityFieldQuery? Remember this bit?

  1.   public function excludeNode($nid) {
  3.     // code snip; we'll come back to this.
  4.   }

We can add our own methods to our new class. Let's do that now.

  1.   /**
  2.    * If we're on a node, and if the entity_type is node, exclude the local node from the query
  3.    */
  4.   public function excludeNode($nid) {
  5.     if (!$nid) {
  6.       $object = menu_get_object();
  7.       $nid = $object->nid;
  8.     }
  9.     if (!empty($nid) && $this->entityConditions['entity_type']['value'] === 'node') {
  10.       $this->propertyCondition('nid', $nid, '<>');
  11.     }
  12.     return $this;
  13.   }

This is a fairly simple method. If you pass in a node ID it will use it; otherwise it will attempt to use the node ID on the page you're currently on. If you're using EntityFieldQuery to search for nodes, and there is a node ID, this method will tell your query to exclude the current node. The key item is this line:

      $this->propertyCondition('nid', $nid, '<>');

Note the "<>" operator. Normally if you're looking to match a value or set of values, you can leave the operator out of your propertyConditions, fieldConditions, etc., because they're set to "=" or "IN" by default, depending on whether you have a single value or multiple that you're matching against. If you want to use a different operator for your condition, you have to enter it explicitly.

Now that we have our method, we just need to add a quick method call to our block callback function, and we're set:

  1.       // remove the current node from the query
  2.       $query->excludeNode();

Most of the rest of the callback looks quite similar to our page listing callback:

  1.       // execute the query
  2.       $result = $query->execute();
  3.       $output = array();
  4.       $block['subject'] = t('Other Content for @state', array('@state' => $state));
  5.       if (!empty($result['node'])) {
  6.         // return the nodes as teasers
  7.         $nodes = node_view_multiple(node_load_multiple(array_keys($result['node'])), 'teaser');
  8.         $block['content']['nodes'] = $nodes;

We execute our query. We set the block title, since that is going to be the same whether we had content or not. If there are results from our query, we load them and then slot them into the block content.

We do generate a set of links to allow the user to get back to the page listings easily:

  1.         // let's include links to the content listing for convenience
  2.         $node_types = array(
  3.           'efq_article' => 'Articles',
  4.           'efq_page' => 'Pages',
  5.           'efq_photo' => 'Photos',
  6.         );
  7.         $links = array(l(t('All Content for @state', array('@state' => $state)), "efq/$state"));
  8.         foreach ($node_types as $node_type => $node_type_name) {
  9.           $links[] = array(l(t('All @node_type_name for @state', array('@node_type_name' => $node_type_name, '@state' => $state)), "efq/$state/${node_type}"));
  10.         }
  11.         $item_list = array(
  12.           '#items' => $links,
  13.           '#type' => 'ul',
  14.           '#theme' => 'item_list',
  15.         );
  16.         $block['content']['links'] = $item_list;

And finally, because we're relatively thorough, we write a simple message to the block if not results are returned:

  1.       else {
  2.         $block['content']['status']['#markup'] = t('No results.');
  3.       }

This Looks Kind Of Familiar...

If you're an old Drupal hand, you might be wondering why I wouldn't just build this in Views. Indeed, this might look a bit masochistic if you're used to Views. However, there are a few things to consider:

  1. EntityFieldQuery is core. If you are interested in keeping your installation as lean as possible, this is something to consider. Views is a contributed module, and so it automatically adds size and overhead to your Drupal install.
  2. EntityFieldQuery is small. The amount of code required to generate an equivalent listing in Views, compared to what we built above, is much, much greater. To be fair, your Views code will almost certainly be exported and managed via Features, and so in effect you're not writing the Views code anyway. But, that highlights a different point: writing code this way keeps you in code, rather than requiring you to work in a point-and-click interface. For the average developer, this is a very good thing.
  3. EntityFieldQuery is code. You might want to have certain things exposed to change and not others. For example, on a recent project, we were building a site for a client that had site builders on staff. These people were comfortable working with Views, and we wanted to make sure that they had the ability to create and modify views. However, we wanted to also make sure that there was some core functionality that was left untouched. If we had built that core functionality with Views, it would have been exposed to change. By building that core functionality with EntityFieldQuery, we kept it strictly in code, which protected it.

Summing Up and Coming Up

There is a lot of code in this post. If you are interested in understanding this better, I recommend installing the sample module, creating some test content, and playing with the code. I think much of the code is a lot easier to understand in context.

In our next post, we'll look at some more advanced EntityFieldQuery techniques. We'll also look at scenarios when EntityFieldQuery is a good candidate for the job, and some scenarios when EntityFieldQuery is not what you want to use.

Want more EntityFieldQuery? Check out 'OR' Queries with EntityFieldQuery from Fredric Mitchell.

Tim Cosgrove

Senior Developer