Custom Field Handlers for Views 2 (Drupal)

This is a step-by-step example how to create custom field handlers for Views 2 for Drupal. I needed this information myself and all I could find was not going very deep, at least not as deep as I needed it. I learned all the necessary details by reading other handlers that came with the views module (in views/modules) and by extensive use of print_r().

This is the goal: Extending the scheduler module to be able to view a countdown until a node is published or unpublished. Countdown meaning "How many time units until the node is published or unpublished". This would be the perfect job for the views module. Scheduler already exposes its publish_on and unpublish_on fields for use with views.

Defining the supported views version in file scheduler.module:

<?php
function scheduler_views_api() {
 
$info['api'] = 2;
  return
$info;
}
?>

Defining the table and fields in file scheduler.views.inc:

<?php
function scheduler_views_data() {
 
$tables['scheduler']['table']['group'] = t('Scheduler');

 
// how is the scheduler table linked to the nodes
 
$tables['scheduler']['table']['join']['node'] = array(
   
'left_field' => 'nid',
   
'field' => 'nid',
  );

 
// description of the fields (table columns)
 
$tables['scheduler']['publish_on'] = array(
   
'title' => t('Publish on'),
   
'help' => t('Date/time on which the article will be automatically published'),
   
'field' => array(
     
'handler' => 'views_handler_field_date',
     
'click sortable' => TRUE,
    ),
   
'filter' => array(
     
'handler' => 'views_handler_filter_date',
     
'label' => t('Publish on'),
    ),
   
'sort' => array(
     
'handler' => 'views_handler_sort_date',
    ),
  );

 
$tables['scheduler']['unpublish_on'] = array(
   
'title' => t('Unpublish on'),
   
'help' => t('Date/time on which the article will be automatically unpublished'),
   
'field' => array(
     
'handler' => 'views_handler_field_date',
     
'click sortable' => TRUE,
    ),
   
'filter' => array(
     
'handler' => 'views_handler_filter_date',
     
'label' => t('Unpublish on'),
    ),
   
'sort' => array(
     
'handler' => 'views_handler_sort_date',
    ),
  );

  return
$tables;
}
?>

I am not going to explain this code in detail because it is really simple and everything is documented really nice in the views help. Since we just exposed existing DB fields we only needed to define how the scheduler table is to be joined with the node table. For displaying the values we could rely on a predefined handler for date values (views_handler_field_date). That is the code that we need to build upon.

What we need now is a custom handler because there is no predefined handler for our purpose. At first we need to define the field. Let's start with a simple second count down:

<?php
  $tables
['scheduler']['publish_countdown'] = array(
   
'title' => t('Publish countdown'),
   
'help' => t('Time until the article will be automatically published'),
   
'field' => array(
     
'handler' => 'scheduler_handler_field_scheduler_countdown',
     
'click sortable' => FALSE,
     
'timestamp_field' => 'publish_on',
    ),
  );
?>

Note the handler definition (scheduler_handler_field_scheduler_countdown). Now we need to actually implement the handler. Handlers need to live in their own files. So we create a file scheduler_handler_field_scheduler_countdown.inc:

<?php
class scheduler_handler_field_scheduler_countdown extends views_handler_field {
  function
query() {
   
$this->ensure_my_table();
   
$this->node_table = $this->query->ensure_table('node', $this->relationship);
   
$this->field_alias = $this->query->add_field(NULL,
     
'IF(publish_on AND publish_on > UNIX_TIMESTAMP(),
        publish_on - UNIX_TIMESTAMP(), NULL)'
,
     
$this->table_alias . '_' . $this->field);
  }
}
?>

This handler class derives from views_handler_field, the base class for field handlers. All we need to do is override the query() method, because the countdown value can be calculated via the query. The add field method of the query adds a new field to the query (dooh!). We need a field in the query result that is calculated like this: publish_on - UNIX_TIMESTAMP(). The rest of the condition statement is to limit the calculation to time stamps that represent future times. This is the actual query that will be performed to determine the fields for a view containing the countdown:

SELECT node.nid AS nid,
       node.title AS node_title,
       IF(publish_on AND publish_on > UNIX_TIMESTAMP(),
           publish_on - UNIX_TIMESTAMP(),
           NULL) AS scheduler_publish_countdown
    FROM node node
    LEFT JOIN scheduler scheduler ON node.nid = scheduler.nid
    WHERE node.status = 0

