Drupal 10: Creating Context Aware Plugins

In previous articles I have written about injecting context into context aware plugins and creating custom context providers and wanted to complete the series by writing about creating context aware custom plugins.

The context system in Drupal is a powerful way of injecting dynamic data into plugins without writing code to add that data directly to the plugin itself. Instead of adding custom code to find the current user or the node from the route of the page you can inject the context into the plugin using the context system and add code to make use of that data. Although most commonly used in blocks it can be found in a couple of other plugin types in Drupal core, like the condition plugin for example.

In this article I will go through how to create a context aware plugin, including how to create custom plugins and how to allow that plugin to understand the context_definitions annotation. Once the custom plugin is complete we will render it using a Drupal controller action to prove that the context works correctly.

Let's start by creating a custom plugin, we'll call this plugin ContextThing and it will be used to print out the context passed to it. The first step in creating custom plugins is to create an Annotation class.

Plugin Annotation Class

Annotations are special kinds of comments that have a number of functions in Drupal, but in this case we are using them to inform Drupal that a particular class is plugin.

As an example of annotations in action we can look at defining custom Blocks. To define a block plugin you would start the class annotation with @Block and then add the fields you need to the annotation definition. This would look something like this.

/**
 * Provides an 'Article Header' block.
 *
 * @Block(
 *  id = "mymodule_article_header",
 *  label = "Article Header",
 *  admin_label = @Translation("Article Header"),
 * )
 */
class ArticleHeaderBlock extends BlockBase {

In order to define custom annotations for your custom plugin you need to define an annotation class that extends the Drupal\Component\Annotation\Plugin class. This class defines a number of properties that then are used to set different parameters in your plugin annotation.

Here is the annotation definition for the ContextThing plugin, which defines an ID and name field for the custom plugin.

<?php

namespace Drupal\context_aware_plugin\Annotation;

use Drupal\Component\Annotation\Plugin;

/**
 * Defines a context thing plugin.
 *
 * Plugin Namespace: Plugin\ContextThing.
 *
 * @see plugin_api
 *
 * @Annotation
 */
class ContextThing extends Plugin {

  /**
   * The plugin ID.
   *
   * @var string
   */
  public $id;

  /**
   * The human-readable name of the context thing plugin.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $name;

}

Using this setup we can add the following annotation to a class in order to denote it as a ContextThing plugin.

/**
 *
 * @ContextThing(
 *   id = "user_context_thing",
 *   name = @Translation("User")
 * )
 */

This annotation doesn't do much on its own, so let's look at the next step of creating a plugin manager.

Plugin Manager

Before we can create our custom plugin we need a way to manage the creation of the custom plugin objects within Drupal. This is done though a plugin manager service that brings together the annotation with the other information needed for the plugin (like where the files are kept).

The plugin manager is defined in the modules *.services.yml file but definition of this service is slightly different to normal services in that we also include a "parent" keyword, which points to an abstract service called default_plugin_manager. An abstract service is quite like an abstract class in that you can't create it directly, but you can extend it to use the same dependencies as the parent.

services:
  plugin.manager.context_aware_plugin.context_thing:
    class: Drupal\context_aware_plugin\Plugin\ContextThingManager
    parent: default_plugin_manager

The ContextThingManager class defined in this service must extend the DefaultPluginManager class. The class doesn't actually need to do a lot, it just needs to know what sort of plugin is being created and where to find them within the Drupal codebase.

<?php

namespace Drupal\context_aware_plugin\Plugin;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Component\Plugin\FallbackPluginManagerInterface;

/**
 * A plugin manager class for context thing plugins.
 *
 * @package Drupal\context_aware_plugin\Plugin
 */
class ContextThingManager extends DefaultPluginManager implements FallbackPluginManagerInterface {

  /**
   * Constructs a ContextThingManager object.
   *
   * @param \Traversable $namespaces
   *   An object that implements \Traversable which contains the root paths
   *   keyed by the corresponding namespace to look for plugin implementations.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Cache backend instance to use.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to invoke the alter hook with.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct(
      'Plugin/ContextThing',
      $namespaces,
      $module_handler,
      'Drupal\context_aware_plugin\Plugin\ContextThing\ContextThingInterface',
      'Drupal\context_aware_plugin\Annotation\ContextThing'
    );
    $this->alterInfo('context_thing_info');
    $this->setCacheBackend($cache_backend, 'context_thing_info_plugins');
  }

  /**
   * {@inheritdoc}
   */
  public function getFallbackPluginId($plugin_id, array $configuration = []) {
    return 'user_context_thing';
  }

}

There are a few parameters being passed to the __construct method here, so let's break them down a little.

  • 'Plugin/ContextThing' is the name of the directory to find plugins of this type. This means that if anyone wants to define a ContextThing plugin then it must be in this directory structure inside the module "src" directory.
  • $namespaces is an injected dependency that is a traversable object of all of the discovered namespaces in the project. This is essentially a list of directories that the manager can look at to discover plugins. This is passed to the plugin manager automatically by Drupal and so we just pass it upstream to the DefaultPluginManager class.
  • $module_handler is another injected service that allows access to the core "module_handler" service. This service allows control of modules in the system, but also allows us to create our own alter hooks to that plugin information can be altered by third party modules.
  • 'Drupal\context_aware_plugin\Plugin\ContextThing\ContextThingInterface' is the default interface defined for the plugin. We haven't defined this yet, but it will be kept in the same directory as the other ContextThing objects.
  • 'Drupal\context_aware_plugin\Annotation\ContextThing' is the where plugin annotation is kept.

The alterInfo() method is used here to set a alter hook name. In this instance we are setting this to "context_thing_info", which means that users can alter the definitions of any ContextThing plugins by using the hook "hook_context_thing_info_alter()".

We also set the prefix of "context_thing_info_plugins" to the current cache backend.

Our class also implements the FallbackPluginManagerInterface, which defines a method called getFallbackPluginId() that returns the ID of a default plugin that can be used to prevent errors if an assigned custom plugin doesn't exist for some reason. We will just add a placeholder of "user_context_thing" for now, which doesn't exist, but we will be building that plugin in the next step.

Now we have the plugin manager we can define the plugin interface and a base class for the plugins to extend from.

Plugin Interface And Base Class

The plugin manager requires the use of an interface for our ContextThings, so we need to define that first. An interface defines what methods the plugin must have, and for the ContextThing plugins we are defining a single method called renderContext(). This method will return a string that will represent the context being injected into the plugin.

<?php

namespace Drupal\context_aware_plugin\Plugin\ContextThing;

use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

/**
 * Defines the interface for context thing plugins.
 *
 * @package Drupal\context_aware_plugin\Plugin
 */
interface ContextThingInterface extends PluginInspectionInterface, ContainerFactoryPluginInterface {

  /**
   * Render the context.
   *
   * @return string
   *   The rendered context.
   */
  public function renderContext():string;

}

Next we need to define a base class for the plugin. This is an abstract class that allows us to abstract away any boiler plate code like constructors out of the actual plugin classes.

This abstract class is where we setup the plugin to be context aware. By implementing the Drupal\Core\Plugin\ContextAwarePluginInterface interface we can get Drupal to call the create() method whilst generating the plugin object. In the create method we use a service called context.repository and call a method called setDefinedContextValues() to create the contexts added to the plugin, as defined in the plugin annotation.

<?php

namespace Drupal\context_aware_plugin\Plugin\ContextThing;

use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\PluginBase;

/**
 * Provides a base abstract class for ContextThings.
 */
abstract class ContextThingBase extends PluginBase implements ContextThingInterface, ContextAwarePluginInterface {
  use ContextAwarePluginTrait;
  use ContextAwarePluginAssignmentTrait;

  /**
   * The context repository service.
   *
   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
   */
  protected $contextRepository;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $object = new static(
      $configuration,
      $plugin_id,
      $plugin_definition
    );
    $object->contextRepository = $container->get('context.repository');
    $object->setDefinedContextValues();
    return $object;
  }

  /**
   * Set values for the defined contexts of this plugin.
   */
  protected function setDefinedContextValues() {
    // Fetch the available contexts.
    $available_contexts = $this->contextRepository->getAvailableContexts();

    // Ensure that the contexts have data by getting corresponding runtime
    // contexts.
    $available_runtime_contexts = $this->contextRepository->getRuntimeContexts(array_keys($available_contexts));
    $plugin_context_definitions = $this->getContextDefinitions();

    foreach ($plugin_context_definitions as $name => $plugin_context_definition) {
      // Identify and fetch the matching runtime context, with the plugin's
      // context definition.
      $matches = $this->contextHandler()
        ->getMatchingContexts($available_runtime_contexts, $plugin_context_definition);
      $matching_context = reset($matches);

      // Set the value to the plugin's context, from runtime context value.
      $this->setContextValue($name, $matching_context->getContextValue());
    }
  }

}

There is a little bit going on in the setDeinfedContextValues() method as we are also making use of the Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait and Drupal\Core\Plugin\ContextAwarePluginTrait traits to call methods like getContextDefinitions(), contextHandler() and setContextValue(). What this code is essentially doing is finding out the current contexts available in Drupal and matching them with contexts we defined in our plugin annotation in the "context_definitions" attribute. If any of the contexts match then we set them to the loaded contexts of the currently created plugin.

With the base class in place we can then start by creating our first ContextThing plugin.

Creating A Concrete ContextThing Plugin

The actual creation of a ContextThing plugin is simple at this point, we just need to define the plugin with the @ContextThing annotation and define the class with the renderContext() method. We are also making use of the context_definitions attribute in the annotation to inject the current user context into the plugin.

Here is the plugin code in full.

<?php

namespace Drupal\context_aware_plugin\Plugin\ContextThing;

/**
 * Provides a user context thing plugin.
 *
 * @ContextThing(
 *   id = "user_context_thing",
 *   name = @Translation("User"),
 *   context_definitions = {
 *     "user" = @ContextDefinition("entity:user", label = @Translation("User"))
 *   }
 * )
 */
class UserContextThing extends ContextThingBase {

  /**
   * {@inheritdoc}
   */
  public function renderContext():string {
    $user = $this->getContextValue('user');
    return $user->label();
  }

}

Let's make use of this newly created plugin

Using The Plugin

Now that we have the custom plugin working we need a way of actually using it in our Drupal application. The simplest way to do this is to load the plugin in a Drupal controller action and use the renderContext() method of the plugin to generate content from the context in our action output.

Here is the route definition for the controller action to create a page with the path /context-thing-page. This would be added to the modules *.routing.yml file.

custom_contexts_context_thing_page:
  path: '/context-thing-page'
  defaults:
    _controller: '\Drupal\context_aware_plugin\Controller\ContextThingController::testContextThingPage'
  requirements:
    # Deliberately setting the access rights to be open as this is an example.
    _access: 'TRUE'

The code of the controller needs to implement the create() method in order to inject our ContextThing plugin manager into the controller object. This is just a case of injecting the "plugin.manager.context_aware_plugin.context_thing" service that we created earlier in this article.

Our controller action method (called testContextThingPage()) uses the ContextThing plugin manager to generate an instance of the "user_context_thing" plugin and render it into a render array.

<?php

namespace Drupal\context_aware_plugin\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * A controller to demonstrate embedding blocks with context.
 */
class ContextThingController extends ControllerBase {

  /**
   * The context_thing plugin manager.
   *
   * @var \Drupal\Component\Plugin\PluginManagerInterface
   */
  protected $contextThingManager;

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container) {
    $object = parent::create($container);
    $object->contextThingManager = $container->get('plugin.manager.context_aware_plugin.context_thing');
    return $object;
  }

  /**
   * Callback for the route custom_contexts_context_thing_page.
   *
   * @return array
   *   The render array.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public function testContextThingPage() {
    $userContextThing = $this->contextThingManager->createInstance('user_context_thing');
    $context = $userContextThing->renderContext();
    return [
      '#markup' => '<p>ContextThing output: ' . $context . '</p>',
    ];
  }

}

When a user visits the page at /context-thing-page they will see the result of the context plugin, which in this case will print out their username. It's important to note that we haven't done more to the "user_context_thing" plugin than add the context_definitions parameter to the annotation. Everything else about injecting the context into the plugin and generating the user object is handled by upstream code.

Conclusion

The context system in Drupal is quite powerful, and you can easily extend it with custom contexts, which makes this technique of having plugins that understand the contexts really useful. Although we did have to set up quite a bit of boiler plate code to get this working, the actual ContextThing plugins are quite simple and consist of just one or two lines of code.

This technique doesn't appear to be used much outside of Drupal core, but it makes sense to use it since it provides powerful integration with Drupal context. If you know of any modules using this technique then please let me know in the comments.

If you want to see all of this code in action then I have created a public github repo that contains all of the code created for these context articles. This includes a separate sub-module called context_aware_plugin that has all of the ContextThing code collected together. Feel free to download it and adapt it to your needs.

More in this series

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
15 + 3 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.