Demystifying Views API - A developer's guide to integrating with Views
Learning to use the Views module from the front end of Drupal is a daunting task, but you can rest assured that many others struggle along with you. There are many tutorials and screencasts dedicated the the subject. Tackling the Views API from the backend, however, is a bit more of a challenge. It's not easy to document the behemoth that is Views, and hence, the documentation is limited.
This blog post is the first of a series that will explore the Views API from the backend-- from the code. Clearly, it's not going to be possible for me to give examples of all the various ways that you can integrate with Views. My goal is to provide you with a general understanding Views, and to give you the same tools that I use to tackle the beast. Namely, a methodology for figuring things out on your own. If you're a developer looking to integrate your module with Views, or if you'd like to build custom Views handlers for your site, then stay tuned.
To Begin
You will need to create a custom module that will house your custom code. I will refer to this as grasmash.module. Once you have your grasmash.info file and a brand, spanking new grasmash.module file ready to go, you'll want to:
Tell Views that your module will be using the Views API
/**
* Implements hook_views_api().
*/
function grasmash_views_api() {
return array(
'api' => 3,
'path' => drupal_get_path('module', 'grasmash') . '/views',
);
}
This snippet of code tells Views that I'll be using version 3 of the Views API, and that it should look for Views-related files in sites/all/modules/grasmash/views. You can adjust the path accordingly, depending on where you'd like to store your custom Views files.
Views expects you to create a file named [your-module].views.inc, so we'll be creating grasmash.views.inc in sites/all/modules/grasmash/views.
Provide your custom data to Views
At its heart, Views is a query builder. It builds a database query, fetches a result, and renders it. So, our first step will be to tell Views where it can find our data. We accomplish this by using hook_views_data(). Start by dropping in a snippet like this:
grasmash.views.inc:
/**
* Implements hook_views_data().
*/
function grasmash_views_data() {
}
hook_views_data() must return an array containing the tables you'd like to query, their fields, and the handlers that will handle the display, sorting, filtering, etc., of those fields.
Now let's find a few examples of hook_views_data() implementations. The best place to find those examples is in the Views module itself. After all, Views author Earl Miles had to integrate Views with core modules like Node, Comment, and User. To see how he did it, we're going to dig through the views/modules sub-directories. First up: the Node module, which is implemented with views/modules/node.views.inc.
Defining a base table to query:
/**
* Implements hook_views_data()
*/
function node_views_data() {
// ----------------------------------------------------------------
// node table -- basic table information.
// Define the base group of this table. Fields that don't
// have a group defined will go into this field by default.
$data['node']['table']['group'] = t('Content');
// Advertise this table as a possible base table
$data['node']['table']['base'] = array(
'field' => 'nid',
'title' => t('Content'),
'weight' => -10,
'access query tag' => 'node_access',
'defaults' => array(
'field' => 'title',
),
);
As you can see, we deliver the data in the form that Drupal loves— arrays inside of arrays (in arrays). You may also notice that there are some helpful comments in this code. Did I add them? No. Views is loaded with great comments like this, you just need to do a bit of exploring to find them!
This snippet starts by telling Views that it should query a table called node. It does this by defining 'node' as the associative key in the $data array. Beneath $data['node'], it starts to get more specific. It indicates that, when {node} is used as a base table in a join, 'nid' should be used as the primary key. It also indicates the table, title, weight, default display field, etc.
The Views API allows you to pass all sorts of information about the data that you'll be querying, but it's not all necessary. For instance, it wasn't necessary for us to indicate the weight or default field for the {node} table. Which fields are necessary? That's a tough question, so I'm just going to say "it depends." You can answer the question best by continuing to look at examples and read the comments in code. Take a look at the following examples:
Joining a table:
// For other base tables, explain how we join
$data['node']['table']['join'] = array(
// this explains how the 'node' table (named in the line above)
// links toward the node_revision table.
'node_revision' => array(
'handler' => 'views_join', // this is actually optional
'left_table' => 'node_revision', // Because this is a direct link it could be left out.
'left_field' => 'nid',
'field' => 'nid',
// also supported:
// 'type' => 'INNER',
// 'extra' => array(array('field' => 'fieldname', 'value' => 'value', 'operator' => '='))
// Unfortunately, you can't specify other tables here, but you can construct
// alternative joins in the handlers that can do that.
// 'table' => 'the actual name of this table in the database',
),
);
Defining a database field
// ----------------------------------------------------------------
// node table -- fields
// nid
$data['node']['nid'] = array(
'title' => t('Nid'),
'help' => t('The node ID.'), // The help that appears on the UI,
// Information for displaying the nid
'field' => array(
'handler' => 'views_handler_field_node',
'click sortable' => TRUE,
),
// Information for accepting a nid as an argument
'argument' => array(
'handler' => 'views_handler_argument_node_nid',
'name field' => 'title', // the field to display in the summary.
'numeric' => TRUE,
'validate type' => 'nid',
),
// Information for accepting a nid as a filter
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
// Information for sorting on a nid.
'sort' => array(
'handler' => 'views_handler_sort',
),
);
Now that looked a bit frightening, but not all fields need quite so much detail. For example:
// changed field
$data['node']['changed'] = array(
'title' => t('Updated date'), // The item it appears as on the UI,
'help' => t('The date the content was last updated.'), // The help that appears on the UI,
'field' => array(
'handler' => 'views_handler_field_date',
'click sortable' => TRUE,
),
'sort' => array(
'handler' => 'views_handler_sort_date',
),
'filter' => array(
'handler' => 'views_handler_filter_date',
),
);
Yet more simple:
$data['node']['path'] = array(
'field' => array(
'title' => t('Path'),
'help' => t('The aliased path to this content.'),
'handler' => 'views_handler_field_node_path',
),
);
You probably get the idea. Most of the array keys make their purpose obvious, but you may still be wondering, "What exactly is a handler?" Great question.
Handlers
Views handlers tell views how it should handle the data that you pass it. After all, there are all sorts of data that you can have in your database. E.g., node ids, arbitrary integers, text strings, dates, etc. Depending on the type of data, you may want to view, sort, filter, or join the data in a different way.
Views comes with a large variety of default handlers located in the views/handlers directory. In most cases, these default handlers will cover the data types that you're working with. These are an excellent resource for learning. I highly recommend that you click through these files to get a sense of the available handlers and how they work.
Sometimes these default handlers aren't enough, and you'll need to write your own custom handler. So, let's talk briefly about their architecture. Handlers are defined using PHP Classes. This is a very good thing; if you'd like to create a custom handler, you can simply extend an existing handler class. There is no need to reinvent the wheel and specify independent methods for construction, rendering, querying, etc. You need only make the modifications or additions that are required for your custom handler— the parents class will take care of defining the rest. Yay for object oriented programming.
Note: for each of the handler examples below, I'll also show you the hook_views_data() array that helps Views find the handler class.
Filter Handler
In this example, we're going to create a Views filter for the Node Ownership module.
A little background information.
This module defines the {nodeownership} table, which contains a 'status' field (among others). The 'status' field will contain an integer with values 0, 1, or 2. These values correspond with the statuses pending, approved, and declined. I want users to be able to filter the nodeownership entities using a dropdown filter with these three options.
Let's start by seeing if there's already a good handler for this field. Browsing through the default views handlers, I see that the views_handler_filter_equality handler class does almost exactly what I need. I could just use it as my handler, but there's one problem— it doesn't know the correct corresponding labels for the status field's integer values. We'll have to make a custom handler to handle this.
First, let's tell Views about the status field in nodeownership.views.inc:
/**
* Implements hook_views_data()
*/
function nodeownership_views_data() {
// ----------------------------------------------------------------
// nodeownership table -- basic table information.
$data['nodeownership']['table']['group'] = t('Node ownership');
// Status.
$data['nodeownership']['status'] = array(
'title' => t('Status'),
'help' => t('The status of a given claim. E.g., pending, approved, or declined.'),
'filter' => array(
'handler' => 'views_handler_filter_nodeownership_status',
'label' => t('Status'),
'use equal' => TRUE,
),
);
Notice that I defined a custom handler class of 'views_handler_filter_nodeownership_status.' That doesn't exist yet. Let's create a views_handler_filter_nodeownership_status.inc file and create that class. We've already determiend that the views_handler_filter_equality handler class does almost exactly what I need. So, let's create our new class by extending views_handler_filter_equality:
/**
* Simple filter to handle equal to / not equal to filters
*
* @ingroup views_filter_handlers
*/
class views_handler_filter_nodeownership_status extends views_handler_filter_equality {
}
Now our new class has inherited everything that was in views_handler_filter_equality. Next, let's skim over the various methods that are contained in the parent class to determine which one we should override. After a quick perusal, It looks like value_form() is responsible for generating the actual exposed filter form. So let's copy, paste, and modify it!
/**
* Simple filter to handle equal to / not equal to filters
*
* @ingroup views_filter_handlers
*/
class views_handler_filter_nodeownership_status extends views_handler_filter_equality {
/**
* Provide a select list for value selection.
*/
function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);
$form['value'] = array(
'#type' => 'select',
'#title' => t('Status'),
'#options' => array(
0 => t('Pending'),
1 => t('Approved'),
2 => t('Declined'),
),
'#default_value' => $this->value,
'#required' => FALSE,
);
}
}
I simply mapped the integer values to their corresponding labels, and we're set! Just clear the caches, and voila! I've got a new, working custom filter handler.
Field Handler
Using the same methodology, I can create a custom field handler with the same small tweak. First, add the information to hook_views_data():
/**
* Implements hook_views_data()
*/
function nodeownership_views_data() {
// ----------------------------------------------------------------
// nodeownership table -- basic table information.
$data['nodeownership']['table']['group'] = t('Node ownership');
// Status.
$data['nodeownership']['status'] = array(
'title' => t('Status'),
'help' => t('The status of a given claim. E.g., pending, approved, or declined.'),
'field' => array(
'handler' => 'views_handler_field_nodeownership_status',
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'views_handler_filter_nodeownership_status',
'label' => t('Status'),
'use equal' => TRUE,
),
);
Then, create a new custom handler with an overridden method:
/**
* Field handler to present an 'accept' link for a given claim.
*
* @ingroup views_field_handlers
*/
class views_handler_field_nodeownership_status extends views_handler_field {
function render($values) {
$value = $this->get_value($values);
$status_map = array(
0 => t('Pending'),
1 => t('Approved'),
2 => t('Declined'),
);
return $status_map[$value];
}
}
Pretty easy stuff!
The wrap up
That should give you a good sense of how Views finds and handles data. Experience is the best teacher, so I suggest that you jump in and try your hand at Views integration.
I will be posting a follow up article that goes a little bit further into Views by focusing on relationships, contextual filters (arguments), and query modification.
Until then, good luck and have fun!
7.x,
drupal, views, views api, backend, handlers