We are not done yet. All handlers must be declared in the _views_handlers() hook which can be implemented in the module.views.inc file. So we extend scheduler.views.inc with this function:

<?php
function scheduler_views_handlers() {
  return array(
   
'handlers' => array(
     
'scheduler_handler_field_scheduler_countdown' => array(
       
'parent' => 'views_handler_field',
      )
    )
  );
}
?>

That's all for a simple countdown field. It can be added to views and shows the number of seconds until a node will be published. Our handler only supports a countdown for the publish_on field because that's hard coded into our query. We can provide all the additional parameters we need in the 'field' array:

<?php
  $tables
['scheduler']['publish_countdown'] = array(
   
'title' => t('Publish countdown'),
   
'help' => t('Time until the article will be automatically published'),
   
'field' => array(
     
'handler' => 'scheduler_handler_field_scheduler_countdown',
     
'click sortable' => FALSE,
     
'timestamp_field' => 'publish_on',
    ),
  );
?>

Now we can change the handler like this:

<?php
    $time_field
= $this->definition['timestamp_field'];
   
$this->field_alias = $this->query->add_field(NULL,
     
'IF('.$timestamp_field.' AND '.$time_field.' > UNIX_TIMESTAMP(),
        '
.$time_field.' - UNIX_TIMESTAMP(), NULL)',
     
$this->table_alias . '_' . $this->field);
?>

Since the name of the actual field is now variable we can add a field for a unpublish countdown:

<?php
  $tables
['scheduler']['unpublish_countdown'] = array(
   
'title' => t('Unpublish second countdown'),
   
'help' => t('Time until the article will be automatically unpublished'),
   
'field' => array(
     
'handler' => 'scheduler_handler_field_scheduler_countdown',
     
'click sortable' => FALSE,
     
'timestamp_field' => 'unpublish_on',
    ),
  );
?>

If scheduler will support scheduled promotion/demotion of nodes or any other action, we can add countdown fields really easy.

But we still only have second countdowns which are actually not really that useful. We would like to have all kinds of countdowns, like in minutes, hours, days and weeks. But we are not going to create handlers for all those values and a fixed definition of fields for those values is also not so nice. How about making the field definition and the handler more generic regarding the actual display format of the countdowns. That's where field options come in handy.

Until now the custom field handler is pretty simple because it only manipulates the database query to provide derived fields. Now we want to alter the rendering of the fields. The user should be able to select how the countdown should be displayed, e.g. its units of scale. It should also be possible to select how the unit should be displayed. At first we need to define the options form in the options_form() function of the handler. We want the user to able to select seconds, minutes, hours, days or weeks as unit of scale and we also want a smart mode which figures out which unit would be best for the current value of the countdown. For the display of the units the user should be able to select long units (e.g. "4 days" or "6 weeks"), short units (e.g. "4d" or "6w") or no units at all.

<?php
 
function options_form(&$form, &$form_state) {
   
parent::options_form($form, $form_state);
   
$form['countdown_display'] = array(
     
'#title' => t('Display countdown as'),
     
'#type' => 'radios',
     
'#options' => array(
       
'smart' => t('Smart mode'),
       
'seconds' => t('Seconds'),
       
'minutes' => t('Minutes'),
       
'hours' => t('Hours'),
       
'days' => t('Days'),
       
'weeks' => t('Weeks'),
      ),
     
'#default_value' => $this->options['countdown_display'],
    );
   
$form['units_display'] = array(
     
'#title' => t('Display time units'),
     
'#type' => 'radios',
     
'#options' => array(
       
'long' => t('Long (e.g. 3 days)'),
       
'short' => t('Short (e.g. 3d)'),
       
'none' => t('No units at all'),
    ),
     
'#default_value' => $this->options['units_display'],
    );
  }
?>

This code defines two option fields as radio buttons. We need to tell views that there will be two options with default values:

<?php
 
function option_definition() {
   
$options = parent::option_definition();
   
$options['countdown_display'] = array('default' => 'smart');
   
$options['units_display'] = array('default' => 'long');
    return
$options;
  }
