Drupal 10: Creating Custom Context Providers

I previously looked at injecting context providers into context aware plugins. This time I will explore more about creating our own context providers to plug into this system.

The context provider system is an ideal way to provide context to context aware plugins, with blocks being the prime example. You would use a context provider to inject an entity or value into the block so that actions can be taken using that data.

For example, we could load the current node from the route using context so that we didn't have to bake the route provider logic into the block.

The values we inject also plug into the cache systems and so we don't need to worry about making sure we integrate the cache systems for each type of context within the block system. Cache contexts is all taken care of in the block plugin code.

In this article I will look at why you might want to create a context provider, how to create one, and some examples of them in use.

By default, Drupal comes with the following context providers:

  • Current language context (provided by \Drupal\Core\Language\ContextProvider\CurrentLanguageContext).
  • The node entity from the current route (provided by \Drupal\node\ContextProvider\NodeRouteContext).
  • The taxonomy term entity from the current route (provided by \Drupal\taxonomy\ContextProvider\TermRouteContext).
  • The current user (provided by \Drupal\user\ContextProvider\CurrentUserContext).

This gives us a variety of contexts that are commonly used, but there are many reasons why we would want to create our own context providers. Here are some examples of potential contexts we could create using this system.

  • Load the current user via the router system. We only have access to the current user who is looking at the site as a default in Drupal. It is also possible to load the currently loaded user entity based on the router being visited and allows us to render some of the user fields in a block.
  • Get an entity that is referenced by the currently loaded page. For example, we could load in a recommended article a taxonomy term or even the author of a page through the context provider. This object would then be injected into the block to use to display data.
  • An entity could be loaded from an argument passed to the page. This would be useful when rendering a form that accepts a node ID as an argument through the URL. Such a mechanism could be used to allow the user to submit information about a particular page. A context could also be created that looks at the form submission and can present a link back to the page that the user left when they first clicked to submit the form.
  • Inject the current user's IP address into a block. This can then be used to determine access to the block or perhaps just shown as information to the user.
  • Load a node from the database into the block to show to the user. This might be a recommended node of some sort that can be used to signpost other parts of the site.
  • And many more...

Let's look at how to create a context provider.

Creating A Context Provider

A Drupal context provider is defined as a service class, which means that we define the class as we would any other service. The main difference here is that we also give the service class a tag of "context_provider" so that Drupal knows what sort of service class it is.

Here is a definition of a context provider class in a module *.services.yml file.

services:
  mymodule.my_context:
    class: Drupal\mymodule\ContextProvider\MyContext
    tags:
      - { name: 'context_provider' }

The class behind this service needs to implement the Drupal\Core\Plugin\Context\ContextProviderInterface interface. This interface defines the methods getRuntimeContexts() and getAvailableContexts(), which we must define for the context provider to work.

These two methods have the following roles in the context provider.

  • getRuntimeContexts(array $unqualified_context_ids)  - Returns an array of the context objects for the given context IDs. This must be an associative array of IDs and context objects.
  • getAvailableContexts() - Returns an array of the available contexts for the purposes of configuration. This is injected into the form when configuring contexts.

Context providers live in the directory src/ContextProvider within your module and this is where the MyContext class defined in the services file is kept. The MyContext class doesn't do anything, but does implement the needed methods for the context provider to not produce any errots.

<?php

namespace Drupal\mymodule\ContextProvider;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;

class MyContext implements ContextProviderInterface {

  /**
   * {@inheritdoc}
   */
  public function getRuntimeContexts(array $unqualified_context_ids) {
    // Returns an array of context values for given IDs.
  }

  /**
   * {@inheritdoc}
   */
  public function getAvailableContexts() {
    // Returns an array of all available contexts for the purposes
    // of configuration.
  }

}

You can use this as a skeleton for context provider classes as it doesn't actually do anything on its own. We will revisit this class to provide functionality later.

First though, let's take a look at how to inject other services into our context provider classes.

Injecting Dependencies Into Context Provider Services

As this class is defined as a service we can also use the module *.services.yml file to inject any dependencies into the context. For example, let's say that we wanted to ass the current_route_match service to the context provider class. This service is useful for extracting objects from the currently loaded route and so is often injected into context providers.

First, we change the service definition to add the service to the arguments settings for the service.

services:
  mymodule.my_context:
    class: Drupal\mymodule\ContextProvider\MyContext
    arguments: ['@current_route_match']
    tags:
      - { name: 'context_provider' }

Then, we update the MyContext class to add this service into the class constructor.

<?php

namespace Drupal\mymodule\ContextProvider;

use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Routing\RouteMatchInterface;

class MyContext implements ContextProviderInterface {

