Sep 07 2016
Sep 07

A recent project involved the use of the Simple Hierarchical Select module to input category data for a particular content type. Simple Hierarchical Select provides a clean way of browsing hierarchical vocabularies to easily add taxonomy terms to nodes.

An initially tricky user interface problem to utilise this module with Search API and Views exposed filters was solved using a couple of Drupal 8 plugins and a bit of smart thinking!

A recent project involved the use of the Simple Hierarchical Select module to input category data for a particular content type. Simple Hierarchical Select provides a clean way of browsing hierarchical vocabularies to easily add taxonomy terms to nodes.

The module works great and does exactly what it says on the tin, however it created something of a head-scratcher as the same project, which was using Apache Solr via Search API, required this data to be searchable by the end user. Creating a view of the indexed data and then exposing the various filters to be included in the search was the obvious answer and initially it was felt that it should be pretty straightforward.

For a number of the exposed filters, it was straightforward; although some required the use of hook_form_alter to make some alterations to the output of some exposed filters such as changing boolean filters from a True/False selection to Yes/No.

Unfortunately, the simple hierarchical select filter was not available as an exposed filter for Search API index views. A few approaches were made to try and resolve this including using hook_form_alter and trying to force the exposed filter to make use of simple hierarchical select. In the end, a solution was found that was actually more simple but perhaps not quite immediately obvious and required creating two plugins; a Search API processor plugin and a Views filter plugin.

Because the taxonomy that required to be searchable was hierarchical in nature, it is quite possible that the user making a search may want to do a search on higher level categories and return results that included sub-categories. For example, the top level categories could be Business and Community and below Business may be sub categories such as Manufacturing, Transport and Retail and then perhaps below Retail would be child categories such as Food, Clothing, Consumer Electronics. A user searching on Retail would expect results to be returned that have been categorised with Food, Clothing or Consumer Electronics rather than simply those categorised as Retail.

The Search API processor plugin is needed to add the parent terms of each taxonomy term to the index so that the views filter would be able to refer to those parent terms when the search query is generated.

It looks something like this:

namespace Drupal\search_api_demo\Plugin\search_api\processor;

use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Adds an additional field containing the term parents.
 *
 * @SearchApiProcessor(
 *   id = "term_parents", 
 *   label = @Translation("Term parents"),
 *   description = @Translation("Adds all term parents for directory field."),
 *   stages = {
 *     "pre_index_save" = -10,
 *     "preprocess_index" = -30
 *   }
 * )
 */
class TermParents extends ProcessorPluginBase {

