Building Views Query Plugins, Part 2

Writing and testing the plugin itself

Welcome to the second installment of our three part series on writing Views query plugins! In part one, we talked about the kind of thought and design work that needs to be done before coding the plugin begins. In this part, we'll start coding our plugin and end up with a basic functioning example.

While writing this article, I realized that creating a functional remote data integration for Views involves not only the query plugin, but also field plugins to expose that data to Views, and potentially filter or argument plugins to limit result sets. That's a lot of code to write, so lets get to it!

Getting Started

There's a joke about writing Views plugins - 10% of the time is spent copying and pasting code, 10% is spent changing class names and array keys, and 80% is spent finding the typo. While probably not strictly true, it does highlight the fact that when you're writing a Views plugin, naming is everything and a single typo in a class name can cause endless grief. In fact, I've actually moved away from a copy/paste paradigm for these plugins, instead building up all the structures by hand with the help of some snippets in my editor. It forces you to actually think through all the keys involved, and vastly reduces the time spent finding all the keys to replace, and the debugging time spent when you miss one somewhere.

There are a lot of steps here, and keep in mind that you will need to do them all in order to begin to actually use your plugin or see any results. This is one of the reasons that writing a plugin can be frustrating - you have to write a ton of very interdependent code before you can even start testing it.

Step 1: Implement hook_views_api()

All that we are doing here is declaring what version of Views we are coding to. Note that this is the only code that is going in our .module file, everything else is loaded on-demand through includes. This is what my hook implementation looks like and yours will be exactly the same, except of course using your own module's name instead of flickr_group_photos.

  
/**
 * Implementation of hook_views_api().
 */
 function flickr_group_photos_views_api() {
   return array(
     'api' => 3.0
   );
 }
  

Step 2: Create a views.inc file

Once hook_views_api() is implemented, Views will automatically look for a file named [module].views.inc in your module's home directory. Plugins, handlers, and other information are exposed through hooks implemented in this file.

Step 3: Implement hook_views_plugins()

The first thing we need to do is describe our plugin to views. This is done by implementing hook_views_plugin() and returning an array of the format $array[plugin_type][plugin_name]. The 'title' and 'help' keys pretty self-explanatory, and will be used in the Views UI and the plugin settings forms. However the 'handler' deserves some close attention. This is the name of the class that you will eventually create to manage queries to your remote service. It should be descriptive, and it should contain the name of the implementing module. We have chosen 'flickr_group_photos_plugin_query' here, and also used this as the plugin name in our array just for consistency. Remember this name, you'll be using it a lot in the future.

For more details, check out the API documentation for hook_views_plugins().

  
/**
 * Implementation of hook_views_plugins().
 */
function flickr_group_photos_views_plugins() {
  $plugin = array();
  $plugin['query']['flickr_group_photos_plugin_query'] = array(
    'title' => t('Flickr Groups Query'),
    'help' => t('Flickr Groups query object.'),
    'handler' => 'flickr_group_photos_plugin_query',
  );
  return $plugin;
}
  

4) Implement hook_views_data()

Usually hook_views_data() is used to describe the tables that a module is making available to Views. However in the case of a query plugin it is used to describe the data provided by the external service. The format of the array is usually $array[table_name]['table'], but since there is no table I've used the module name instead. This array needs to declare two keys - 'group' and 'base'. 'group' is used as a prefix in the Views UI anywhere this plugin's data is referred to. Then the 'base' key is used to describe this as a base table for views, in that it is a core piece of data that Views can be built around (just like nodes, users, and the like.) This data is essentially the same as you described above in hook_views_plugins(), except that it is used in the Views UI whenever you need to choose what kind of data to show. Also pay close attention that the 'query_class' key is the same name as you used in hook_views_plugins() for the 'handler' key. If not, things won't work well (see how those fat fingers can mess you up!)

For more details, check out the API documentation for hook_views_data().

  
/**
 * Implementation of hook_views_data().
 */
function flickr_group_photos_views_data() {
  $data = array();

  // Base data
  $data['flickr_group_photos']['table']['group']  = t('Flickr Groups');
  $data['flickr_group_photos']['table']['base'] = array(
    'title' => t('Flickr Groups'),
    'help' => t('Query Flickr groups.'),
    'query class' => 'flickr_group_photos_plugin_query'
  );
  return $data;
}
  

Step 5: Expose fields

The data you get out of a remote API isn't going to be much use to people unless they have fields they can use to display it. Fields are also exposed in hook_views_data(). The declaration is very similar to the one in hook_views_plugin() - you provide a title, help text, and the name of a handler class for your field. In an ideal world, you could just use one of the default field classes provided by Views and be on your way. However, when working with remote data, there is a change to the query() method that needs to be made. Therefore, what we we will do is subclass the Views base field class with a new class called flickr_group_photos_handler_field and make our changes there. This class will be fine for basic text data, and when we need to handle more complex data, we will extend that class.

Just to get started, lets define a simple text field for the title of a photo. We'll add the following to hook_views_data(), making sure it is above our existing return statement!

  
// Fields
$data['flickr_group_photos']['title'] = array(
  'title' => t('Title'),
  'help' => t('The title of this photo.'),
  'field' => array(
    'handler' => 'flickr_group_photos_handler_field',
  ),
);
  

As you can see, this is very similar to our plugin declaration. We have a key of $data[module_name][field_name], along with some information fields. The title and help fields will be used wherever information about this field is displayed, and we discussed the handler class above.