?>

Now the user will be presented this two options, but they will still have no effect. We need to define how the field values will be rendered. A field handler can define a render() function for this task. If there is no such function a value will be displayed as the database query returns it.

Let's define the scale values and the unit names first:

<?php
 
const SECOND_SCALE = 1;
  const
MINUTE_SCALE = 60;
  const
HOUR_SCALE = 3600;
  const
DAY_SCALE = 86400;
  const
WEEK_SCALE = 604800;

  private
$render_params = array(
   
'seconds' => array(
     
'scale' => self::SECOND_SCALE,
     
'singular' => 'second',
     
'plural' => 'seconds',
     
'abbreviated' => 's',
    ),
   
'minutes' => array(
     
'scale' => self::MINUTE_SCALE,
     
'singular' => 'minute',
     
'plural' => 'minutes',
     
'abbreviated' => 'min',
    ),
   
'hours' => array(
     
'scale' => self::HOUR_SCALE,
     
'singular' => 'hour',
     
'plural' => 'hours',
     
'abbreviated' => 'h',
    ),
   
'days' => array(
     
'scale' => self::DAY_SCALE,
     
'singular' => 'day',
     
'plural' => 'days',
     
'abbreviated' => 'd',
    ),
   
'weeks' => array(
     
'scale' => self::WEEK_SCALE,
     
'singular' => 'week',
     
'plural' => 'weeks',
     
'abbreviated' => 'w',
    ),
  );
?>

Because we defined the above array of unit properties we do not need a switch-statement for the render function:

<?php
 
function render($values) {
   
$countdown_display = $this->options['countdown_display'];
   
$value = $values->{$this->field_alias};

   
# what about the "smart" scale?
   
$params = $this->render_params[$countdown_display];

   
$scaled_value = round($value / $params['scale']);
    switch (
$this->options['units_display']) {
      case
'long':
       
$rendered_value = format_plural($scaled_value, '1 '.$params['singular'],
                           
'@count '.$params['plural']);
        break;
      case
'short':
       
$rendered_value = $scaled_value.$params['abbreviated'];
        break;
      case
'none':
       
$rendered_value = $scaled_value;
        break;
    }
    return
$rendered_value;
  }
?>

This code does not handle the "smart" scale option yet. We need to examine the actual value and check if it is larger than the scale value of each unit. We want to use the largest unit possible. E.g. if the value is 30 we want seconds as the unit, if the value is 61 we want minutes as the unit (and so on). Just replace the comment in the code above with the code below:

<?php
   
if ($countdown_display == 'smart') {
      if (
$value > self::WEEK_SCALE) {
       
$countdown_display = 'weeks';
      }
      elseif(
$value > self::DAY_SCALE) {
       
$countdown_display = 'days';
      }
      elseif(
$value > self::HOUR_SCALE) {
       
$countdown_display = 'hours';
      }
      elseif(
$value > self::MINUTE_SCALE) {
       
$countdown_display = 'minutes';
      }
      else {
       
$countdown_display = 'seconds';
      }
    }
?>

That's it. We are done. The user can now include a countdown until a node is published or unpublished in her views. Actually it should be quite easy to use our handler in other modules for other kinds of countdowns.

The final code can be found here: scheduler.views.inc and scheduler_handler_field_scheduler_countdown.inc

Thank you!

Thanks a lot! That tutorial was really helpful.

Only thing I would like to be added for Views 3: you should add

files[] = MYMODULE_handler_field_example_field.inc

to your MYMODULE.info. Otherwise your handler code is not ever get executed, and you get 'broken/missing handler' message.

I have spent a couple of hours trying to figure it out. However, other things worked out of the box!

Thanks!

Hey, that's a really clean and totally awesome post, I'll even bookmark it for any future references.

Thanks again.

Thank you!

Here I found last piece of pazzle to the problem I've been struggling with: necessity of using hook_views_handlers(). Now my code works like charm.

You the man!

final code broken links...

Thank you so much... but your final code is broken links... can you send me the file please... thanks....

Ooops

Thanks for pointing this out. The links broke when drupal switched from cvs to git (quite a while ago). I fixed the links.

Thanks again.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.

Powered by Drupal, an open source content management systemCreative Commons LicenseSyndicate content