fromAugust 2011
Column:

How to Create New Features in Views 3

Creating an Area Handler
0

Views is one of the most extensible subsystems in the Drupal ecosystem, and it has led the way in showing Drupal developers how to create systems that are powerful and extendible. Views 3 has no less than seven different plugin types and six different handler types. In the second installment of his regular column, "How to create new features in Views 3," Views' author Earl Miles walks us through the steps necessary to create an area handler.

Every view contains several components of data that are not just the content itself. In the last issue of Drupal Watchdog we explored the pager portion of the view, which is a list of links to other pages of the same overall content list. In this issue, we're going to talk about the area handlers, which are generic plugins that can put almost any kind of data into the header, footer, or empty text of a view. These areas are typically used to display some kind of information about the data being displayed or, as with the case of empty text, some alternative that explains or works around the lack of data to display. Footers often contain such things as links to add more of the data being listed.

Database RobotIn Views 2, those areas were simply a formatted text field. Using Drupal's PHP input format it was possible to embed code in them, allowing them to be very flexible. However, this input type has several unfortunate drawbacks:

  • Embedding code into the database makes that code difficult, if not impossible to maintain. If it contains bugs (or worse, bugs are revealed much later through changing data conditions), it can be difficult to find these bugs;
  • There is no source control at all, so it is impossible to find the history of the code;
  • Because it uses an internal Drupal code evaluation function, it has no direct access to the view or its data. This can be worked around by using views_get_current_view() but is still somewhat tricky;
  • Allowing PHP input into your database can be a vector for attack;
  • You can only have one of them in a given area;
  • Translation can be very difficult with formatted text.

Views 3 introduced a new method of controlling this part of a view, the pluggable area handler. Much like fields, they can be added from a list, be configured, and re-ordered. Views comes with two simple area handlers: The first embeds text the old fashioned way; the second allows another view to be used. Using another view is a great way to offer more information about an argument, for example. An additional area handler found in Drupal contrib. provides common administrative links.

But what to do in the traditional use of area handlers, where PHP code was embedded directly in the view? Over time we have learned that this is a bad way to maintain a site. Code directly in the database is difficult to locate, debug and maintain. Area handlers to the rescue!

Area handlers may logically seem to be plugins but are actually implemented as handlers in the Views system to be able to take advantage of existing UI elements for multiple use and ordering. This means that instead of using hook_views_plugins() to implement them, you use hook_views_data() and attach them to the global table.

/**
 * Implementation of hook_views_data()
 */
function views_area_sum_views_data() {
  $data['views']['views_area_sum'] = array(
    'title' => t('Sum'),
    'help' => t('Show a row of summed values.'),
    'area' => array(
      'handler' => 'views_area_sum_handler_sum',
    ),
  );
  return $data;
}

The views table is not an actual table, but a special global table defined in modules/views.views.inc:

  // The views table definition from Views itself.
  $data['views']['table']['group'] = t('Global');
  $data['views']['table']['join'] = array(
    '#global' => array(),
  );

You can easily define your own table, of course, or use the views table to have your handler put into the Global group. Either method is perfectly acceptable, but whichever method is chosen you must ensure that the table or the area handler name is created in such a way as to avoid namespace collision! Always use your module name!

Like plugins, handlers are created as a class in a separate file that shares the name of the class. In Drupal 6 you need to implement hook_views_handlers() to tell Views where to look. In Drupal 7 you merely need to add this file to your module's .info file so that the class registry will know where to find it.

/**
 * Implementation of hook_views_handlers()
 * Drupal 6 only.
 */
function views_area_sum_views_handlers() {
  return array(
    'handlers' => array(
      'views_area_sum_handler_sum' => array(
        'parent' => 'views_handler_area',
      ),
    ),
  );
}

For our example, we're going to do something very simple: Go through all of the rendered output, determine which fields have numeric values, and add them up. This is not a particularly useful handler by itself, but it is meant to demonstrate that we can dig into data about the view, manipulate it, and present it in whatever format we want. Since area handlers are mostly about rendering a blob of data, most of the real work happens in the render() method on the area handler class.

class views_area_sum_handler_sum extends views_handler_area {
  function render($empty = FALSE) {
    // Ensure we have an array.
    $this->totals = array();

    // Calculate the total for all numeric fields.
    foreach ($this->view->style_plugin->rendered_fields as $row_index => $row) {
      foreach ($row as $field_id => $value) {
        if (is_numeric($value)) {
          if (!isset($this->totals[$field_id])) {
            $this->totals[$field_id] = 0;
          }

          $this->totals[$field_id] += $value;
        }
      }
    }

    $output = '';

    // Now, if we ended up with results, render them.
    foreach ($this->totals as $field_id => $total) {
      // See template_preprocess_views_view_fields() for the right way to do
      // this.
      // Actually printing a field properly is relatively involved because of
      // all the semantic options. For the purposes of this example we won't
      // do that, but will just brute force it.
      $field = $this->view->field[$field_id];
      $value = check_plain($field->label());
      if ($value) {
        if ($field->options['element_label_colon']) {
          $value .= ': ';
        }
      }
      $value .= $total;

      // Simple div wrapper that should really be configurable and probably
      // taken from the semantic information on the field.
      $output .= '<div>' . $value . '</div>';
    }

    // This should also be configurable.
    if ($output) {
      $output = '<div><strong>' . t('Totals') . '</strong></div>' . $output;
    }
    return $output;
  }
}

As with all handlers and plugins, the full array of methods such as option_definition(), options_form() can be used. They can also take a page from the table style and configure which fields they will draw their data from. That said, Views uses a complex formula to determine the alias for all data in the select query, and it can be difficult to figure out what this alias will be, particularly once relationships become involved. It is therefore safer to try and use the rendered data available on the style plugin. If raw data is truly necessary, field handlers contain a get_value() method that could be handy. But be careful: The format of this data does not have to be simple. For example, most many-to-one fields will return an array, and any Field API field will return a complex array of all data in the database for that field.

The full example code for this article may be found in the Views Plugin Examples module at http://drupal.org/project/views_plugin_examples