A Views handler, the easy way.

If you are like me, you have avoided diving into coding for the black hole that is Views. Every time I started to look at the mass amount of documentation, I went blind. There is no shortage of documentation and examples. Besides looking for how other modules have implemented Views, the following documentation is available.

If you are like me, you have avoided diving into coding for the black hole that is Views. Every time I started to look at the mass amount of documentation, I went blind. There is no shortage of documentation and examples. Besides looking for how other modules have implemented Views, the following documentation is available.

Even with all of this documentation, something didn't quite click. Sure, I could do the easy stuff, like expose a new table to views or manipulate an existing view, but when it came it creating a custom handler, I was lost. So today I'm sharing with you an example of a new custom handler. We had the need to create a modification of the taxonomy term filter that allowed for using multiple vocabularies. We also needed it to be in an exposed filter to be used in an administration screen. Let's start by first looking at what is different, then at the current filter.

  • Change the selection element selection to a checkbox element.
  • Keep all of the current functionality of the term selction

From the 10,000 foot view (no pun intented) that's it. Using the wonderful world of objects, we are going ot utilize the current handler, only overriding a few functions. Let's start at the beginning. The first thing you have to do is tell Views that your module is going to using the Views API. This is done using hook_views_api():

  1. /**
  2.  * Implementation of hook_views_api
  3.  */
  4. function MY_MODULE_views_api() {
  5. return array(
  6. ‘api’ => 2,
  7. );
  8. }

Now that was easy. This tells views to look in mymodule.views.inc for what do to next. Since we are going to need a new handler, we will need to use hook_views_handlers() and explain to Views the name of our handler, along with the handler we are extending. We place this in mymodule.views.inc:

  1. /**
  2.  * Implementation of hook_views_handlers() to register all of the basic handlers
  3.  * views uses.
  4.  */
  5. function mymodule_views_handlers() {
  6. return array(
  7. ‘info’ => array(
  8. ‘path’ => drupal_get_path(‘module’, ‘MY_MODULE’),
  9. ),
  10. ‘handlers’ =>; array(
  11. // The name of my handler
  12. ‘my_module_handler_filter_term_node_tid’ => array(
  13. // The name of the handler we are extending.
  14. ‘parent’ => ‘views_handler_filter_term_node_tid’,
  15. ),
  16. ),
  17. );
  18. }

Views now knows to look for the class called mymodule_handler_filter_term_node_tid in my main module directory within a file called mymodule_handler_filter_term_node_tid.inc. If you wanted to place the include file within your 'inc' directory, just add it to the 'path' value of the array. As with adding any other field, argument, or filter to Views, we need to implement hook_views_data(). We are not adding a new table or fields, just adding a new filter based upon an existing field. We will be querying against the tid field in the term_node table. The implemenation of hook_views_data() below was copied directly from the implemenation of the original filter with two modifications. The use of 'real field' tells views that the name of the field on the table is different than the name of the key in the array. The other change is the name of the handler. It matches what we used in hook_views_handlers():

  1. /**
  2.  * Implementation of hook_views_data().
  3. */
  4. function MY_MODULE_views_data() {
  5. $data[‘term_node’]span>[‘term_multiple’] = array(
  6. ‘group’ => t(‘Taxonomy’),
  7. ‘title’ => t(‘Term Name (multiple)),
  8. ‘help’=>; t(‘Term from multiple vocabularies’),
  9. ‘real field’ => ‘tid’,
  10. ‘filter’ => array(
  11. ‘handler’ => ‘my_module_handler_filter_term_node_tid’,
  12. ‘hierarchy table’ => ‘term_hierarchy’,
  13. ‘numeric’ => TRUE,
  14. ‘skip base’ => ‘term_data’,
  15. ‘allow empty=> TRUE,
  16. ),
  17. );
  18. return $data;
  19. }

You can now see the filter showing up in the Views UI. Clicking on it would produce an error since we haven't created our handler yet. Here comes the fun part! Let's create our class.

  1. class my_module_handler_filter_term_node_tid
  2. extends views_handler_filter_term_node_tid {
  3. }

