Building a Custom Block Plugin
Block plugins are ideal for reusable, placeable UI backed by application logic. A production block needs more than build(): access, configuration, dependencies, and cacheability all matter.
Use an attributed plugin
Modern Drupal discovers block plugins through the Block attribute. Give the plugin a stable machine ID, translated label, and meaningful category.
Inject the service
Implement ContainerFactoryPluginInterface when the block needs services. Keep storage queries in a repository or domain service rather than embedding them in the plugin.
Return correct cache metadata
If output depends on configuration, entities, routes, roles, or users, declare the corresponding tags and contexts. Correct caching is part of functional correctness.
Working example
#[Block(id: 'journal_featured', admin_label: new TranslatableMarkup('Featured journal article'))]
final class FeaturedArticleBlock extends BlockBase {
public function build(): array {
return [
'#theme' => 'journal_featured',
'#article' => $this->repository->featured(),
'#cache' => ['tags' => ['node_list:journal_article']],
];
}
}Configuration is part of the plugin contract
If a block needs editor-controlled settings, define defaults, expose a configuration form, validate the input, and store the values through the plugin configuration API. Avoid reaching directly into arbitrary configuration from build() when the setting conceptually belongs to the block instance.
This distinction matters when the same block plugin is placed more than once with different behavior.
Access and cacheability must agree
A block that is visible only to certain users needs both a correct access decision and cache metadata that varies with the factor used by that decision. Otherwise Drupal may correctly calculate access once and then reuse that result in a context where it no longer applies.
The same principle applies to content. If the block displays an entity or a list of entities, propagate the relevant cacheability instead of treating the rendered result as permanent markup.
Keep build focused on presentation
Queries, external requests, and domain decisions are easier to reuse and test when they live in injected services. The block plugin should coordinate those collaborators and return a render array that describes the result.
That separation also makes it easier to reuse the same behavior in a controller, queue worker, command, or another plugin later.
Key Takeaways
- Keep plugins thin and inject application services.
- Make configuration explicit.
- Treat cache metadata as required output.