Next we create our class. This class file should be named [class_name].inc and can live anywhere within the module's root. I like to put plugins and handlers into their own directories, so this is handlers/flickr_group_photos_handler_field.inc

  
/**
 * @file
 *   Views field handler for basic Flickr group fields.
 */

/**
 * Views field handler for basic Flickr group fields.
 *
 * The only thing we're doing here is making sure the field_alias
 * gets set properly, and that none of the sql-specific query functionality
 * gets called.
 */
class flickr_group_photos_handler_field extends views_handler_field {
  function query() {
    $this->field_alias = $this->real_field;
  }
}
  

Finally there is one more very important step. We need to add a reference to this file into our module's .info file, otherwise the autoloader will have no idea how to find it.

  
files[] = handlers/flickr_group_photos_handler_field.inc
  

Forgetting to do this is one of the most common pitfalls I've encountered when building views plugins. It's a very small thing, but very important.

Step 6:Create a class that extends views_plugin_query

After all that setup we're almost ready to finally start interacting with a remote API! We just have one more task to do, and that is to create the class for our query plugin. As mentioned above, I like to put these into a plugins directory. We already named this class above so we need to create plugins/flickr_group_photos_plugin_query.inc, create a class flickr_group_photos_plugin_query that extends views_plugin_query, and again add it to the files array in our .info file. Here is the shell of our class

  
/**
 * @file
 *   Views query plugin for Flickr group photos.
 */

/**
 * Views query plugin for the Flickr group photos.
 */
class flickr_group_photos_plugin_query extends views_plugin_query {

}
  

and our .info file entry

  
files[] = plugins/flickr_group_photos_plugin_query.inc
  

At this point, if you've done everything right and you clear Drupal's cache, you will be able to create a new View and see your new data type available for Views to use. It won't actually do anything, but this is a good place to stop and verify that everything you've done so far is correct. That way if you encounter problems later, you'll at least know that all your setup stuff was good, and it will reduce the potential points of failure.

Step 7: Override the query() and execute() functions

There are two things we need to do in our query class. First we need to override the query() method with an empty one. In normal views operation this is where SQL queries are constructed, and since that doesn't apply to us, we just eliminate that functionality.

  
function query($get_count = FALSE) {  }
  

That was easy. Now for the fun part! We override the execute() function to retrieve our data and save it into a specific format for views. This format is an array of row objects, with properties named the same as the field name we used as they key when we declared the field in hook_views_data(). So in this first example, where we are only returning the photo title, we just need objects with a 'title' property. Here is what the code looks like.

  
function execute(&$view) {
  $flickr = flickrapi_phpFlickr();
  $photos = $flickr->groups_pools_getPhotos('2221193@N21', NULL, NULL, NULL, NULL, 20);
  foreach ($photos['photos']['photo'] as $photo) {
    $row = new stdClass;
    $photo_id = $photo['id'];
    $info = $flickr->photos_getInfo($photo_id);
    $row->title = $info['photo']['title'];
    $view->result[] = $row;
  }
}
  

The most noteworthy thing about this code is how simple it is after all the setup we did. The first thing we do is create a $row object, where we will store all the data we need for this 'row' of data.

As a reminder, we are using the Flickrapi module to simplify our interaction with the Flickr service. Flickrapi provides a flickr object that defines a function for every Flickr API call, with the dots replaced with underscores. So the groups.pools.getPhotos API function becomes a groups_pools_getPhotos() function on the flickr object.

There are a couple of parameters of this call that are worth noting. The first is the ID of the group you are retrieving photos from. In this case, the ID is for the Lullabot Team group, where we store our team photos. The last parameter is the number of photos to retrieve per page. Obviously it would be nice if these parameters could be configured through the Views UI rather than hardcoded, but I set them this way for the sake of simplicity. We will look at how to add query configuration options in the second part of this article.

The groups_pools_getPhotos() function returns an array of photos, however as discussed above, this does not contain the data we need for our purposes. In order to get the photo title, we need to call the photos.getInfo API function (or photos_getInfo() on our flickr object.) This also returns an array of data, which includes our photo's title. This title gets saved in our $row object, and then the row object is saved in the results array of the view object that is passed as a parameter to this function.

That's it! We should now have a simple but fully functioning query plugin that can interact with a flickr group from Views. After installing this module, you should be able to create a new View of type Flickr Group, add a Title field, and get a listing of photo titles.








screen_shot_2013-09-05_at_11.55.27_am.png

Debugging problems

The most common problem you will encounter is seeing the message 'broken or missing handler' when attempting to add a field or other type of handler. This pretty much always points to a class naming problem somewhere. Go through your keys and class definitions and make sure that you've got everything spelled properly, including your include file names and your files[] definition in the .info file.

For debugging actual functionality, watchdog() is your best friend. Writing results to the screen with dpr() or the like will play havoc with the ajax calls in the Views UI, but writing to the events log will get you everything you need.

Summary

Most of the work here actually has nothing to do with interacting with remote services at all - it is all about declaring where your data lives and what its called. Once we get past the numerous steps that are necessary for defining any plugins, the meat of creating a new query plugin is pretty simple.

  • Create a class that extends views_query_plugin
  • Override the query() function to do nothing
  • Override the execute() function to retrieve your data into an object with properties named for your fields, and save that object into the results[] array on the views object.

In reality, most of your work will be spent investigating the API you are interacting with, and figuring out how to model the data to fit into the array of fields that Views expects.

Next steps

In the third part of this article, we'll look at the following topics:

  • Exposing configuration options for your query object
  • Creating advanced field plugins for displaying images
  • Creating optional filter plugins

Stay tuned!

Published in:

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!