Notice that we still have to exend our class even though we told Views it was going to be our parent.  hook_views_handlers() is used to make sure the correct files are included. By extending the views_handler_filter_term_node class, we have all of the functions at our disposal. Take a minute to look at those fuctions now in the Views API. Our plan involves utilizing the current handler for all data handling and only manipulating the interface. Pay closest attention to extra_options_form and value_form. These won't be the only functions in our class, but they are the most used. First let's create our extra options form. This is the form displayed when you click on the gears. We start here because the data feeds our value form.

  1. function extra_options_form(&$form, &$form_state) {
  2. // Get the parent’s $form array.
  3. parent::extra_options_form($form, $form_state);
  4. // Change the radio to select
  5. $form[‘vid’][#type’] = ‘select’;
  6. $form[‘vid’][#multiple’] = TRUE;
  7. // Remove what we don’t want
  8. unset($form[‘markup_start’]);
  9. unset($form[‘type’]);
  10. unset($form[‘hierarchy’]);
  11. unset($form[‘markup_end’]);
  12. return $form;
  13. }

The function extra_options_form() needs to return an array in the format of the Form API. This is nice and very familiar to all Drupal developers. Since less code is more, we pull the form array from views_handler_filter_term_node_tid and manipulate it. I'm not going into an OO lesson but using the syntax parent::() calls that function in the object you are extending. In this case we called the extra_options_form() function. This passes the $form array by reference. We made few small changes to the $form array and returned it. We only need to list the vocabularies, so we remove the unnecessary fields. One important note is that we changed the format of the values in the vid element. Since we did this, we need to tell views about it. This is done using the options_definition() function. The options_definition function defines the variables used in the forms. We are changing the vid to an array and removing the type. First we get the parent classes options.

  1. function option_definition() {
  2. // Get the parents options
  3. $options = parent::option_definition();
  4. // Change the vid to an array.
  5. $options[‘vid’] = array(default=> array(0));
  6. // Remove what we don’t need.
  7. unset($options[‘type’]);
  8. return $options;
  9. }

Great! Things are moving along nicely. The extra options form is not fully functional. Next, we move to the value form. There are too many differences between the parent classes value form and ours, so we will not be pulling in the parent item's $form array. We only have one element on our value form: a select box that has the tree structure of the selected vids from the extra options form. This should be very easy for most Drupal developers to follow. You have flexibility in how you would like the element to display.

  1. function value_form(&$form, &$form_state) {
  2. $vocabulary = array();
  3. $options = array();
  4. $default_values = array();
  5.  
  6. // Cycle through the vids entered in the extra options form
  7. foreach ($this->options[‘vid’] as $vid) {
  8. // Load the vocab.
  9. $vocabulary = taxonomy_vocabulary_load($vid);
  10. // Load the term tree.
  11. $tree = taxonomy_get_tree($vid);
  12. // Create the $options array what will be used in our select
  13. if ($tree) {
  14. $options[$vid .:0] =**. $vocabulary->name .**;
  15. foreach ($tree as $term) {
  16. $options[$vid .:. $term->tid] =
  17. str_repeat(-, $term->depth + 1) .‘ ‘.
  18. $vocabulary->name .:. $term->name;
  19.  
  20. if (in_array($term->tid, $this->options[‘value’])) {
  21. $default_values[$vid .:. $term->tid] =
  22. $vid .:. $term->tid;
  23. }
  24. }
  25. }
  26. }
  27.  
  28. // We are just using a select to keep things simple.
  29. $form[‘value’] = array(
  30. #type’ => ‘select’,
  31. #title’ => t(‘Select the Terms’),
  32. #options’ => $options,
  33. #default_value’ => $default_values,
  34. #multiple’ => TRUE,
  35. #size’ => min(9, count($options)),
  36. );
  37.  
  38. // If this is a exposed form then call the helper options form
  39. if (empty($form_state[‘exposed’])) {
  40. // Retain the helper option
  41. $this->helper->options_form($form, $form_state);
  42. }
  43. }

At this point, we have a working extra options form and we can display our value form. Through teh magic of objects, we have the operators and reduce duplicate options on our value form with all of the included functionality. Before the value form will function, we need to figure out how to manipulate the $form values to work with our parent class. To do this we find out how views__handler__filter__term__node__tid wants the value sent to the submit function. Upon examining the validate functions, we notice that they both call a function named validate_term_strings(). This function returns a string of tids separated by commas. This string is then assigned to the value. So all we need to do is format the tids as a comma separated string. If you examined the code for the value form, you would have noticed that the $options array had a key of vid:tid. We will need to separate this out. Since there are two different functions for the value form and exposed value form, we will use a helper function:

  1. function format_tids($input) {
  2. if (!is_array($input)) {
  3. $values[$input] = $input;
  4. } else {
  5. $values = $input;
  6. }
  7. $output = array();
  8. // Just make sure no vocab items are selected
  9. foreach ($values as $key => $value) {
  10. list($vid, $tid) = explode(:, $value);
  11. // We really only want to save the tid.
  12. // The vid is used to make the select box
  13. if ($tid) {
  14. $output[$tid] = $tid;
  15. }
  16. }
  17. return $output;
  18. }

Now, the value form validate function. Since this is an object, we call the helper function by using $this->function_name:

  1. function value_validate(&$form, &$form_state) {
  2. $values = $form_state[‘values’][‘options’][‘value’];
  3. $form_state[‘values’][‘options’][‘value’] =
  4. $this->format_tids($values);
  5. }

Finally, we validate the exposed form. With exposed forms, the name of the identifiers can be changed in the UI. We pull the identifier value from "$this->options['expose']['identifier']". The validation function needs to be assigned to the validated_exposed_input variable of the object.

  1. function exposed_validate(&$form, &$form_state) {
  2. if (empty($this->options[‘exposed’])) {
  3. return;
  4. }
  5. $identifier = $this->options[‘expose’][‘identifier’];
  6. $values = $form_state[‘values’][$identifier];
  7. $tids = $this->format_tids($values);
  8. if ($tids) {
  9. $this->validated_exposed_input = $tids;
  10. }
  11. }

That's it!! We now have a fully functional views handler. Below is the complete class.

  1. class my_module_handler_filter_term_node_tid
  2. extends views_handler_filter_term_node_tid{
  3. function option_definition() {
  4. // Get the parents options
  5. $options = parent::option_definition();
  6. // Change the vid to an array.
  7. $options[‘vid’] = array(default=> array(0));
  8. // Remove what we don’t need.
  9. unset($options[‘type’]);
  10.  
  11. return $options;
  12. }
  13.  
  14. function extra_options_form(&$form, &$form_state) {
  15. // Get the parent’s $form array.
  16. parent::extra_options_form($form, $form_state);
  17.  
  18. // Change the radio to select
  19. $form[‘vid’][#type’] = ‘select’;
  20. $form[‘vid’][#multiple’] = TRUE;
  21. // Remove what we don’t want
  22. unset($form[‘markup_start’]);
  23. unset($form[‘type’]);
  24. unset($form[‘hierarchy’]);
  25. unset($form[‘markup_end’]);
  26.  
  27. return $form;
  28. }
  29.  
  30. function value_form(&$form, &$form_state) {
  31. $vocabulary = array();
  32. $options = array();
  33. $default_values = array();
  34.  
  35. // Cycle through the vids entered in the extra options form
  36. foreach ($this->options[‘vid’] as $vid) {
  37. // Load the vocab.
  38. $vocabulary = taxonomy_vocabulary_load($vid);
  39. // Load the term tree.
  40. $tree = taxonomy_get_tree($vid);
  41.  
  42. // Create the $options array what will be used in our select
  43. if ($tree) {
  44. $options[$vid .:0] =**. $vocabulary->name .**;
  45. foreach ($tree as $term) {
  46. $options[$vid .:. $term->tid] =
  47. str_repeat(-, $term->depth + 1) .‘ ‘.
  48. $vocabulary->name .:. $term->name;
  49.  
  50. if (in_array($term->tid, $this->options[‘value’])) {
  51. $default_values[$vid .:. $term->tid] =
  52. $vid .:. $term->tid;
  53. }
  54. }
  55. }
  56. }
  57.  
  58. // We are just using a select to keep things simple.
  59. $form[‘value’] = array(
  60. #type’ => ‘select’,
  61. #title’ => t(‘Select the Terms’),
  62. #options’ => $options,
  63. #default_value’ => $default_values,
  64. #multiple’ => TRUE,
  65. #size’ => min(9, count($options)),
  66. );
  67.  
  68. // If this is a exposed form then call the helper options form
  69. if (empty($form_state[‘exposed’])) {
  70. // Retain the helper option
  71. $this->helper->options_form($form, $form_state);
  72. }
  73. }
  74.  
  75. function value_validate(&$form, &$form_state) {
  76. $values = $form_state[‘values’][‘options’][‘value’];
  77. $form_state[‘values’][‘options’][‘value’] =
  78. $this->format_tids($values);
  79. }
  80.  
  81. function exposed_validate(&$form, &$form_state) {
  82. if (empty($this->options[‘exposed’])) {
  83. return;
  84. }
  85.  
  86. // Get the identifier vlaue
  87. $identifier = $this->options[‘expose’][‘identifier’];
  88. $values = $form_state[‘values’][$identifier];
  89. $tids = $this->format_tids($values);
  90. if ($tids) {
  91. $this->validated_exposed_input = $tids;
  92. }
  93. }
  94.  
  95. function format_tids($input) {
  96. if (!is_array($input)) {
  97. $values[$input] = $input;
  98. } else {
  99. $values = $input;
  100. }
  101. $output = array();
  102.  
  103. // Just make sure no vocab items are selected
  104. foreach ($values as $key => $value) {
  105. list($vid, $tid) = explode(:, $value);
  106. // We really only want to save the tid.
  107. // The vid is used to make the select box
  108. if ($tid) {
  109. $output[$tid] = $tid;
  110. }
  111. }
  112. return $output;
  113. }
  114. }

Neil Hastings