Third & GroveThird & Grove
Jun 17, 2015 - Wes Jones

Using Object-Oriented Principles to extend EntityFieldQuery

 

We’re working on a big project here at Third & Grove where performance is critical. Potentially thousands of users will be accessing this application at the same time. We made the decision to avoid using Views if at all possible. So that left us with figuring out how to create pages like lists of nodes with pagination and filter fields. We decided to extend EntityFieldQuery using some seriously useful Object-Oriented Programming principles.

The Entity API isn’t the only Drupal system that incorporates classes. Views, Apache Solr, Rules, SimpleTest, and many other modules internally use classes to organize code. Here I’ll show you how we extended EntityFieldQuery for custom entity types.

Let's start with an example: Let's say we have an entity person and a bundle/type of employee, and we want to filter on field field_employee_status. In a normal implementation we would call EntityFieldQuery in this way:

 

$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'person')
  ->entityCondition('bundle', 'employee');
$query->fieldCondition('field_employee_status', 'hired');
$results = $query->execute();
$resetted_results = reset($results);
$entity_ids = array_keys($resetted_results);
$entities = entity_load('element', $entity_ids);

 

That’s a mouthful. Let's clean it up a bit.

 

class MyEmployeeQuery extends EntityFieldQuery {
 
  public function __construct() {
    $this->entityCondition('entity_type', 'person')
      ->entityCondition('bundle', 'employee');
  }
 
  public function filterOnEmployeeStatus($status) {
    $this->fieldCondition('field_employee_status', $status);
    return $this;
  }
 
  public function execute() {
    $result = parent::execute();
    $resetted_result = reset($result);
    $resetted_result = entity_load($this->entityType, array_keys($resetted_result));
    $this->ordered_results = reset($this->ordered_results);
    return $resetted_result;
  }
}

 

Now our example code can be rewritten like this:

 

$query = new MyEmployeeQuery();
$query->filterOnEmployeeStatus('hired');
$entities = $query->execute();

 

More more compact and maintainable.

What’s great about inheritance is that all the methods and variables in the parent class are still available to us. So we can add something like this:

 

$this->propertyCondition('status', 1);

 

And it will filter on published status just like EntityFieldQuery.

Here’s another OOP principle we can put to good use: interfaces. An interface is an abstract class that another class implements. All methods defined in the interface are required to be implemented or it will generate an error.

This is useful to use for standardizing the structure of entity classes.

Returning to our example, let’s say we defined this interface.

 

interface MyModuleQueryInterface {
 
  /**
   * Filter on a field or property.
   *
   * @param string $name
   *   The field or property name.
   * @param string $value
   *   The value to compare.
   * @param boolean $operator
   *   An optional operator to pass to propertyCondition().
   *
   * @return MyModuleQueryInteface
   */
  public function filterOn($name, $value, $operator = NULL);
 
  /**
   * Executes the query.
   */
  public function execute();
}

 

That's it. Short and sweet. An interface is only a stub for methods that need to be implemented by the inheriting class. We’ve defined two methods, filterOn() and execute(). Here’s an implementation of filterOn():

 

public function filterOn($name, $value, $operator = NULL) {
  // Convert snake case to camelcase as per the method here:
  // @see http://php.net/manual/en/function.ucwords.php#92092
  $func = 'filterOn' . preg_replace('/(?:^|_)(.?)/e',"strtoupper('$1')", $name);
  if (!method_exists($this, $func)) {
    throw new InvalidArgumentException('Invalid parameter: No method exists called "' . $func . '"');
  }
  $this->{$func}($value, $operator);
  return $this;
}

 

This function checks for the presence of a defined method of the form filterOnCamelCaseFieldName() and will call that.

filterOn() is an abstract method which will allow us to easily call add fields and conditions to filter on. For example, if field_first_name is a field attached to our entity, we would then create a function filterOnFieldFirstName() and use it by calling filterOn('field_first_name').

So in our earlier MyEmployeeQuery class, in usage we would just have to call filterOn('employee_status', 'hired'), and it will automagically run filterOnEmployeeStatus('hired'). Neat!

There’s one more benefit to using interfaces. Let’s say later in development we want to use Apache Solr Search instead to filter entity results. We can extend SearchApiQuery and have it implement MyModuleQueryInterface and then everything will be in the same format.

Here's our new custom Solr query class:

 

class MyEmployeeSolrQuery extends SearchApiQuery implements MyModuleQueryInterface {
 
  filterOn() {
    // implement filterOn() in a Solr way.
  }
 
  execute() {
    // execute and return results the Solr way.
  }
}

 

Here’s our usage example using Solr:

 

$query = new MyModuleSolrQuery();
$query->filterOnEmployeeStatus('hired');
$entities = $query->execute();

 

Once again, here’s that same query using EntityFieldQuery:

 

$query = new MyEmployeeQuery();
$query->filterOnEmployeeStatus('hired');
$entities = $query->execute();

 

Wow – much, much cleaner, and so easy to fall back to MyEmployeeQuery if Solr is unavailable.

This approach has proven highly effective and delightful for us. What we love about this solution is that:

  1. it performs better than using expensive Views objects,
  2. it implements a standard interface that we can use for ANY entity type or query class in our system, and
  3. it builds off of solid OOP programming principles.

That third point is crucially important as we move toward Drupal 8. The whole Drupal community will be transitioning toward a Symfony architecture which is very object oriented. This approach gives us a huge head start and cleaner, more maintainable code today.