Create a custom block plugin

Last updated on
18 April 2024

Blocks in Drupal are instances of the block plugin. Custom block plugins are automatically listed in the 'Place Block' dialogue pop-up.

The Drupal block manager scans your modules for any classes that contain a #Block PHP attribute.( PHP attributes was introduced in Drupal 10.2. For older versions see @Block annotations).

PS! A site-builder can define custom blocks without writing code, from Drupal path /block/add.

Defining a new block plugin

The example snippet below makes use of the #Block PHP attribute along with the properties "id" and "admin_label" to define a custom block.

Create the file src/Plugin/Block/HelloBlock.php within the module skeleton created earlier and add the code below.

<?php

namespace Drupal\hello_world\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Provides a 'Hello' Block.
 */
#[Block(
  id: "hello_block",
  admin_label: new TranslatableMarkup("Hello block"),
  category: new TranslatableMarkup("Hello World")
)]
class HelloBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    return [
      '#markup' => $this->t('Hello, World!'),
    ];
  }

}

Note: After creating this file, clear the Drupal site cache in order to recognize this new class.

To add 'Hello block' you can go to Structure -> Block Layout (admin/structure/block) and click on 'Place Block' button associated with each available region.

Clicking on 'Place Block' button for any given region a 'Place Block' dialogue pop-up will appear, with a listing of all available blocks. To quickly find your block, simply use 'Filter by block name' option or use mouse-scroll to locate 'Hello block'. This way you can add any number of instances of the custom block anywhere on your site.

Troubleshooting

  • The class name and the file name must be the same (class HelloBlock and src/Plugin/Block/HelloBlock.php). If the class name is different, the block will appear in the list of available blocks, however, you will not be able to add it.

  • The namespace at the top of the block must match your module structure (the hello_world module will need namespace Drupal\hello_world\Plugin\Block).

  • Be sure to double check all paths and filenames. Your .php file must be in the correctly labeled directory (src/Plugin/Block/), otherwise it won't be discovered by Drupal.

  • If your block fails to place in a region with no error on screen or in watchdog, check the PHP / Apache error logs.

  • If your block is not on the list, be sure to rebuild the Drupal caches (e.g. drush cr).

  • Make sure the module which contains the block is turned on.

  • Make sure your module's naming convention is all lowercase/underscores. Some users report blocks not showing for modules with camelCase naming convention. For example, myModule will never show defined blocks, while my_module will. This was last verified against Drupal 8.8.1

  • If the block appears in the block layout, but not in the actual page, double check the visibility settings - they might prevent the block from rendering.

Using Twig templates with custom blocks

Example 1: passing simple variables.

1. Add a hook_theme in your .module file. 
Note: Do not name the theming function like 'block__...'. This will not pass any variables down to the twig templates. Instead, you might use the module name as prefix.

Module file: hello_world.module
Example: defining two new custom variables.

/**
 * Implements hook_theme().
 */
function hello_world_theme() {
  return [
    'hello_block' => [
      'variables' => [
        'custom_data' => [],
        'custom_string' => '',
      ],
    ],
  ];
}

2. Use '#theme' in the render array in the build method and pass the variables on the same level as the '#theme''#varname'.

Example of build() function in HelloBlock.php to pass variables to a twig file.

  /**
   * {@inheritdoc}
   */
  public function build() {
    return [
      '#theme' => 'hello_block',
      '#custom_data' => ['age' => '31', 'DOB' => '2 May 2000'],
      '#custom_string' => 'Hello Block!',
    ];
  }

3. Print variables in twig file.

Template file: templates/hello-block.html.twig

{# Output string variable. #}
  <p>{{ custom_string }}</p>

{# Output values from the array. #}
  <strong>{{ custom_data.age }}:</strong> {{ custom_data.DOB }}<br>

{# Output array in for loop. #}
{% for key,value in data %}
  <strong>{{ key }}:</strong> {{ value }}<br>
{% endfor %}

Example 2: passing more complex structures.

Example of build() function in HelloBlock.php to pass tabs configuration variable to a twig file.

  /**
   * {@inheritdoc}
   */
  public function build() {
    $some_array = [
      0 => [
        'is_active' => 'active',
        'label' => 'lorem ipsum',
        'url' => 'http://google.com',
      ],
      1 => [
        'is_active' => 'inactive',
        'label' => 'lorem ipsum',
        'url' => 'http://amazon.com',
      ],
    ];

    return [
      '#theme' => 'hello_block',
      '#active_tab' => 'some_string',
      '#body_text' => [
        '#markup' => 'some_html_string',
      ],
      '#tabs' => $some_array,
    ];
  }

Module file: hello_world.module

/**
 * Implements hook_theme().
 */
function hello_world_theme($existing, $type, $theme, $path): array {
  return [
    'hello_block' => [
      'variables' => [
        'active_tab' => NULL,
        'body_text' => NULL,
        'tabs' => [],
      ],
    ],
  ];
}

Template file: templates/hello-block.html.twig

<div{{ attributes }}>

  <!-- tabs -->
  {% for key,value in tabs %}
    <a class="{{ value.is_active }}" href="{{ value.url }}">{{ value.label }}</a>
  {% endfor%}

  <!-- body -->
  {{ body_text }}
</div>

Caching blocks

In the same PHP class file src/Plugin/Block/HelloBlock.php that we have created earlier, because our class derives from the BlockBase class, we can easily add cache tags, contexts or max-age to the block.

Cache context

Let's say the block shows user's IP. To add cache context to make the block cachable per each IP address, we can add getCacheContexts() method as follows:

/**
 * {@inheritdoc}
 */
public function getCacheContexts() {
  return Cache::mergeContexts(parent::getCacheContexts(), ['ip']);
}

Cache tags

Adding custom cache tags is done in a similar way. Here we are adding a node_list cache tag, which invalidates block cache whenever any node is changed.

/**
 * {@inheritdoc}
 */
public function getCacheTags() {
  return Cache::mergeTags(parent::getCacheTags(), ["node_list"]);
}

Cache max age

You can add cache Max-Age to the block. Here we are returning -1, which if used without cache tags or contexts, should make the block's cache never expire.

/**
 * {@inheritdoc}
 */
public function getCacheMaxAge() {
  return -1;
}

Disable caching

To update the contents on every page load, disable caching by using return 0; instead.

There is also Cache::mergeMaxAges() function available if you want to use it instead of returning max age value.

More about cache tags and cache context:

Help improve this page

Page status: No known problems

You can: