How To Create A Custom Filter Handler In Views

For those who work with Drupal the Views-module is probably known as well as Drupal itself. For a good reason: it's simply unbelievable how many amazing things you can do with it, without having to write a single line of code. Views has most of the things you'll need for your queries already baked in and there is a pile of modules extending it, but as always in life there are cases where you need something special. So as an example on how to extend Views this article will show how to create you own custom filter.

Our filter will allow users to filter their result depending on the number of words appearing in the node title. You need to create a basic custom module in your Drupal installation. The one I have in my custom environment is called "my_module". The very first thing that has to be done is to implement a hook in your .module file, telling Views which version of Views we are using, so that your module would know how to include files and handle further hooks later. We're using version 3, so our code looks like this:

/**
 * Implements hook_views_api().
 */
function my_module_views_api() {
  return array(
    'api' => 3,
  );
}

The next step is to literally tell Views that we want to add a new filter. This is done by adding this information in form of an array to the table structure already known by Views. In order to do that we have to implement another hook - "hook_views_data_alter". This hook (and eventually other hooks related to views) has to be implemented in a file called "my_module.views.inc" (of course replace "my_module" with the name of your module). Per default you add this file in the root of your module or in another location, if you specifiy such in "hook_views_api" above with the key of "path". We haven't done that so add the file to the root of your module. Then add following code to the file:

/**
 * Implements hook_views_data_alter().
 */
function my_module_views_data_alter(&$data) {
  $data['node']['title_count']['title'] = 'Title word count';
  $data['node']['title_count']['help'] = 'Count the number of words in titles.';
  $data['node']['title_count']['filter']['handler'] = 'my_module_handler_filter_field_count';
}

The hook gets passed the data structure known to Views per reference, so we just need to add our part to it. It our case we add a field "title_count" into the "node" array, where "node" stands for the node table of the database. Take a look at the documentation of hook_views_data and/or print out the contents of the $data-variable within your hook_views_data_alter-implementation to better understand how this array is structured. We're using "hook_views_data_alter" because we want to add information to a table already known to Views. If we're adding database tables through our module, "hook_views_data" needs to be implemented to fully describe the new table.

In our case, the "node"-table already exists in the array, so we add our "title_count"-Views-field to the it, giving it "title" and "help" for identification in the Views UI. Most importantly we tell Views also that this field should be available as filter and the handler for that filter is called "my_module_handler_filter_field_count" (or whatever makes sense). If we would need to have this field also to appear in the sort section of Views, we would need to add $data['node']['title_count']['sort']['handler'] = 'my_module_handler_sort_field_count'; respectively, but that's not part of our example (yet). At that point you can already see this filter listed in Views:

word_count_filter.jpg

It's not doing anything yet, because we need to add the handler we specified above. This is a two step process: first you need to add the information about the file which contains the handler to your modules' .info file. This way views will know about it and will autoload it whenever needed. Simply add following line to your .info file and clear your cache after that so the Drupal knows about the changes:

files[] = my_module_handler_filter_field_count.inc

We've put the file in the root of the module, but if you have more handlers it's probably a better idea to add it to a subdirectory to keep your files better organized.

So far so good. The last thing is to actually create this handler inside the file we specified above. We are not going to need to create everything from scratch though, because Views already has a handler for numerical filtering, which we will be using as a base for our needs. Because Views is OOP-written what we need to do is to extend the "views_handler_filter_numeric"-class and the methods that it provides to change them to be the way we need them. This is what we end up with (some explanations follow):

class my_module_handler_filter_field_count extends views_handler_filter_numeric {
  function operators() {
    $operators = parent::operators();
    // We won't be using regex in our example
    unset($operators['regular_expression']);
 
    return $operators;
  }
 
  // Helper function to return a sql expression
  // for counting words in a field.
  function field_count() {
    // Set the real field to the title of the node
    $this->real_field = 'title';
 
    $field = "$this->table_alias.$this->real_field";
    return "LENGTH($field)-LENGTH(REPLACE($field,' ',''))+1";
  }
 
  // Override the op_between function
  // adding our field count function as parameter
  function op_between($field) {
    $field_count = $this->field_count();
 
    $min = $this->value['min'];
    $max = $this->value['max'];
 
    if ($this->operator == 'between') {
      $this->query->add_where_expression($this->options['group'], "$field_count BETWEEN $min AND $max");
    }
    else {
      $this->query->add_where_expression($this->options['group'], "($field_count <= $min) OR ($field_count >= $max)");
    }
  }
 
