Drupal 10: Using Context Definitions To Create Context Aware Plugins

When developing plugins in Drupal a common task is to inject context so that certain tasks can be performed.

For example, we might have a block plugin that needs to know about the content entity of the page it is currently being rendered on. We could potentially inject the routing system into the plugin and use this to find the currently loaded entity, but there is a drawback to this. There is a fair amount of custom logic involved in finding the content entity from the route and we would need to use this same code every time we want to solve this problem.

This method also hard codes the route into the plugin and so makes it difficult to select any other sort of context. There is also a problem when it comes to caching as you also need to inform the plugin about the use of routing so that the caching layers know how to cache things.

This is where context definitions come in. We can use this system to automatically inject context awareness into plugins and then select how that context is detected. This also takes care of the cache systems so we don't need to worry about that.

In this article I will go through using context definitions to detect different types of entities in a block and how caching is handled though context. I'll also show how to configure where the context comes from via the block configuration page.

Adding Context To A Block

Let's take a simple block that renders a field from a node entity. We can place this block onto the site, but it don't show anything as we don't currently have access to the node object.

<?php

namespace Drupal\mymodule\Plugin\Block;

use Drupal\Core\Block\BlockBase;

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

  /**
   * {@inheritdoc}
   */
  public function build() {
    $node = NULL; // <- Need to retrieve this from the system.

    if (!($node instanceof NodeInterface)) {
      return [];
    }

    $build = [];

    // Render the article header using the 'default' display mode.
    $image = $node->get('field_article_header')->view('default');

    // Pass the rendered field to the render array.
    $build['header'] = $image;

    return $build;
  }

}

I have previously talked about using dependency injection to load the node from the current route, but what we want to do here is use the context definition to find the node object.

The context definition is injected into the block annotation using the context_definitions attribute. In our case we want to inject the node into the context of the block using the following structure.

context_definitions = {
  "node" = @ContextDefinition("entity:node")
}

This is added to the block annotation like this.

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

With this in place we can now tell the block to pull the node object from the context.

This can be done by using the getContext() method, which is a part of the block plugin class.The trait Drupal\Core\Plugin\ContextAwarePluginTrait is injected into the block plugin class to allow this.

The getContext() method returns the context we are asking for, which in this case is the 'node' context. We can then use this context object to get the node entity that we need.

/** @var \Drupal\Core\Plugin\Context\Context $node */
$cacheContext = $this->getContext('node');

/** @var \Drupal\node\NodeInterface $node */
$node = $cacheContext->getContextValue();

This can be simplified by using the getContextValue() method, which wraps the above objects into a single method call.

$node = $this->getContextValue('node');

We now have access to the node object and can use this to render the information we need in the block.

The default context we used here is the node route context (as defined in the class Drupal\node\ContextProvider\NodeRouteContext) and so the node we load here is from the route service. The NodeRouteContext object has code that understands everything about how the node object is stored in the route, including what happens when we look at revisions or the preview page. It handles all of the edge cases that are usually never thought about when injecting the node object into blocks via the route.

During the setup of the NodeRouteContext object it also injects information into the cache system used by blocks. This means that we no longer need to override the getCacheTags() or getCacheContext() methods as they are context aware and will figure out the cache based on the generated contexts.

Adding Context Definitions Annotations

Let's look at some different ways in which we can add context definitions using annotations.

Minimum Context Definition Setting

The minimum amount of information required for the context_definitions annotation to work is as follows.

context_definitions = {
  "node" = @ContextDefinition("entity:node")
}

We have already seen this in action. The "entity:node" part tells the context definition what type of object we are trying to access. Other options might be "entity:user" or "entity:taxonomy_term".

The Entity Key Is A Custom String

    The key can be set to any value we like. For example, we could set this to "article" like this.

    context_definitions = {
      "article" = @ContextDefinition("entity:node")
    }

    With this in place we call the getContextValue() method using the string we set previously.

    $article = $this->getContextValue('article');

    Optional And Required Contexts

    Contexts are required to be present by default, but it is possible to set this context to be optional using the required attribute.

    context_definitions = {
      "node" = @ContextDefinition("entity:node", required = FALSE)
    }

    Adding A Label

    The route context is automatically selected as a default for node entities since it is the only type of context built into Drupal core. If more than one context provider exists for the node entity then it must be selected using configuration. This is also the case if the context is set to be optional.

    For blocks, the block configuration form exposes any contexts that needs to be configured and the label attribute is used to add a label that context form.

    context_definitions = {
      "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
    }

    This has the following effect, which can be seen on the block configuration form.

    A screenshot of the Drupal block config form, showing the context definition being surfaced into the form..

    Different Types Of Entity

    A number of different entity types are built into Drupal and we can supply more than one of these contexts to the context definition at the same time. The following example allows us to load the currently loaded node or taxonomy term as well as the current user and the current language.

    context_definitions = {
      "node" = @ContextDefinition("entity:node", label = @Translation("Node"), required = FALSE),  
      "taxonomy_term" = @ContextDefinition("entity:taxonomy_term", label = @Translation("Term"), required = FALSE),
      "current_user" = @ContextDefinition("entity:user"),
      "language" = @ContextDefinition("language", label = @Translation("Language"))
    }

    We can load these contexts just like any other context.

    $node = $this->getContextValue('node');
    $term = $this->getContextValue('taxonomy_term');
    $user = $this->getContextValue('current_user');
    $language = $this->getContextValue('language');

    Note that for the language context there are a couple of different ways in which this context can be determined. These are the interface language and the content language, and because there are more than one the context must first be selected in the block configuration screen before you are able to use the context.

    You can also inject multiple different types of the same entity into the context definition setup.

    context_definitions = {
      "article" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node")),
      "page" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node"))
    }

    Although this is valid syntax it doesn't differentiate between one content type and the other. The default behaviour of this is to set the currently loaded node to be present in both the "article" and "page" contexts. It is possible to load different entities of the same type in different ways using custom context providers. This requires an additional context provider to be inserted into Drupal.

    Conclusion

    Context definitions are a really powerful way of injecting the needed context into plugins. They also ensure that the contexts you use then inform the plugin about how to cache the resulting data.

    This system is really powerful and saves a lot of time and energy when writing code for plugins. In fact, every time you think about adding a service to inject an entity into a block you should think about using context definitions instead.

    We have only looked at block plugins here, but the same concepts apply to any context aware plugins. As an example, the Condition plugin is also context aware and can be used in the same way. You can also create your own plugins that interact with this system of context.

    If you want to know more then there is a documentation page on the Drupal website that looks at plugin contexts, including how to create a custom plugin that understands context.

    Comments

    Hey Phil, 

    interesting stuff that.  Thanks for digging into this topic. It feels a little contradictory to use these contexts instead of injecting services that access nodes, but I think you explained it pretty well.  I like that the caching is respected.

    I wonder if it would be realistic to add a gist or add a zip file to this post of a working module with an example using this.  I'm sure you have a working module or two where you tried this out.

     

    Permalink

    Hi Selwyn! Thanks for commenting.

    The only code I needed was the block class, so there isn't really that much code to put into a zip file.

    I am, however, working on a couple of other posts around creating context definition aware plugins and context provider services. That will need some more code so I'll make it available on a git repo or something.

    Name
    Philip Norton
    Permalink

    Thank you for this article it is interesting.

    Permalink

    Add new comment

    The content of this field is kept private and will not be shown publicly.
    CAPTCHA
    2 + 7 =
    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.