I was trying to build a block plugin in Drupal 8 recently. The requirement was straight-forward. I would show these blocks on node pages of a certain type. Depending on the values in that node, the block content changes.
UPDATE: I wrote a follow-up post with an easier method to get the node.
The requirements are straight-forward but there are a lot of things to consider in this scenario. One of the more complicated things to handle here is caching. Drupal 8 has introduced a new caching technique and we have to work with that by providing tags and context based on which our content could change (so that Drupal would know to clear the cache if required). If we didn’t handle this in our block plugin, then the content of our block will not vary for each node. In other words, it will show the same content regardless of where we see it. We don’t want that. In our case, we want our block content to change with each node. Not just that, we also want the cache to be cleared when the node changes.
Drupal 8 Caching Concepts
Drupal provides a mechanism for each scenario. For varying the content for each node page, there are cache contexts, and for clearing the cache when node changes, there are cache tags. The documentation covers these in depth but for our discussion, it is sufficient to know that these cache tags and contexts may be used in several places like render arrays, and also with blocks.
For our requirements, all we have to do is add specific tags and contexts for our block and we are done. Since we want our content to change on each page, we will add a context called route
. This means, that there is a different cached content for each route. To ensure we clear the cache when the node changes, we use a cache tag with the format node:[nid]
. Whenever a node is updated, Drupal clears all cache entries with that tag. For example, if you update a node with ID 215, it will also clear cache entries with the tag node:215
.
For blocks, we can specify these by overriding specific methods in your block plugin class. See the full code sample at the bottom of this post to see how it all ties together. We will also discuss an easier way to do this in a later post.
public function getCacheTags() {
$node = $this->getNode();
return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
}
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
}
With above, we are also taking care of any other cache tags and contexts that the block may have to set. There is not a lot of special handling in the above merge
methods but it is a good idea to use them so that you can be sure it doesn’t break in the future.
Getting the node using the route matcher service
While this is not related to caching, let’s look at what the getNode method looks like.
protected function getNode() {
$obj = $this->routeMatch->getParameter('node');
if (!$obj instanceof NodeInterface) {
throw new \UnexpectedValueException("Not a node page");
}
// Do more checks on node object (like checking for node type).
return $obj;
}
Here, the routeMatch
is a property holding an instance of current_route_match
service. The ideal way to set this is in the constructor using dependency injection (look at ContainerFactoryPluginInterface). I won’t get into more details about dependency injection here. There are plenty of articles and examples on that subject (Tip: Use the Drupal console to generate the block plugin and you will get an option to load services). You can see the full code sample below which uses dependency injection. In a pinch, you can also use \Drupal::routeMatch()
to get this service (which I wouldn’t recommend).
We also want to hide this block on non-node pages. That is simple with blockAccess
. If you want to show this block on all pages, skip this method. Note that depending on your rules, you can do a similar thing with UI. In our case, the block doesn’t make sense on other pages and it is a good idea to hide it in code itself.
protected function blockAccess(AccountInterface $account) {
try {
$node = $this->getNode();
}
catch (\UnexpectedValueException $ex) {
return AccessResult::forbidden();
}
return parent::blockAccess($account);
}
Now, all that’s left is to use this node in your build method.
public function build() {
$node = $this->getNode();
return [
'#markup' => 'The node id is ' . $node->id(),
];
}
Complete Code Sample
The complete block plugin may be viewed in this gist.
A better way?
That’s it for today. I hope you enjoyed this post and found it useful.
This is a lot of code for a simple thing. In the next post, we will see how to achieve the same thing with contexts with a lot less code.