  // Override the op_simple function
  // adding our field count function as parameter
  function op_simple($field) {
    $field_count = $this->field_count();
 
    $value = $this->value['value'];
 
    $this->query->add_where_expression($this->options['group'], "$field_count $this->operator $value");
  }
}

Let's see what's exactly going on here:

  • first we create our own version of the operators()-method, where we get the numerical comparison operators defined in the same method in the parent class (the "views_handler_filter_numeric"-class) and remove the regex comparision from the array (let's say we don't need it in our case). You can also add your own operators by adding an additional array inside (just print out the contents of $operators and see how the parent class adds them). By actually removing the regex comparison we can see that the rest of the operators are calling the methods "op_simple" and "op_between" to do their filtering.
  • the field_count-method is just a helper function for our handler which uses a mix of mysql's LENGTH and REPLACE functions and returns a statement which will be turned into the number of words in a sentence when mysql evaluates it. This method is only here, because we need the same statement in both "op_simple" and "op_between" methods, so it's better store it separately.
  • the last part of our class is to override of the op_simple and op_between methods of the parent class. This is necessary because in the parent class those methods use the "add_where" method of the views_plugin_query_default class. Because we convert the title of the node into a numerical value and add those additional mysql-functions (LENGTH and REPLACE) in the process, we need to do the filtering through another method of the views_plugin_query_default-class called - the "add_where_expression". This method allows us to create more complex expressions so the mysql-functions we added can be evaluated also.

That should be it. You can now add this filter to the a views listing nodes and give it some parameters:

word_count_filter_form.jpg

and that filter should work right away:

word_count_content_list.jpg

This is just an example, but it show the necessary steps to implement a filter in Views. Your filter would probably make way more sense, but the point here is made clear.

If you have additional ideas or find a bug or anything related please leave a comment. I'd be happy to read it. Thanks!

Comments

Jeffrey (not verified)

Thank you!

tomas.teicher (not verified)

thanks for this tutorial. But I have problem to make this work. When I click to Add filter, check " Content: Title word count" and click "apply", then I get this error:
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ') ))) subquery' at line 1

Mohamed Ibrahim (not verified)

Thanks
i try to customize Field from currency field
i use this code
--------------------
function currency_field_views_data_alter(&$data)
{
if (isset($data['field_data_field_currency']))
{
$data['field_data_field_currency']['field_currency_currency']['filter']['handler'] = 'views_handler_filter_currency_field_list';
}
}
--------------------

i wonder if this is the best practise to deal with fileds ?

marwen blel (not verified)

thanks now it works for me

Penjual Obat Herbal (not verified)

it works sir. thanks you.

http://husnaherbal.com/

Alexis (not verified)

Hey, very clear post!, do you guys know where can I find more advanced codes? I need to add a current year (plus +3 , -3) filter to be compared with two fields in my view.

Thanks

Martin Q (not verified)

Mohamed Ibrahim's comment is the right approach if you need your filter to apply to a particular field type (e.g. one that your module provides) rather than to an entity type. In this case, you should not implement hook_views_data_alter() but instead implement hook_field_views_data_alter(). The following is adapted from drupal stackexchange question 123614 (his example deals with specific, known field instances, whereas this assumes the field type can be instantiated on any entity):

/**
* Implements hook_field_views_data_alter().
*/
function yourmodule_field_views_data_alter(&$result, $field, $module) {
if($field['type'] == 'YOUR_FIELD_TYPE'){
$name = $field['field_name'];
foreach ($result as $table_name => $table_data) {
if (isset($table_data[$name]['field'])) {
$result[$table_name][$name]['filter']['handler'] = 'YOUR_VIEWS_HANDLER_FUNCTION';
$result[$table_name][$name]['title'] .= ' (' . $name . ')';
}
}
}
}

Distributor Gold-G (not verified)

An offering of your article is very useful. I really like with all the content of this article, thanks for sharing.

Agen Gold-G (not verified)

Happy to be visiting your blog .. regards blogger.

pio.fernandes (not verified)

Really nice.
Thanks a lot for sharing.

Add new comment