Building Energy.gov without Views.

Views is an amazing module. The power it provides to build lists of content from within the UI is amazing. The plugin architecture is complicated but extraordinarily powerful. There are currently 125 Drupal 7 modules that extend the functionality of Views. So why didn't we use it for development of Energy.gov?

Neil Hastings
#Drupal | Posted

Views

is an amazing module. The power it provides to build lists of content from within the UI is amazing. The plugin architecture is complicated but extraordinarily powerful. There are currently 125 Drupal 7 modules that extend the functionality of Views. So why didn't we use it for development of Energy.gov?

When we started development on Energy.gov, we took a step back. This would be our first Drupal 7 project. We took this opportunity to reconsider our development practices, and looked at every module we had been accustomed to using and asked two questions:

  • Does it provide a robust API (i.e. can we use it without the UI)?
  • Do we really need it? Are we using the module just because that's what you use when you create a Drupal site? Are we we pushing the module beyond it's original intent? Does this module try to solve too many problems?

Ok, so the second question is really 4 questions. In the case of Views, we could answer "yes" for the first question, we couldn't for the second set of questions. There was no specific requirement for a visual query builder. In past projects, we often spent as much time investigating quirks about how Views (or any other large module) works as we do in custom development.

In the end, one argument against using Views for our content queries overrode all others: we wanted our client to use Views. Our client had specified that once they received the sites, their own developers would be using Views to build blocks and pages themselves. We knew that if we worked in Views for our own work, the Views we created would eventually be exposed to them, which leads to possibilities of regression and error. We wanted our core querying functionality to continue to function without concern that it might be tampered with. We explained our concerns and our proposed approach to the client, and they agreed to it.

So, what would be used in its place? In short, the answer is EntityFieldQuery. EntityFieldQuery is a class, new to Drupal 7, that allows retrieval of a set of entities based on specified conditions. It allows finding of entities based on entity properties, field values, and other generic entity metadata. The syntax is really compact and easy to follow, as well. And, best of all, it's core Drupal; no additional modules are necessary to use it.

A typical EntityFieldQuery that looks up the 5 most recent nodes of types article, page, or blog created by the current node's author, that are published (a typical "more by this author" query) might look like the following:

  1. // get the current node; we're assuming for this example we know we are on a node.
  2. $node = menu_get_object();
  3. $query = new EntityFieldQuery();
  4. $query->entityCondition('entity_type', 'node') ->propertyCondition('status', 1)
  5. ->propertyCondition('type', array('article', 'page', 'blog'))
  6. ->propertyCondition('uid', $node->uid)
  7. ->propertyOrderBy('created', 'DESC')
  8. ->range(0, 5);
  9. $result = $query->execute();

Note a few things here:

  • All methods of the EntityFieldQuery class generally chain; that is, they return the modified EntityFieldQuery object itself. You may be familiar with this sort of construct from jQuery.
  • Like Drupal 7's query building in general, the methods of EntityFieldQuery have default operators that they assume, and are fairly agnostic in terms of what sorts of values they'll accept. So for example, propertyCondition() has either '=' or 'IN' as its default operator, depending on whether you pass it a string/number or an array as a comparison value. Of course, if you want a different comparison, i.e. '<>' or 'NOT IN' or such, you can pass that in explicitly.
  • Note that we're not querying fields just yet. EntityFieldQuery really starts to shine when you start querying field values, because it takes care of finding the appropriate field table and doing joins for you.

