Building Views Query Plugins, Part 3

Exposing options and configuration

Welcome to the third part of our series on writing Views query plugins! In part 1, we talked about the kind of thought and design work that needs to be done before coding on the plugin begins. In part 2, we went through the basics of actually writing a query plugin. In this final chapter, we will investigate some enhancements to make your plugin more polished and flexible.

Exposing configuration options

In part 2, we hardcoded things like the ID of the Flickr group we wanted to retrieve photos from, and the number of photos to retrieve. Obviously it would be better to expose these things as configuration options for the user to control.

In order to define configuration options for your plugin you need to add two methods to its class: option_definition() and options_form() (yes, the first is singular and the second is plural.) option_definition() provides metadata about the options your plugin provides, and options_form() provides form elements to be used in the Views UI for setting or modifying these options. Let's look at some code.

  
function option_definition() {
  $options = parent::option_definition();

  $options['num_photos'] = array(
	'default' => '20',
  );
  $options['group_id'] = array(
	'default' => '',
  );

  return $options;
}
  

As you can see, option_definition() is just an info hook, providing data about our options. The only required piece of data we need to provide is a default value, but there are several other options available including special handling for booleans and translations. Check out the full API description for more detail. The one important thing to note is that at the beginning of the function we are calling the parent. This ensures that any options defined by the base class are carried forward into ours. Forgetting to call the parent is a very common source of problems with Views plugins.

  
function options_form(&$form, &$form_state) {
  $form = parent:: options_form($form, $form_state);

  $form['num_photos'] = array(
	'#type' => 'textfield',
	'#title' => t('Number of photos'),
	'#description' => t('The number of photos that should be returned from the specified group.'),
	'#default_value' => $this->options['num_photos'],
  );
  $form['group_id'] = array(
	'#type' => 'textfield',
	'#title' => t('Flickr group ID'),
	'#description' => t('The ID of the Flickr group you want to pull photos from. This is a string of the format ######@N00, and it can be found in the URL of your group\'s "Invite Friends" page.'),
	'#default_value' => $this->options['group_id'],
  );
}
  

Assuming you've done everything correctly, you should now be able the see the following form at Advanced -> Query Settings.








query_plugins_screenshot_1.png

Having implemented these forms, we now need to be able to retrieve the saved values and use them in our query. These values are stored in the 'options' array on your view, and the individual options are keyed just as they are in your form definition (just as if they were being referred to in $form in FAPI).

  
function execute(&$view) {
  $flickr = flickrapi_phpFlickr();
$photos = $flickr->groups_pools_getPhotos($this->options['group_id'], NULL, NULL, NULL, NULL, $this->options['num_photos']);
   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;
  }
}
  

Functionally, of course, this is exactly the same as the last version. However, it is much more flexible and empowers your users to make the changes they need to.

Displaying images

Now we're retrieving data from Flickr, but we're still waiting for the stuff that is the whole point of this exercise: the images! There are a couple things we need to do to make this happen. We need to extend the query code to get the image data out of the Flickr API, and as we discussed in Part 1, getting all the data we need to display an image is a bit of a challenge given Flickr's API. We'll need to do the following:

  • Call flickr.groups.pool.getPhotos to get a list of photos in the group.
  • Iterate through each photo retrieved to get its ID.
  • Call flickr.photos.getSizes for the photo and choose the size we want. The list of sizes is unpredictable, but every photo has an 'Original' size so we will always choose that one.

We will also need to create a new field handler to display the images.

Let's create the field handler first. The setup is the same thing we did before with the Title. First we add an entry to hook_views_data() in flickr_group_photos.views.inc to describe the field we are making available.

  
  $data['flickr_group_photos']['image'] = array(
	'title' => t('Image'),
	'help' => t('The actual image from Flickr.'),
	'field' => array(
	  'handler' => 'flickr_group_photos_field_image',
	),
  );
  

Then we create a new handler called 'flickr_group_photos_field_image' as we have named it above. We will put this in a file called flickr_group_photos_field_image.inc in our handlers directory.

  
/**
 * @file
 *   Views field handler for Flickr group images.
 */

/**
 * Views field handler for Flickr group images.
 */
class flickr_group_photos_field_image extends views_handler_field {

  /**
   * Called to add the field to a query.
   */
  function query() {
	$this->field_alias = $this->real_field;  
  }

}
  

Pretty much the same as our text field handler, but this is just going to return the text of whatever image URL we have, and that isn't what we want. We want to display the actual image! In order to do that we need to override the render() function and rewrite the data we're returning. This function should return the HTML we want to be displayed when we add this field to our view. So we could do something like this.

  
  /**
   * Render the field.
   *
   * @param $values
   *   The values retrieved from the database.
   */
  function render($values) {
	$image_info = array(
		'path' => $values->{$this->field},
	);
	$return = theme('image', $image_info);
  }
  