  /**
   * The route match object.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatch;

  /**
   * Constructs a new MyContext object.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match object.
   */
  public function __construct(RouteMatchInterface $route_match) {
    $this->routeMatch = $route_match;
  }

  /**
   * {@inheritdoc}
   */
  public function getRuntimeContexts(array $unqualified_context_ids) {
    // Returns an array of context values for given IDs.
  }

  /**
   * {@inheritdoc}
   */
  public function getAvailableContexts() {
    // Returns an array of all available contexts for the purposes
    // of configuration.
  }

}

The current_route_match service is now ready to use within the context provider class. We can use it to grab information from the route, including what objects are currently loaded.

Let's take a deeper look at the methods in the context provider class.

The getAvailableContexts() Method

This method is used when configuring the context aware plugin. It must return an array of \Drupal\Core\Plugin\Context\Context objects that detail the available contexts that the class provides. The Context object is used to hold information about the context in the form of a \Drupal\Core\Plugin\Context\ContextDefinition object, which is injected into the Context object constructor.

How you implement the getAvailableContext() method depends on your usage. If you are retuning a simple value as your context then you can define the Context objects using the type that you want to return.

The following code is representative of what you'll need to do to return a simple value.

public function getAvailableContexts() {
  $context = new Context(new ContextDefinition('string'));
  $context->getContextDefinition()->setLabel('Context Label');

  return [
    'context_key' => $context,
  ];
}

You can create as many contexts as you require for your purposes. The following code shows two contexts being generated in the getAvailableContext() method. All of your contexts should be the same type.

public function getAvailableContexts() {
  $context1 = new Context(new ContextDefinition('string'));
  $context1->getContextDefinition()->setLabel('Context Label 1');

  $context2 = new Context(new ContextDefinition('string'));
  $context2->getContextDefinition()->setLabel('Context Label 1');

  return [
    'context_key_1' => $context1,
    'context_key_2' => $context2,
  ];
}

Simple value types available in Drupal core are email, float, integer, list, language, language_reference, map, string, timespan, timestamp, and uri. You can also pass "any" to allow any type of object to be used. The DataType plugin is used to define these types so you can create your own custom types if the need arrises.

If, however, you are returning an entity as your context value then the helper class \Drupal\Core\Plugin\Context\EntityContext can be used to easily generate the needed context definition. This class has a couple of static helper methods that can be used to generate the needed context objects based on the entity type being returned.

Here is a typical EntityContext object being created for a node entity.

public function getAvailableContexts() {
  $context = EntityContext::fromEntityTypeId('node', $this->t('Node Context Label'));
  return [
    'node' => $context
  ];
}

There are many entity types available in Drupal, all of which can be used as a context in this way.

This code will fill in a select list of options on the context aware plugin. For example, with the above code for the "Node" Context Label" the following will appear on the block configuration form when we add the provider to the block.

A context provider displaying an option in a context aware plugin in a Drupal site.

Selecting this option will allow the context aware plugin to use this context provider to provide the correct context.

The getRuntimeContexts() Method

This method is used to return values of the context provider. Similar to the getAvailableContexts() method, it must return an array of Context objects, and they are built in the same sort of way. The addition of cache metadata allows the Context objects to plug into the cache system for context aware plugins.

Let's take the example of returning a simple string as a context value. The code below will set the value of the context, wrap this in a Context object and return this as an array with the key of the array matching the key we set in the getAvailableContexts() method. The second parameter of the Context constructor is the value of the context itself and this is used to communicate the value to context aware plugins.

public function getRuntimeContexts(array $unqualified_context_ids) {
  // Set the context value (in this case a static string).
  $value = 'abcdefg';

  // Wrap the value in a Context object.
  $context = new Context(new ContextDefinition('string'), $value);

  // Attach cache metadata to the context.
  $cacheability = new CacheableMetadata();
  $cacheability->setCacheContexts(['route']);
  $context->addCacheableDependency($cacheability);

  // Return the context as an array.
  return [
    'context_key' => $context,
  ];
}

The use of the \Drupal\Core\Cache\CacheableMetadata class is important here as it informs the code upstream how this context is intended to be cached. In the example above we are setting the cache context to be 'route', which means that this value will be cached per page.

The $unqualified_context_ids parameter is used if you have more than one context definition available; you can use this variable to detect which context is being used at any given time. You must provide a new Context object for each item found in the passed array.

The following shows a simple context provider that has two different string contexts available.

public function getRuntimeContexts(array $unqualified_context_ids) {
  $return = [];

  $value = NULL;
    
  foreach ($unqualified_context_ids as $id) {
    // Set the context value depending on the IDs received.
    switch ($id) {
      case 'string_value_1':
        $value = 'abcdef';
        break;

      case 'string_value_2':
        $value = 'ghijklmn';
        break;
    }

    // Generate a new Context object for each ID found.
    $context = new Context(new ContextDefinition('string'), $value);

    $cacheability = new CacheableMetadata();
    $cacheability->setCacheContexts(['route']);
    $context->addCacheableDependency($cacheability);

    $return[$id] = $context;
  }

  return $return;
}

