A custom block
A block is a piece of the page that the administrator places into theme regions: the header, the sidebar, the footer. A module can add its own block — for example Latest articles. It takes one class in a particular folder.
Where a block lives
The block class goes to the src/Plugin/Block folder of the module. The path matters: Drupal looks for blocks exactly there. Create the folders and the file by hand or with the drush generate plugin:block command.
A simple block
// web/modules/custom/blog/src/Plugin/Block/LatestArticlesBlock.php
namespace Drupal\blog\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[Block(
id: 'blog_latest_articles',
admin_label: new TranslatableMarkup('Latest articles'),
category: new TranslatableMarkup('Blog'),
)]
class LatestArticlesBlock extends BlockBase
{
public function build(): array
{
return [
'#markup' => $this->t('The latest articles will be here.'),
];
}
}The #[Block(...)] entry above the class is a PHP attribute: from it Drupal learns the machine name of the block and its name for the panel. The build() method returns a render array — the same kind as in a controller.
Placing it
Run drush cr and open Structure → Block layout, the address /admin/structure/block. Next to the region you want, press "Place block", find Latest articles and save. The block appears on the site pages in the chosen region. The placement form lets you set a title and limit the block to certain pages or roles.
A block with data
Let us output real articles. A block gets services through create(), but for blocks this method takes more arguments, and the class must declare the ContainerFactoryPluginInterface interface:
// web/modules/custom/blog/src/Plugin/Block/LatestArticlesBlock.php
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class LatestArticlesBlock extends BlockBase implements ContainerFactoryPluginInterface
{
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static
{
return new static($configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'));
}
public function build(): array
{
$storage = $this->entityTypeManager->getStorage('node');
$ids = $storage->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->condition('status', 1)
->sort('created', 'DESC')
->range(0, 5)
->execute();
$links = [];
foreach ($storage->loadMultiple($ids) as $node) {
$links[] = $node->toLink()->toRenderable();
}
return [
'#theme' => 'item_list',
'#items' => $links,
'#cache' => ['tags' => ['node_list:article']],
];
}
}The #[Block] attribute stays above the class, as in the first example. The block outputs a list of five links to the latest articles. The #cache property says the block must be rebuilt when an article is created or changed — the cache has a lesson of its own.
Block settings
To let the administrator choose the number of articles, add three methods to the class:
// web/modules/custom/blog/src/Plugin/Block/LatestArticlesBlock.php
use Drupal\Core\Form\FormStateInterface;
public function defaultConfiguration(): array
{
return ['count' => 5];
}
public function blockForm($form, FormStateInterface $form_state): array
{
$form['count'] = [
'#type' => 'number',
'#title' => $this->t('How many articles to show'),
'#default_value' => $this->configuration['count'],
'#min' => 1,
'#max' => 20,
];
return $form;
}
public function blockSubmit($form, FormStateInterface $form_state): void
{
$this->configuration['count'] = (int) $form_state->getValue('count');
}The field appears in the block placement form. In build() replace range(0, 5) with range(0, $this->configuration['count']). One block can be placed several times with different settings.
Common mistake
The block does not appear in the "Place block" list. Check in order: the file is in src/Plugin/Block, the namespace is Drupal\blog\Plugin\Block, the class has the attribute with a unique id, and drush cr was run after adding the file. A mistake in any of these gives no message — the block is silently absent.