Creating a custom processor for facets

Nov 30, 2015

In this post, I’ll demonstrate how to create a custom facets processor for drupal 8.

One of the new concepts, added in drupal 8, is the Plugin API. A plugin is a PHP class that can add new functionality to an already existing system. Examples in core are: Views fields, Views filters, text formats, image formatters, validation rules and many others. This API is widely used in contrib as well and learning how to use it will be useful for all sorts of modules, including Search API, Facets, Display Suite, Rules and more.

For facets we’re using plugins for multiple concepts, we have them for the following:

  • Widgets
  • Facet Sources
  • Query types
  • Processors

We’ll start by focussing on the plugin you will probably be implementing the most: Processors. A processor in drupal 7 was a sort widget, an url processor or a filter. While an URL processor still is somewhat harder to implement than a filter or sort widget, it’s simpler to do.

In this blog post I’ll show you how to create a new “filter” processor, this processor we’re going to be building is a processor that hides all results that start with a configurable character. We’re going to use several steps to get to our result. First of all, we’re going to have a look at the annotation for the processor, next we’re going to make the processor hide all results that start with the letter ‘A’. When we verified that this works, we’re going to add the configuration to change the character.

Processor annotiation

The first thing you should know about a plugin is that it is defined with a PHP annotation. To see how we should implement this for a processor, we’ll open up src/Annotation/FacetsProcessor from the facets module, this class describes the annotation. All the public properties in the annotation class should be set on the annotation used on the plugin. Some of the properties on the annotation (such as deriver) already have a default set, so they don’t explicitly need to be defined on the annotation.

Here’s the annotation that we’ll use for the demo processor.

<?php

/**
 * Provides a processor that hides results start with a configurable character.
 *
 * @FacetsProcessor(
 *   id = "hide_start_with",
 *   label = @Translation("Hide start with some letter"),
 *   description = @Translation("Hide all results that start with a configurable character"),
 *   stages = {
 *     "build" = 40
 *   }
 * )
 */
 class HideStartWithProcessor {}

You can see that we use @Translation in the annotation, this is a wrapper around the Drupal t function that allows for translation of the label and description. The stages defined in the annotation is an array with the stages where the processor can run and the default weight of that processor. In this case the processor can run in the build fase and it will run fairly late in the process.

Adding the functionality in the processor.

The processor we’re going to implement is one that changes the result set and returns a subset of it. To provide this kind of processor, we’re going to implement a BuildProcessor. This means we’re going to have to look at the src/Processor/BuildProcessorInterface and implement all the required methods. In this case, that’s only one method: ::build. We’ll start by implementing the processor in a very basic form; that hides all results that start with the letter ‘A’.

<?php

namespace Drupal\hide_starts_with\Plugin\facetapi\processor;

use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;

class HideStartWithProcessor extends ProcessorPluginBase implements BuildProcessor {
  public function build(FacetInterface $facet, array $results) {
    /** @var \Drupal\facets\Result\ResultInterface $result */
    foreach ($results as $id => $result) {
      if (strpos(strtolower($result->getDisplayValue()), 'a') === 0) {
        unset($results[$id]);
      }
    }
    return $results;
  }
}

You can see that we’re also extending the ProcessorPluginBase, this is a class that makes working with processors easier as it already defines part of the methods needed for the processor. This is all the code we need to get this processor to work.

  • Create a class in the right folder (src/Plugin/facets/processor) with the annotation with the code provided;
  • Enable the module;
  • Create a facet;
  • Apply the processor the the facet;
  • Make sure the facet is added to the page you want it to be shown, trough the block placement UI;
  • Profit.

All results that start with the character ‘a’ will no longer be shown.

Adding configuration to the processor

However, we set out to create a processor that has configuration. This is a little bit harder to do, but still not that big of a deal. You will have to supply a yaml file containing the processors’s settings first, so lets do that. Creating a yaml file in the config/schema folder in your module. In this example that is: config/schema/hide_starts_with.schema.yml.

The contents of this file are:

plugin.plugin_configuration.facets_processor.hide_starts_with:
  type: mapping
  label: 'Settings for the hide-starts-with processor'
  mapping:
    character:
      type: string
      label: Character

We’ve introduced a new configuration for the processor, every time you provide new configuration schema files, it’s wise to reinstall the module to be sure they are picked up correctly by drupal. A simple drush oneliner to do this is drush pmu hide_starts_with -y; drush en hide_starts_with -y. Having something like this in your bash history is really useful while developing a module that provides new configuration.

The next step is defining a configuration form for the newly added configuration. You can do this by implementing the ::buildConfigurationForm method on the processor and returning a form from it.

<?php

class HideStartWithProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
  public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
    $processors = $facet->getProcessors();
    $config = isset($processors[$this->getPluginId()]) ? $processors[$this->getPluginId()] : null;

    $build['character'] = [
      '#title' => $this->t('Character to hide'),
      '#type' => 'textfield',
      '#default_value' => !is_null($config) ? $config->getConfiguration()['character'] : $this->defaultConfiguration()['exclude'],
      '#description' => $this->t("All results that start with this character will be hidden."),
    ];

    return $build;
  }
}

Of course this will not change our build method, so we have to update that one as well.

<?php

class HideStartWithProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
  public function build(FacetInterface $facet, array $results) {

    $processors = $facet->getProcessors();
    $config = $processors[$this->getPluginId()];

    $char = $config->getConfiguration()['character'];

    /** @var \Drupal\facets\Result\ResultInterface $result */
    foreach ($results as $id => $result) {
      if (strpos(strtolower($result->getDisplayValue()), $char) === 0) {
        unset($results[$id]);
      }
    }
    return $results;
  }
}

You could add a setting in the form to make sure disable the usage of the strtolower call if you’d want. That would mean that you’d add another setting in the schema.yml and change the configuration form and the build method to take the new setting into account. This processor can be extended with as much extra functionality as you can imagine.

Seeing all the code wired together.

All the code quoted here is also available on github. If you want to test it on a drupal 8 install, run $ git clone https://github.com/borisson/facetapi-processor-blogpost/ modules/custom/hide_starts_with from the drupal root.

Is there any processor that you think will be useful for a lot of people and should get added in the main Facet module? You can always file an issue on drupal.org and we’ll gladly take a look at the code or the idea you have.

Hopefully this makes the processors easier to understand.




Updated Dec 06, 2015:

#2624862 - Change the code namespace to facets landed at this weekends’ belgian user group sprint thanks to the awesome work done by mr.baileys. This means we needed to change all occurences of the facetapi namespace to facets.



Updated Dec 21, 2015

#2616108 - Integration with the plugin project landed this weekend so updates to the code were needed. A oneline fix to the plugin configuration is needed, see the commit on github for the exact change.