Разработка кастомных блоков (Blocks) Drupal
Блоки в Drupal — это плагины, которые размещаются в регионах темы. Есть два типа: Content blocks (создаются через UI редакторами) и Plugin blocks (код). Кастомные плагин-блоки — это то, что пишет разработчик, когда нужна динамическая логика: вывод последних записей, виджет формы, баннер из конфигурации.
Анатомия плагин-блока
// src/Plugin/Block/LatestNewsBlock.php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @Block(
* id = "my_module_latest_news",
* admin_label = @Translation("Latest News"),
* category = @Translation("My Module"),
* context_definitions = {
* "node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Current node"))
* }
* )
*/
class LatestNewsBlock extends BlockBase implements ContainerFactoryPluginInterface {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private readonly 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'),
);
}
/**
* Форма конфигурации блока (видна в Layout Builder / Block UI).
*/
public function blockForm($form, FormStateInterface $form_state): array {
$form = parent::blockForm($form, $form_state);
$config = $this->getConfiguration();
$form['count'] = [
'#type' => 'number',
'#title' => $this->t('Number of items'),
'#default_value' => $config['count'] ?? 4,
'#min' => 1,
'#max' => 20,
];
$form['exclude_current'] = [
'#type' => 'checkbox',
'#title' => $this->t('Exclude current node'),
'#default_value' => $config['exclude_current'] ?? TRUE,
];
return $form;
}
public function blockSubmit($form, FormStateInterface $form_state): void {
$this->configuration['count'] = $form_state->getValue('count');
$this->configuration['exclude_current'] = $form_state->getValue('exclude_current');
}
public function build(): array {
$config = $this->getConfiguration();
$count = $config['count'] ?? 4;
$storage = $this->entityTypeManager->getStorage('node');
$query = $storage->getQuery()
->condition('type', 'news')
->condition('status', 1)
->sort('created', 'DESC')
->range(0, $count)
->accessCheck(TRUE);
// Исключаем текущую ноду если включена опция
if ($config['exclude_current'] ?? TRUE) {
try {
$current_node = $this->getContextValue('node');
if ($current_node && $current_node->id()) {
$query->condition('nid', $current_node->id(), '<>');
}
} catch (\Exception $e) {
// Контекст не доступен — не исключаем
}
}
$ids = $query->execute();
if (empty($ids)) {
return ['#markup' => ''];
}
$nodes = $storage->loadMultiple($ids);
$view_builder = $this->entityTypeManager->getViewBuilder('node');
return [
'#theme' => 'my_module_news_list',
'#items' => array_map(fn($n) => $view_builder->view($n, 'teaser'), $nodes),
// Кэш: инвалидируется при изменении любой ноды типа news
'#cache' => [
'tags' => Cache::mergeTags(['node_list:news'], $this->getCacheTags()),
'contexts' => ['route', 'user.roles'],
'max-age' => Cache::PERMANENT,
],
];
}
public function getCacheTags(): array {
return Cache::mergeTags(parent::getCacheTags(), ['node_list:news']);
}
}
Content Blocks — управление через UI
Content Blocks (Block content entities) создаются редакторами в /admin/content/block. Можно создать кастомный тип блока с полями через /admin/structure/block-content/types.
Программное создание типа блока:
use Drupal\block_content\Entity\BlockContentType;
use Drupal\block_content\Entity\BlockContent;
// Тип блока
$block_type = BlockContentType::create([
'id' => 'promo_banner',
'label' => 'Promo Banner',
'description' => 'Промо-баннер с изображением и кнопкой',
]);
$block_type->save();
// Добавляем тело (стандартное body поле)
block_content_add_body_field($block_type->id());
// Программно создать Content Block
$block = BlockContent::create([
'type' => 'promo_banner',
'info' => 'Летняя акция',
'body' => ['value' => '<p>Скидка 20% до 31 июля</p>', 'format' => 'full_html'],
'field_image' => [['target_id' => 42]],
'field_button_text' => 'Узнать больше',
'field_button_url' => ['uri' => 'internal:/promo'],
]);
$block->save();
Layout Builder — блоки в inline-редактировании
С включённым layout_builder редакторы размещают блоки непосредственно на странице. Кастомный плагин-блок автоматически появляется в списке доступных.
Настройка Layout Builder для типа контента:
drush en layout_builder layout_discovery -y
В /admin/structure/types/manage/{type}/display включаем Layout Builder.
Блок с формой
Блок, который рендерит форму:
use Drupal\Core\Form\FormBuilderInterface;
/**
* @Block(
* id = "my_module_search_block",
* admin_label = @Translation("Search Block"),
* )
*/
class SearchBlock extends BlockBase implements ContainerFactoryPluginInterface {
public function __construct(
array $configuration, $plugin_id, $plugin_definition,
private readonly FormBuilderInterface $formBuilder,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
public static function create(ContainerInterface $container, ...$args): static {
return new static(...$args, $container->get('form_builder'));
}
public function build(): array {
return $this->formBuilder->getForm(\Drupal\my_module\Form\SearchForm::class);
}
public function getCacheContexts(): array {
return Cache::mergeContexts(parent::getCacheContexts(), ['url.query_args:q']);
}
}
Размещение блока программно
use Drupal\block\Entity\Block;
// Создаём экземпляр блока и размещаем его в регион
$block = Block::create([
'id' => 'latest_news_sidebar',
'plugin' => 'my_module_latest_news',
'theme' => 'my_theme',
'region' => 'sidebar_first',
'weight' => -5,
'settings' => [
'label' => 'Последние новости',
'label_display' => 'visible',
'count' => 3,
'exclude_current' => TRUE,
],
'visibility' => [
'node_type' => [
'id' => 'node_type',
'bundles' => ['article' => 'article', 'news' => 'news'],
'negate' => FALSE,
'context_mapping' => ['node' => '@node.node_route_context:node'],
],
],
]);
$block->save();
Конфигурация в YAML
После размещения блоков через UI экспортируем в конфигурацию:
# block.block.latest_news_sidebar.yml
id: latest_news_sidebar
theme: my_theme
region: sidebar_first
weight: -5
plugin: my_module_latest_news
settings:
id: my_module_latest_news
label: 'Последние новости'
label_display: visible
count: 3
exclude_current: true
visibility:
node_type:
id: node_type
bundles:
article: article
news: news
negate: false
Кэширование блоков
Drupal имеет сложную систему кэширования. Для блоков:
-
cache_tags— инвалидация по тегам (при изменении данных) -
cache_contexts— вариации кэша (по роли, по URL, по языку) -
max-age— временное ограничение
public function getCacheMaxAge(): int {
// Блок не кэшируется совсем — для реального времени
return 0;
// Кэш на час
return 3600;
// Постоянный кэш с инвалидацией по тегам
return Cache::PERMANENT;
}
Ошибка — вернуть 0 для всех блоков. Это убивает производительность. Правильно — использовать теги и контексты, устанавливать max-age = Cache::PERMANENT.
Сроки
Один кастомный плагин-блок с конфигурацией: 1 день. Набор блоков с формами, Layout Builder интеграцией, сложным кэшированием: 3–5 дней.