Have a look at the class \Drupal\Core\Language\ContextProvider\CurrentLanguageContext for a good example of multiple contexts being returned from a single provider.

You might notice that there's quite a lot of code overlap in the code when a single context value is returned. The getRuntimeContexts() and getAvailableContexts() methods both create a Context object and return this as an array. For this reason it is sometimes possible to simplify the code a little by just calling one function from the other.

public function getAvailableContexts() {
  return $this->getRuntimeContexts([]);
}

Note that you should only do this if you are only setting a single context parameter value. In other words, if the $unqualified_context_ids parameter is ignored then feel free to use this technique.

That's pretty much it for creating context provider classes, so let's look at some examples of context providers in use.

Examples Of Context Providers

Let's look at some examples of context providers in action. I will provide both the service definition and the context provider class itself, or at least the relevant code.

I have created a git repo that contains all of the code seen in this article, so if you want to use anything I have created in your own projects then feel free.

IP Address Context Example

To demonstrate the use of a string based context provider I thought it would be good to generate an IP address. This can be used to print out a message to the user showing them what their IP address is, which might be useful in some situations.

First, we need to define the IP address context as a service, injecting the request_stack service so that we can easily find the user's IP address.

services:
  custom_context.ip_address_context:
    class: Drupal\custom_contexts\ContextProvider\IpAddressContext
    arguments: ['@request_stack']
    tags:
      - { name: 'context_provider' }

The implementation itself if not complex. All we need to do is find the IP address of the user and then return this as a Context object. As we aren't doing anything complex here we can use the getRuntimeContexts() method to provide the information for the getAvailableContexts() method.

<?php

namespace Drupal\custom_contexts\ContextProvider;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class IpAddressContext implements ContextProviderInterface {

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * IpAddressContext constructor.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack used to retrieve the current request.
   */
  public function __construct(RequestStack $request_stack) {
    $this->requestStack = $request_stack;
  }

  /**
   * {@inheritdoc}
   */
  public function getRuntimeContexts(array $unqualified_context_ids) {
    $value = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $this->requestStack->getCurrentRequest()->getClientIp();

    $context = new Context(new ContextDefinition('string'), $value);
    $context->getContextDefinition()->setLabel('IP Address');

    $cacheability = new CacheableMetadata();
    $cacheability->setCacheContexts(['ip']);
    $context->addCacheableDependency($cacheability);

    return [
      'ip' => $context,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getAvailableContexts() {
    return $this->getRuntimeContexts([]);
  }

}

The cache context we set here is 'ip', which means that the cache will be set for each IP address encountered.

We can then use this context in a block by injecting the IP address into the block plugin annotation like this.

/**
 * Provides a 'IP Address' block.
 *
 * @Block(
 *  id = "ip_context_block",
 *  label = "IP Address Context Block",
 *  admin_label = @Translation("IP Address Context Block"),
 *  context_definitions = {
 *    "ip_address" = @ContextDefinition("string", label = @Translation("IP Address"))
 *  }
 * )
 */

The build method of the block just needs to get the context value and add this to a render array.

public function build() {
  $build = [];
  $build['value'] = [
    '#markup' => $this->t('IP Address:') . $this->getContextValue('ip_address'),
  ];
  return $build;
}

One thing to realise here is that we are using the 'string' type as the context type. This means that if we have another string type in use then we need to be careful that we configure the block correctly or we will display the wrong information to the user.

We could further improve this by creating an IP address DataType plugin and using this as the type of context being provided. This is slightly beyond the scope of this article, but can be explored further, if requested.

Referenced Node Context Example

One common approach in Drupal is to create references between entities, which can be used by our context providers to great effect.

Let's say we had a site that had an entity reference field between two nodes. We can easily get the current page node using the core NodeRouteContext context provider, but what if we wanted to load the referenced entity in our context? We can do that too in the context provider.

First we need to define the service for our context provider. This just needs to have access to the current_route_match service.

services:
  custom_context.referenced_node_context:
    class: Drupal\custom_contexts\ContextProvider\ReferencedNodeContext
    arguments: ['@current_route_match']
    tags:
      - { name: 'context_provider' }

Assuming we have a field on a content type called "field_referenced_article" we can use this to load the referenced entity from the currently loaded node. The code mostly consists of making sure the node is loaded and that a referenced node exists in the correct field. Once we are sure everything is in place we just set the value and create the context.

public function getRuntimeContexts(array $unqualified_context_ids) {
  $result = [];
  $context_definition = EntityContextDefinition::create('node')->setRequired(FALSE);
  $value = NULL;

  // Grab the current route context.
  $route_object = $this->routeMatch->getRouteObject();
  $route_contexts = $route_object->getOption('parameters');

  if (isset($route_contexts['node'])) {
    // We have a node entity in our route parameters.
    $node = $this->routeMatch->getParameter('node');
    if ($node->hasField('field_referenced_article')) {
      // The node contains the field "field_referenced_article".
      $reference = $node->field_referenced_article->referencedEntities();
      if (isset($reference[0]) && $reference[0] instanceof NodeInterface) {
        // A referenced entity exists.
        $value = $reference[0];
      }
    }
  }

  $cacheability = new CacheableMetadata();
  $cacheability->setCacheContexts(['route']);

  $context = new Context($context_definition, $value);
  $context->addCacheableDependency($cacheability);

  $result['node'] = $context;

  return $result;
}

public function getAvailableContexts() {
  $context = EntityContext::fromEntityTypeId('node', $this->t('Referenced node'));
  return ['node' => $context];
}

We can use this context provider just like any other node context. See the article "Drupal 10: Using Context Definitions To Create Context Aware Plugins" for more information on how to use node context providers in your context aware plugins. In this case though, the value returned will be a referenced node, rather than the currently loaded node.

This same technique can be used for users, taxonomy terms or anything else that is linked through the currently loaded page.

User Route Example

Drupal core only provides a context for the currently logged in user, but it doesn't allow us to load the user based on the profile currently being viewed. Thankfully, the User Route Context module exists that allows you to load the user based on the current route.

The module isn't large (it doesn't need to be), but it is well written, unit tested, and does exactly what it needs to do.

The services file for the module just adds the context provider with the current_route_match service as a single argument.

services:
  user_route_context.user_route_context:
    class: Drupal\user_route_context\ContextProvider\UserRouteContext
    arguments: ['@current_route_match']
    tags:
      - { name: 'context_provider' }

The getRuntimeContexts() method is pretty simple. It just loads the user from the currently loaded route and then returns it (once we sure that the object is, in fact, a user).