EntityFieldQuery is powerful, but it didn't do everything we wanted it to. We used Organic Groups on this site, and we wanted our queries to be aware of groups without adding that each time. Also, in almost all cases we were querying for nodes which were published, and we often wanted a reverse chronological ordering. Fortunately, EntityFieldQuery is a PHP class, so it is very easy to extend. We created EnergyEntityFieldQuery.

  1. class EnergyEntityFieldQuery extends EntityFieldQuery {
  2. /**
  3.   * apply some defaults to all instances of this object
  4.   */
  5. public function __construct() {
  6. $this
  7. // we’re interested in nodes
  8. ->entityCondition(‘entity_type’, ‘node’)
  9. // Default to published
  10. ->propertyCondition(‘status’, 1)
  11. // default to reverse chronological order
  12. ->propertyOrderBy(‘created’, ‘DESC’);
  13. /* make assumption that we want group content; see method below */
  14. $this->setPrimaryAudienceCondition();
  15. }
  16.  
  17. /**
  18.   * Helper function for querying by topic vocabulary terms.
  19.   * Will do lookup from term names as a convenience; tids are also recognized.
  20.   *
  21.   * @param $topics
  22.   * String, numeric or array; converts to array if necessary
  23.   */
  24. public function setTopicCondition($topics) {
  25. $topics = !is_array($topics) ? array($topics) : $topics;
  26. if (count($topics)) {
  27. // if a term is not numeric, do a lookup for each term and replace it with its tid
  28. foreach ($topics as $idx => $topic) {
  29. // try to find a tid for non-numeric terms
  30. if (!is_numeric($topic)) {
  31. // look it up
  32. $vocab = taxonomy_vocabulary_machine_name_load(‘topics’);
  33. $candidate_terms = taxonomy_get_term_by_name($topic);
  34. foreach ($candidate_terms as $candidate) {
  35. if ($candidate->vid == $vocab->vid) {
  36. $topics[$idx] = $candidate->tid;
  37. }
  38. }
  39. }
  40. }
  41. // field_topic_term is our term reference field for the Topics vocabulary
  42. // once we have converted all our terms to tids, we set them as a field condition for our search
  43. $this->fieldCondition(‘field_topic_term’, ‘tid’, $topics);
  44. }
  45. return $this;
  46. }
  47.  
  48. /**
  49.   * Add the field condition to search by the primary audience field.
  50.   * EnergyEntityFieldQuery makes the assumption that we want content that matches the current group.
  51.   * The class will provide an undo method
  52.   *
  53.   * @param $gid
  54.   * An array or integer for the gid(s) to search the primary audience field
  55.   * based on. If empty, will try to pull current group from the page context.
  56.   */
  57. public function setPrimaryAudienceCondition($gid = NULL) {
  58. if (empty($gid)) {
  59. $current_group = og_context_determine_context();
  60. $gid = $current_group->gid;
  61. }
  62.  
  63. if (!empty($gid)) {
  64. $this->fieldCondition(‘group_audience’, ‘gid’, $gid);
  65. }
  66. return $this;
  67. }
  68.  
  69. /**
  70.   * Unset group content conditions
  71.   *
  72.   * Use this method if you do not want to filter by group content.
  73.   */
  74. public function clearAudienceConditions() {
  75. foreach ($this->fieldConditions as $idx => $fieldCondition) {
  76. $field_name = $fieldCondition[‘field’][‘field_name’];
  77. if (($field_name === ‘group_audience’) || ($field_name === ‘group_audience_other’)) {
  78. unset($this->fieldConditions[$idx]);
  79. }
  80. }
  81. return $this;
  82. }
  83.  
  84. /**
  85.   * If we’re currently on a node, and if the entity_type is node, exclude the local node from the query.
  86.   * This prevents the node the user is viewing from showing up in queries.
  87.   */
  88. public function excludeNode($nid) {
  89. if (!$nid) {
  90. $object = menu_get_object();
  91. $nid = $object->nid;
  92. }
  93. if (!empty($nid) && $this->entityConditions[‘entity_type’][‘value’] === ‘node’) {
  94. $this->propertyCondition(‘nid’, $nid,<>);
  95. }
  96. return $this;
  97. }
  98.  
  99. }

Note that it is also possible to override the protected methods of EntityFieldQuery itself. This may be useful, for example, if you have more complex propertyCondition needs than EntityFieldQuery itself provides.

We found that we were able to solve around 90% of our content listing use cases with EnergyEntityFieldQuery. We build a simple UI to allow users to pass parameters to the class, which allowed them to easily create dynamic query.

Let's run through a simple example of displaying a list of nodes. Remember that this is not sample code. This is real code we are using on Energy.gov.

The easy part. Remember using EnergyEntityFieldQuery allows us to restrict our queries to OG group content and sets the default entity type to node.

$query = new EnergyEntityFieldQuery();

We only want nodes that are rebates.

$query->entityCondition(‘bundle’, ‘rebate’);

EntityFeildQuery has built in paging and table functionality which is trivial to add.

$query->pager(10);

We are pull conditions from the query string to create taxonomy filters.

  1. function energy_rebate_savings_search($filters = array()) {
  2. // This maps the vocabulary machine names to the field names for term references to that vocabulary.
  3. $term_field_map = array(
  4. ‘rebate_provider’ => ‘field_rebate_provider’,
  5. ‘rebate_savings_for’ => ‘field_rebate_savings_for_short’,
  6. ‘rebate_eligibility’ => ‘field_rebate_eligibility_short’,
  7. );
  8. // Get the non ‘q’ parameters
  9. $params = drupal_get_query_parameters();
  10. $param_filters = array();
  11. foreach (array_keys($term_field_map) as $vocab) {
  12. if (isset($params[$vocab])) {
  13. $param_filters[$vocab] = $params[$vocab];
  14. }
  15. }
  16. $filters = array_merge($param_filters, $filters);
  17. // Set the condition for the terms if there are any
  18. foreach ($filters as $filter => $value) {
  19. if ($value != 0) {
  20. $query->fieldCondition($term_field_map[$filter], ‘tid’, $value);
  21. }
  22. }
  23. // Run the query.
  24. $result = $query->execute();
  25. // process and theme the results, not part of this code example
  26. }

This returns to us an array of entity ids (nids in this case) that match the conditions we've given it. Once we have those, we can process them any way we like. Our preferred method is to use Drupal 7's greatly expanded concept of view modes, which we will talk about in a future post in this series.

So, this is all well and good. But how do we get these to display? How do we place them? One of the advantages of Views is that it can provide blocks of your constructed query which become available to the system immediately.

The answer to that, for Energy.gov, is Beans. Bean is a contributed module we developed in the course of this project which creates blocks as entities. In the next post, you will learn how we used the Bean module to replace View blocks, and how Beans can be used to do much more.

Neil Hastings