Drupal 10: Programmatically Injecting Context Into Blocks

Context definitions in Drupal are really useful when you want to inject context into your plugins and I have been using them for a little while now. It has made building custom blocks to add content to pages much easier as I don't need to copy the same block of code to find an entity of a particular sort into the block plugin.

One problem I encountered when using this technique was when trying to programmatically create and render a block. The problem was that the context wasn't available in the block when it was used in this way, and it took me a little investigation to find out how to put all of the pieces together.

Rendering a block programmatically is sometimes a necessity to do when you need the block outside of the block's normal region. You might create a block that collects together the output of a number of blocks, which allows you to template them together in a very precise way. You might also need to print out a block without having to create a region on a page.

There are a few steps involved in getting things working in this way. In this article I will show how to print blocks programmatically, and how to do so using context.

To show the problem in detail, let's take an example of a block called UserContextBlock that has a user entity as the available context. This is a custom block, created in a custom module.

<?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('Username: ') . $user->getAccountName(),
      ];
    }
    return $build;
  }

}

When injected through the Drupal administration UI, this block functions correctly by printing out the user's name.

To render the block programmatically we need to use the plugin.manager.block service. We use this service to create an instance of the UserContextBlock object, which we can then use to generate the render array that contains the content of the block. We can also use this object to perform access checks on the block as well if requried.

The following code is a section of a controller class that has an action method called blockPage(). The create() method is a method used to inject services into objects and in this case we are adding the plugin.manager.block to render the block and the current_user service, which we can use to perform an access check.

public static function create(ContainerInterface $container) {
  $object = parent::create($container);

  $object->pluginManagerBlock = $container->get('plugin.manager.block');
  $object->currentUser = $container->get('current_user');

  return $object;
}

public function blockPage() {
  // Create the block object.
  $pluginBlock = $this->pluginManagerBlock->createInstance('user_context_block');

  // Some blocks might implement access check.
  $accessResult = $pluginBlock->access($this->currentUser);

  // Return empty render array if user doesn't have access.
  if ((is_object($accessResult) && $accessResult->isForbidden()) || (is_bool($accessResult) && !$accessResult)) {
    return [];
  }

  // Build and return render array.
  return $pluginBlock->build();
}

The problem is that this code does not work. When we attempt to run the build() method on the block object it generates the following error.

Drupal\Component\Plugin\Exception\ContextException: The 'entity:user' context is
required and not present. in Drupal\Core\Plugin\Context\Context->getContextValue()
(line 73 of /var/www/html/web/core/lib/Drupal/Core/Plugin/Context/Context.php).

What this tells us is that although we created the block object correctly, it doesn't know about the context. This means that when we call the code $this->getContextValue('user'); we get the above error message since the context we are asking for hasn't been created.

To rectify this we need to add the context.repository and context.handler services so that we can inject the correct context into the block. Using these services also requires performing the following actions.

  • The createInstance() method must have the configuration for the context mapping injected into it (this is as an array on second parameter). This array is used to configure the block in different ways and can be used to set the visibility of the block and other things. For our purposes we need to inform the context services the name of the context being used and the context we want to inject into that name.
  • We then need to use the context.repository service to load the available contexts from the block and then use the context.handler to load those contexts into the block instance.

Adding this extra functionality to the blockPage() method changes it to look like this.

public static function create(ContainerInterface $container) {
  $object = parent::create($container);

  $object->pluginManagerBlock = $container->get('plugin.manager.block');
  $object->currentUser = $container->get('current_user');

  $object->contextRepository = $container->get('context.repository');
  $object->contextHandler = $container->get('context.handler');

  return $object;
}

public function blockPage() {
  // Create the block configuration.
  $config = [
    'context_mapping' => [
      'user' => '@user.current_user_context:current_user',
    ],
  ];
  // Create the block object.
  $pluginBlock = $this->pluginManagerBlock->createInstance('user_context_block', $config);

  if ($pluginBlock instanceof ContextAwarePluginInterface) {
    // Inject the configured context into the block.
    $contexts = $this->contextRepository->getRuntimeContexts($pluginBlock->getContextMapping());
    $this->contextHandler->applyContextMapping($pluginBlock, $contexts);
  }

  // Some blocks might implement access check.
  $accessResult = $pluginBlock->access($this->currentUser);

  // Return empty render array if user doesn't have access.
  if ((is_object($accessResult) && $accessResult->isForbidden()) || (is_bool($accessResult) && !$accessResult)) {
    return [];
  }

  // Build and return render array.
  return $pluginBlock->build();
}

Now, when we try to use the 'user' context in the block it will return the correct object from the context provider. The block now functions in exactly the same way that it would if injected into the page using the Drupal block administration UI.

The context name here (user.current_user_context:current_user) is quite descriptive. It tells us the name of the context provider service (user.current_user_context) and the value that is returned from that context provider (current_user).

If we needed to know the current node from the route then we would use the value node.node_route_context:node, which is the node.node_route_context context provider, returning the node value.

Incidentally, if you use the Twig Tweak module then it is also possible to inject context directly into the block from the drupal_block() twig function. The following is an example of the above code rendering a block into a template with the aid of this function.

{{ drupal_block('user_context_block', {context_mapping: {user: '@user.current_user_context:current_user'}}) }}

After using context providers for a little while, this problem was the only issue I encountered and I was able to easily solve is using the above techniques. I hope this information is useful to you.

I'm interested if you have used context providers and what you have done with them to improve your Drupal code. Let me know in the comments!

More in this series

Comments

Dear hashbangcode.com webmaster, Great content!

Permalink

Hi,

Great explanation !

I'm juste wondering how did know the context names (user.current_user_context:current_user, node.node_route_context:node, etc), is there any documentation to find them ? Also what about custom contexts, what would be the name of them ?

Many thanks 

Permalink

Hi Joe!

Thanks for reading (and commenting).

The context names are a combination of the service used to register the context and the name of the context being returned from the service class.

For example, the `user.current_user_context` is registered in the user.services.yml file. To find these contexts, look for anything with the tag of 'context_provider'.

  user.current_user_context:
    class: Drupal\user\ContextProvider\CurrentUserContext
    arguments: ['@current_user', '@entity_type.manager']
    tags:
      - { name: 'context_provider' }

Looking inside the CurrentUserContext class, I can see that it returns an array of contexts. The name of the context we need is called "current_user".

    $result = [
      'current_user' => $context,
    ];

Putting these two things together we have the string "@user.current_user_context:current_user", with the @ symbol at the front. I'm not sure what the @ symbol is for, but if it's missing then the context manager throws an error so I guess it's best to keep it in there.

As for where these are documented, I didn't find any. I just poked about in the Drupal codebase until I found what I was looking for. Would you find such a list useful?

Name
Philip Norton
Permalink

Add new comment

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