  /**
   * Term storage.
   *
   * @var \Drupal\taxonomy\TermStorageInterface
   */
  protected $termStorage;

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $plugin->setTermStorage($container->get('entity_type.manager')->getStorage('taxonomy_term'));
    return $plugin;
  }

  protected function setTermStorage(TermStorageInterface $storage) {
    $this->termStorage = $storage;
  }

  public function preprocessIndexItems(array &$items) {
    foreach ($items as $item) {
      foreach ($this->filterForPropertyPath($item->getFields(), 'field_category) as $field) {
        foreach ($field->getValues() as $tid) {
          foreach ($this->termStorage->loadAllParents($tid) as $term) {
            $field->addValue($term->id());
          }
        }
      }
    }
  }

  public function calculateDependencies() {
    parent::calculateDependencies();
    $this->addDependency('config', 'field.storage.node.field_category);
    $this->addDependency('config', 'taxonomy.vocabulary.category');
    return $this->dependencies;
  }
}

​The most interesting part of this class is the preprocessIndexItems method. When Search API indexes the data, the processor plugin iterates through each item to be indexed, filtering out fields other than field_category, which contains the term id stored by the particular node and the field we are interested in working with. It loads the parents of the term, by term id, and adds the id of each parent to the index using the addValue method. This method is basically storing each term id value in the item field, in this case field_category.

Note that the indexed item mentioned above isn’t an entity, it is referring to a Search API Item. This item could as easily be a user as a node, taxonomy term, file, comment or any other piece of data that Search API is able to index and it represents data that is being indexed or returned as a search result.

Incidentally, this functionality is already available in the Drupal 7 version of Search API and progress is being made on porting this to Drupal 8.

The second plugin is the Views filter itself. This is in two parts, the plugin itself and a hook that lets Views know about the filter. Firstly, the hook:

/**
 * Implements hook_views_data_alter().
 */ 
function search_api_demo_views_data_alter(array &$data) {
  $data['search_api_index_default']['shs'] = [
    'title' => t('Category filters'), 
    'help' => t('SHS filter for category'), 
    'filter' => [
      'field' => 'field_category’, 
      'id' => search_api_demo_shs_taxonomy_index_tid_depth',
    ],
  ];
}

The array key search_api_index_default refers to the Default Search API index or if you were creating a custom filter for node data, you would want to use node_field_data to refer to the database table instead. The field key is the important item in this array and refers to the actual field, field_category, we want to be able to filter on. shs is a reference in Views to the field_category field and can be set to anything. Lastly, the id key is the plugin we want to use.

Alternatively if you simply wanted to create a filter that would provide a text filter for a particular database, or Search API, field, this id could be set to String which would use the core Views string filter plugin and you’d be finished now. As we have a specific requirement for our filter, we need to create a plugin, which has been given the id search_api_demo_shs_taxonomy_index_tid_depth.

The plugin code is simple and looks as follows:

namespace Drupal\search_api_demo\Plugin\views\filter; 

use Drupal\search_api\Plugin\views\filter\SearchApiFilterTrait; 
use Drupal\search_api\UncacheableDependencyTrait; 
use Drupal\shs\Plugin\views\filter\ShsTaxonomyIndexTidDepth; 

/**
 * Filter handler for taxonomy terms with depth. 
 * 
 * @ingroup views_filter_handlers 
 * 
 * @ViewsFilter("search_api_demo_shs_taxonomy_index_tid_depth") 
 */ 
class SearchAPIDemoFilter extends ShsTaxonomyIndexTidDepth { 

  use UncacheableDependencyTrait; 
  use SearchApiFilterTrait; 

  public function query() {
    if ($value = reset($this->value)) {
      $this->getQuery()
        ->addCondition('field_directory_type', $value);
    }
  }
}

The magic happens in two places. Firstly the class extending Simple Hierarchical Select’s ShsTaxonomyIndexTidDepth views filter, which does all the hard graft of creating the hierarchical select widget, and secondly SearchApiFilterTrait, a trait which gives access to Search API filter methods and lets us add our filter as a condition to the search query, as can be see in the query() method in the code above.

All this results in turning the default select list that only works on the current term id...

...to something more flexible and easier to use, and that respects taxonomy hierarchy:

Search API Drupal 8 Plugins Views
Jul 18 2016
Jul 18

As part of our code review process for a current project, it was suggested that rather than calling the static Drupal formBuilder function to insert a form into a custom block, we actually inject the *Form Builder service* directly into the module, and for bonus points also inject the renderer service.

I'd previously had exposure to dependency injection earlier on the same project but hadn’t exactly grokked the concept fully and so with a few pointers in the right direction, I set about refactoring the code I’d written using dependency injection and Drupal services.

Dependency Injection is defined as: "A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern."

Drupal.org defines a service as "any object managed by the services controller". These services provide useful reusable functionality. Examples of services provided by Drupal are the current_user service which gives access to the current user, or email.validator that provides a method of, obviously, validating email addresses. There any many, many services provided by Drupal and the entire list can be found in core.services.yml.

It is also possible to define your own custom services and share them through your own contrib modules, and Drupal.org has a great guide on how to do this and Drupal Console makes it easy to scaffold a service.

Drupal 8 plugins are small pieces of functionality that have mostly replaced the concept of hooks that we used in Drupal 7. A block is now a plugin; field widgets and field types, image formatters and image effects are all plugins.

We’re going to look at using services with a Block plugin and how I injected the Form Builder service into the plugin to embed a form into a custom block.

Firstly we need to define a Block plugin, which was scaffolded using the Drupal Console command drupal generate:plugin:block.

namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * @Block( * id = "example_form_block", 
 * admin_label = @Translation("Example Form Block"), 
 * ) 
 */
 class ExampleFormBlock extends BlockBase {
   public function build() {
     $build = [];
     $build['example_form_block']['#markup'] = 'Implement ExampleFormBlock.';
     return $build;
   }
}

This will create a simple block that does nothing but output "Implement ExampleFormBlock" as its content, however we want our block to do a little more than just that.

Using Drupal Console again, we can simply scaffold a basic contact form using the command drupal generate:form, which contains nothing more than name and message fields.

class ExampleForm extends FormBase {

  public function getFormId() {
    return 'example_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state, $placeholder = NULL) {

    $form['name'] = [
      '#title' => $this->t('Name'),
      '#type' => 'textfield',
      '#maxlength' => 64,
      '#size' => 64,
    ];

    $form['message'] = [
      '#title' => $this->t('Message'),
      '#type' => 'textarea',
      '#rows' => 5,
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }    
}

This form won't actually do anything but works well as an example.

To use the form builder service or, in fact, any service, we need to inject that dependency into our plugin and we can do this by implementing the ContainerFactoryPluginInterface in our block plugin. The ContainerFactoryPluginInterface allows plugins to pull dependencies from the service container and provides a create method to our plugin which we can use to inject whichever services we require.

We also need to create a constructor method to store the services we wish to inject in class properties. Updating our code to do this looks like the following:

namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a 'ExampleFormBlock' block.
 *
 * @Block(
 *  id = "example_form_block",
 *  admin_label = @Translation("Example Form Block"),
 * )
 */
class ExampleFormBlock extends BlockBase implements ContainerFactoryPluginInterface {

  protected $formBuilder;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $formBuilder) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->formBuilder = $formBuilder;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('form_builder')
    );
  }

  public function build() {
    $build = [];
    $build['example_form_block']['#markup'] = 'Implement ExampleFormBlock.';
    return $build;
  }

}

And simple as that, we've injected the Form Builder service into our Block plugin. You could of course inject other services into the plugin, for example the Renderer service, by creating additional class properties, and adding the service to the services container as we did with injecting form_builder.

We are now able to utilise the Form Builder service in our plugin, and in this example, we're going to use the service to get our previously scaffolded form. Because the Form Builder service is now available, it's as simple as updating the build() method in our Block plugin to call the getForm() method:

public function build() {
  $form = $this->formBuilder->getForm('Drupal\example\Form\ExampleForm');
  return $form;
}

On refreshing the page our custom block has been placed on, you will see our contact form embedded in the block.

Wrapping up

As you can see, dependency injection in Drupal 8 is not difficult to implement with plugins and offers a powerful way of taking advantage of services provided by Drupal core and also contrib modules. You can read up on more advanced topics with Drupal services and dependency injection in a blog post by Lee Rowlands.

Drupal 8 Dependency Injection Refactoring

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web