The most notable thing is how we retrieve the data from our field. The render() function recieves an object with all the data for a specific row in our view, and we retrieve the property named the same as our field, which we retrieve from our instance of the handler object. This makes the code a little more portable since we aren't just hardcoding the name of our field in there. Then we pass this path to theme_image() to generate the output.

This will work, however its not really optimal because it will display the image in its original size, and that will rarely be what we want. We could add the 'width' and 'height' keys to the $image_info array, but that is really suboptimal when we have no idea what our source images will look like. What we really want to do is apply an image style to our image! In theory this would be pretty simple, however Drupal's image styles only work on images that are stored locally, and not having any locally stored files was sort of the entire point of this exercise.

Contrib to the rescue! The Imagecache External module allows you to use core's image styles on external images. Phew. We can implement this in our field by calling theme('imagecache_external') with the path to our image, and the style we want to apply. Here's the newly modified code.

  
  /**
   * Render the field.
   *
   * @param $values
   *   The values retrieved from the database.
   */
  function render($values) {
	$image_info = array(
		'path' => $values->{$this->field},
		'style_name' => 'thumbnail',
	);
	$return = theme('imagecache_external', $image_info);
  }
  

And finally, let's not forget to add this class to our .info file!

  
    files[] = handlers/flickr_group_photos_field_image.inc
  

If you've done everything correctly to this point, you should be able to go into an appropriate view, click Fields->Add, and see the Flickr Groups: Image field available to be added. If you try and add it and get the 'Broken or missing handler' error, then something is mostly likely improperly named somewhere along the way.








query_plugins_screenshot_2.png

OK so the field type is in place, now we need to get the data from our query plugin. This just involves retrieving the new data we need, and saving it to an appropriately named property in our row object.

  
  function execute(&$view) {
	$flickr = flickrapi_phpFlickr();
	$photos = $flickr->groups_pools_getPhotos($this->options['group_id'], NULL, NULL, NULL, NULL, $this->options['num_photos']);
	foreach ($photos['photos']['photo'] as $photo) {
	  $row = new stdClass;
	  $photo_id = $photo['id'];
	  $info = $flickr->photos_getInfo($photo_id);
	  $row->title = $info['photo']['title'];
	  $sizes = $flickr->photos_getSizes($photo_id);
	  foreach ($sizes as $size) {
		if ($size['label'] == 'Original') {
		  $row->image = $size['source'];
		}
	  }
	  $view->result[] = $row;
	}
  }
  

As you can see we've added another loop where we iterate over the available sizes until we hit the one labeled 'Original', and we use the 'source' property of that size as our image property on the row. Pretty simple stuff in the end. Once again, getting the data out of Flickr and into the view is the simple part. It's the pieces that surround and support that which take most of the work.

So having done all this, and clearing cache of course, you should now be able to see titles AND images in your view!








query_plugins_screenshot_3.png

Field options

There's one more thing that's irritating in this code: the image style is hardcoded into the field handler. Wouldn't it be nicer if we could choose which image style we want? Thankfully, fields support options forms just like queries do. In fact, pretty much all views handlers and plugins support this functionality. Just add this code to your flickr_group_photos_field_image class.

  
  function option_definition() {
	$options = parent::option_definition();
	$options['image_style'] = array('' => '-');
	return $options;
  }

  function options_form(&$form, &$form_state) {
	// Offer a list of image styles for the user to choose from.
	parent::options_form($form, $form_state);

	$form['image_style'] = array(
	  '#title' => t('Image style'),
	  '#type' => 'select',
	  '#default_value' => $this->options['image_style'],
	  '#options' => image_style_options(FALSE),
	);
  }
  

Not much to explain there: it looks like the query options we implemented above. After adding this code, you should have an option to choose an image style when you add a Flickr Groups: Image field to your view.

We will also need to tweak the field rendering to use the image style the user has chosen like so.

  
  function render($values) {
	$image_info = array(
			  'path' => $values->{$this->field},
			  'style_name' => $this->options['image_style'],
		  );
		  $return = theme('imagecache_external', $image_info);
  }
  

Now we can have nicely styled images and their titles! Things are really looking nice now, aren't they?

Wrapup

We've covered a lot in this series, and there's so much more we can dig into! While we've looked at a lot of code, I don't think that any of it has been horribly complicated or mind-bending. It's mostly a matter of knowing what to put where, with a healthy dose of planning to make sure our data fits into the Views paradigm properly. In summary, the steps are:

  • Make a plan of attack, taking into account the data you're retrieving and the way Views expects to use it.
  • Create field handlers for your data.
  • Write remote queries to retrieve your data and store it in rows in the view object.

There's a lot of work in those steps, but after running through it a couple times the architecture makes a lot of sense.

Get the code!

I've made the code from this article available on Github! In addition to the functionality described here, it makes a couple more fields available and integrates them into the query engine. Feel free to fork and send pull requests if you find anything wrong or want to add more features.

Thanks for reading and following along. Now, go forth and consume APIs!

Published in:

Get in touch with us

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