Custom Views filter plugin in Drupal 9

Let's explore how to create a custom Views filter plugin in Drupal for filtering by start and end date (date range). The filter will have the options form where administrators or whoever has the Administer views permission can configure the start and end date.

Image

As you can see in the screenshot above, the start and/or end date doesn't have to be the timestamp value. It can also be a string containing a relative date. Basically, anything that works with the strtotime() function will work with this filter as well.

So, let's start creating our filter plugin by first implementing the hook_views_data() hook in our *.module file:

/**
 * Implements hook_views_data().
 */
function MY_MODULE_views_data() {
  $data = [];

  $data['views']['start_and_end_date'] = [
    'title' => t('Start and End date - Custom Filter'),
    'filter' => [
      'title' => t('Start and End date - Custom Filter'),
      'field' => 'id',
      'id' => 'start_and_end_date',
    ],
  ];

  return $data;
}

We want our filter to work with all entity types (to be Global), that's why we are using $data['views']. If you want it to work with just for example Nodes you can use $data['node'].

I always append the Custom Filter suffix to the title of my Views plugins, just to be sure that I always know that these are not built-in but custom-created.

Now that we implemented the required Views data hook, we can create the plugin class. The class file should be created inside the src/Plugin/views/filter directory:

<?php

namespace Drupal\MY_MODULE\Plugin\views\filter;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\StringFilter;

/**
 * Filter by start and end date.
 *
 * @ingroup views_filter_handlers
 *
 * @ViewsFilter("start_and_end_date")
 */
class StartAndEndDateFilter extends StringFilter {

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();

    $options['start_date'] = ['default' => NULL];
    $options['end_date'] = ['default' => NULL];

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    $form['start_date'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Start date'),
      '#description' => $this->t('Timestamp or string containing a relative date.'),
      '#default_value' => $this->options['start_date'],
      '#required' => TRUE,
    ];

    $form['end_date'] = [
      '#type' => 'textfield',
      '#title' => $this->t('End date'),
      '#description' => $this->t('Timestamp or string containing a relative date.'),
      '#default_value' => $this->options['end_date'],
      '#required' => TRUE,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    parent::validateOptionsForm($form, $form_state);

    if (strtotime($form_state->getValues()['options']['start_date']) === FALSE) {
      $form_state->setError($form['start_date'], $this->t('Provided value is not a valid timestamp or string containing a relative date.'));
    }

    if (strtotime($form_state->getValues()['options']['end_date']) === FALSE) {
      $form_state->setError($form['end_date'], $this->t('Provided value is not a valid timestamp or string containing a relative date.'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function query() {
    $this->ensureMyTable();

    /** @var \Drupal\views\Plugin\views\query\Sql $query */
    $query = $this->query;
    $table = array_key_first($query->tables);

    $first_day_last_month = strtotime($this->options['start_date']);
    $query->addWhere($this->options['group'], $table . '.created', $first_day_last_month, '>=');

    $first_day_this_month = strtotime($this->options['end_date']);
    $query->addWhere($this->options['group'], $table . '.created', $first_day_this_month, '<=');
  }

}

As you can see we have a very simple form with two textual fields. Our validation is also simple -- we are just checking if strtotime() is returning FALSE which means that entered values cannot be converted to valid timestamp values.

And finally, you can also see in the query() method that we are comparing start and end dates with the created field, but you can change this and use any other timestamp field like changed for example.

Adding custom Views plugins programmatically is useful when you are unable to create required filters by using the built-in filters through the Views UI. Similarly, you can also create custom contextual filters, exposed filters, custom fields, and sort handlers.

… and that’s it! Now go on and create some custom Views filters. And if you need help with anything let me know.

About the Author

Goran Nikolovski is a creator, speaker, open-source contributor, web developer specialized in Drupal and DevOps enthusiast. He is the founder of this website and he enjoys sharing his knowledge and experiences.