Context-aware blocks
James Williams
Defining your own Drupal block plugins in custom code is really powerful, but sometimes you can feel limited by what those blocks have access to. Your block class is like a blank canvas that you know you'll be pulling data into, but how should you get that data from the surrounding page? Often you have to resort to fetching the entity for the current page out of its route parameters (e.g. on a node page), in order to get the values out of its fields that you want to display. Plugins can actually have a context passed to them directly - which can be common things like the logged-in user, or the node for the page being viewed. Let's have a look at how to tell Drupal what your plugin needs, so you don't have to do the donkey work.
If you've created a block plugin, you'll already be aware of the annotation comment just above the class name at the top of your PHP file. It might look something like this:
<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Show some fields for the current page in a block.
*
* This block can be placed anywhere on the page, so the fields could appear in
* a sidebar, etc.
*
* @Block(
* id = "specialityfields",
* admin_label = @Translation("Special fields"),
* context_definitions = {
* "node" = @ContextDefinition("entity:node", label = @Translation("Node")),
* }
* )
*/
class SpecialityFields extends BlockBase {
Spot that last property in the annotation: the context_definitions
part. That's where the block is defined as requiring a node context. The 'entity:node' part tells Drupal that the context should be a node; Drupal supports it just being 'entity' or various other things, such as 'language' or even 'string'. You can very easily get hold of the context for your block, e.g. in the build()
method of your class, allowing your block to adapt to its surroundings:
/**
* {@inheritdoc}
*/
public function build() {
$entity = $this->getContextValue('node');
return $entity->field_my_thing->view();
}
I've used a very simple tip from our article on rendering fields for the output of this method. But the key here is the use of $this->getContextValue('node');
. Our block class is extending the BlockBase
base class, which gives us that getContextValue()
method to use (via ContextAwarePluginTrait
, which you could use directly in your plugin class if you're not extending BlockBase
). The 'node' parameter that we've passed to it should match the key of the context definition array up in the class annotation - it's just a key that you could rename to anything helpful. Plugins can specify multiple contexts, so distinguish each with appropriate names.
Using contextual values like this also sorts caching out for you. Drupal understands that caching for your block will need to be segmented according to this contextual value, and refreshed if the supplied node (or whatever the context type is) is updated, etc. This is so much better than having to manually fiddle with #cache
properties!
In this basic case of using a node, the chances are that you're just wanting to use the node that the current page is for. Drupal core has 'context provider' services - one of which provides exactly that. Most basic Drupal installations probably won't have other context providers that provide nodes, so the node just gets automatically passed through, without you having to do anything else to wire it up. Brilliantly, the block will only show up when on a node page, regardless of any other visibility settings in the block placement configuration. You can bypass that by flagging that the context is optional in its definition - spot the 'required' key:
context_definitions = {
"node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node")),
}
A slightly more interesting example is for users, as Drupal core can potentially provide two possible contexts for them:
- The currently logged-in user, or at least the entity object representing the anonymous user if no-one is logged in.
- The user being viewed - which will only be available when visiting an actual user profile page.
When there are more than one possible contexts available, block placement configuration forms offer the choice of which to use. So you might want a block in a sidebar on profile pages to show things to do with the user who owns that profile - in which case, select the 'User being viewed' option in the dropdown. Otherwise the data your block shows will just be about you, the logged-in user, even when looking at someone else's profile. Internally, your selection in the dropdown gets stored as the context mapping, which you can see in the exported configuration files for any context-aware block (including those automatically selected due to only one context being available).
If all this talk of Contexts is reminding you of something, it's because the concept was brought into core after being used with Panels in older versions of Drupal. Core's Layout Builder now uses it heavily, usually to pass the entity being viewed into the blocks that represent fields etc that you place in each section. For anyone that really wants to know, those blocks are defined using a plugin deriver - i.e. a separate class that defines multiple possible block plugins, dynamically. In the case of Layout Builder, that means a block is dynamically created for every field an entity can have. If you use plugin derivers, you might need dynamically set up context definitions too. So in the deriver's getDerivativeDefinitions()
method, you could have something like this, the PHP equivalent of the regular block's static annotation:
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$derivative['context_definitions'] = [
'node' => new \Drupal\Core\Plugin\Context\ContextDefinition('entity:node', $this->t('Node')),
];
$this->derivatives['your_id'] = $derivative;
return $this->derivatives;
}
I've only lightly touched on context provider services, but you can of course create your own too. I recently used one to provide a related 'section' taxonomy term to blocks, which pulls from an entity reference field that nearly all entity types & bundles on a site had. The blocks display fields from the current page's parent section. It made for a common interface separating that 'fetching' code from the actual block 'display' code. I recommend understanding, copying & adapting the NodeRouteContext
class (and accompanying service definition in node.services.yml) for your case if you have a similar need.
I hope this awareness of context allows your blocks to seamlessly adapt to their surroundings like good chameleons, and maybe even write better code along the way. I know I've had many blocks in the past that each had their own ways of pulling relevant data for a page. Contexts seem like the answer to me as they separate fetching and display data, so each part can be done well. Getting creative with making my own context types and context providers was fun too, though probably added unnecessary complication in the end. Let me know in the comments what context-aware plugins enable you to do!
Photo by Andrew Liu on Unsplash