  public function getRuntimeContexts(array $unqualified_context_ids): array {
    $result = [];
    $context_definition = EntityContextDefinition::create('user')->setRequired(FALSE);
    $value = NULL;

    if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['user'])) {
      $user = $this->routeMatch->getParameter('user');

      // Ensure that we have a valid User object.
      if ($user instanceof UserInterface) {
        $value = $user;
      }
    }

    $cacheability = new CacheableMetadata();
    $cacheability->setCacheContexts(['route']);

    $context = new Context($context_definition, $value);
    $context->addCacheableDependency($cacheability);
    $result['user'] = $context;

    return $result;
  }

The getAvailableContexts() method is used to inform the configuration form with what this context provider does.

  public function getAvailableContexts(): array {
    $context = EntityContext::fromEntityTypeId('user', $this->t('User from URL'));
    return ['user' => $context];
  }

To use this context provider we just need to create a block that uses the user entity context provider. The following code defines a block that can be used to print out user information based on the value from the context provider.

<?php

namespace Drupal\custom_contexts\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'User' block.
 *
 * @Block(
 *  id = "user_context_block",
 *  label = "User Context Block",
 *  admin_label = @Translation("User Context Block"),
 *  context_definitions = {
 *    "user" = @ContextDefinition("entity:user", label = @Translation("User"))
 *  }
 * )
 */
class UserContextBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    /** @var \Drupal\user\Entity\User $user */
    $user = $this->getContextValue('user');

    if ($user) {
      $build['user'] = [
        '#markup' => $this->t('User name: ') . $user->getAccountName(),
      ];

    }
    return $build;
  }

}

Be sure to select the correct context provider in the block configuration screen or this will be connected to the current user, rather than the user loaded through the route.

The User Route Context module also provides a good example of how to test your context providers, so if you are creating a context provider of your own then this is a good place to start.

Conclusion

Context providers are a useful way of adding context to your plugins (especially blocks) without having to inject extra services into your plugins. They can allow for different types of values or entities to be added to your block without having to change any code.

Due to the fact that any plugin that uses a context provider needs to also understand the cache context that the provider has it means that caching sorts itself out. You don't need to add extra code to your block classes to update caches, that's automatically taken care of by the fact you have injected a provider.

The only thing to watch out for is when you have providers of the same type used in different ways. The 'current user' and 'user current route' providers return the same type of object, but return different data and have very different uses.

Remember, if you want to have a go at the context providers seen here then they are all available as a stand alone module in a git repository.

More in this series

Add new comment

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