Rendering plugin blocks the right way
On a recent client website, I needed to programmatically load and render 3 blocks. Plugin blocks to be precise. In modern Drupal, there are 3 types of block you can load/render:
- Content blocks
- Config blocks
- Plugin blocks
And today we're interested in plugin blocks.
Plugin blocks are blocks defined by the block plugin API in Drupal. They typically extend the BlockBase
class and reside in a modules src/plugin/Block directory.
Now, with that in mind, I hadn't done this sort of block load/rendering/creating for quite some time so I was kinda lost! I needed to load 2 views blocks and 1 webform block. After a little while of scratching my head, it occured to me that I need to look into the src/plugin/Block
directory of the views/webform module to work what I needed. Initially I was struggling with the plugin_id
value of the createInstance
method on the plugin.manager.block
service.
Step 1
Looking at the plugin definition of each block type I was able to finally workout the ID needed to get an instance of each block. For views, it was views_block:<view_machine_name>-<view_display>
. This was particularly tricky because the plugin definition doesn't actually tell you what the exact plugin id is. Sure, it gives you the first part e.g. views_block
and for most plugin blocks that would have sufficed. But not with Views. During debugging, I noticed that I wasn't getting anything back from ->createInstance('views_block')
so I had to go hunting for what wasn't working. Turns out the ID is made up in the pattern I outlined above. I found this via the deriver class within ViewsBlock
(the plugin's class) and found these lines of code:
// Add a block plugin definition for each block display.
if (isset($display) && !empty($display->definition['uses_hook_block'])) {
$delta = $view->id() . '-' . $display->display['id'];
It was then I knew the makeup of the plugin_id. I said it before, it's tricky and this was fairly well hidden. Nowhere is this documented!
The webform ID is thankfully quite different, it was just webform_block
but required making use of the second parameter of createInstance(): $configuration
. This is an array of information/settings that you can configure manually in the blocks UI. In this case, webform wanted me to pass in a webform_id
key with the ID of the webform I wanted in the block. Again I found this via debugging the class for the plugin Drupal\webform\Plugin\Block
and saw the webform_id
key inside the defaultConfiguration()
method.
You can already see the difficulty here: no two plugin block definitions are the same. Views was quite happy for me to pass in the view I wanted in the $plugin_id
parameter, whereas webform wanted the webform I wanted to use to be passed into the createInstance()
method via the $configuration
parameter.
Step 2
Really easy step this, once you've sorted out your plugin_id and you're happy that you get something back that resembles what you would expect, you now need to call ->build()
on your new block instance. Don't worry, a full example will be shown below.
Step 3
Now that you've (hopefully) got a render array of your block, you can now add some of the configurations you would expect to see if you were placing a block in the UI (/admin/structure/block). E.g. you can set the block level config like the label (via #configuration), the ID of the block, attributes etc plus other block_plugin related configuration.
Step 4
Render your block, simples! Assign your block to the $variables array (if you're in a preprocess for example) and then print your block out in a twig template. Done :)
<div>{{ my_plugin_block }}</div>
Code examples
// A webform block example
$configuration = [
'label' => 'Contact us',
'label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE,
'webform_id' => 'contact_us',
'provider' => 'webform',
];
$contact_form_instance = $plugin_block_manager->createInstance('webform_block', $configuration);
$contact_form_render['content'] = $contact_form_instance->build();
$contact_form_render += [
'#theme' => 'block',
'#id' => 'block-webform-contact-us',
'#attributes' => [],
'#contextual_links' => [],
'#configuration' => $configuration,
'#plugin_id' => $contact_form_instance->getPluginId(),
'#base_plugin_id' => $contact_form_instance->getBaseId(),
'#derivative_plugin_id' => $contact_form_instance->getDerivativeId(),
];
$variables['contact_form_block'] = $contact_form_render;
// A view block example
$related_posts_block_instance = $plugin_block_manager->createInstance('views_block:blog-related_blog_posts');
$related_posts_block_instance['content'] = $related_posts_block_instance->build();
$related_posts_block_instance += [
'#theme' => 'block',
'#id' => 'block-blog-releated-blog-posts',
'#attributes' => [],
'#contextual_links' => [],
'#configuration' => ['provider' => 'views_block'],
'#plugin_id' => $related_posts_block_instance->getPluginId(),
'#base_plugin_id' => $related_posts_block_instance->getBaseId(),
'#derivative_plugin_id' => $related_posts_block_instance->getDerivativeId(),
];
$variables['related_posts_block'] = $related_posts_block_instance;
Tips and tricks
- If you're still strugging to work out what your plugin_id should be, you can run this handy drush command to get a list of definitions:
drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()))";
2. After a quick win? You can make use of the twig tweak module to produce a 1 liner to render out a plugin block. See this article on how to do it.
Gotchas
As with anything these days, not all is as it seems.... there are a few things you should be aware of when loading/rendering blocks like this:
- Some hooks won't fire with plugin blocks that are loaded in the way outlined in this article. Blocks like (and not limited to)
hook_block_view()
andhook_block_build_alter()
won't run for these types of blocks. And they're really common hooks, too! So just be mindful. If you're in a hook and it's not running when you expect it to (for a plugin block you've loaded) this is likely the reason why. - Be careful with access. Some blocks may have their own access checks and this method of loading can bypass those - whenever loading blocks like this just double check you're correctly following - or reimplementing - the access that was originally intended with that blocks.
- Views blocks might lose the context that was originally intended for them. Contexts such as arguments etc may need to be re-set so the block can function properly.
- The same goes for caching. You may need to re-implement the caching that was originally set for the block you've loaded. it should be as simple as using the following code snippet:
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromRenderArray($content))
->applyTo($build);