logo

Дополнительные типы блоков (EBT) — новый опыт конструктора страниц❗

Дополнительные типы блоков (EBT) — стилизованные, настраиваемые типы блоков: слайдшоу, вкладки, карточки, аккордеоны и многие другие. Встроенные настройки для фона, DOM Box, плагины Javascript.

Демо EBT модули Скачать EBT модули

❗Дополнительные типы параграфов (EPT) — новый опыт работы с параграфами

Дополнительные типы параграфов (EPT) — набор модулей, основанный на аналогичных параграфах.

Демо EPT модули Скачать EPT модули

Scroll

Использование сокращённой записи свойств конструктора PHP (Constructor Property Promotion) в пользовательских модулях Drupal

13/06/2025, by Ivan

Menu

PHP 8 представил сокращённую запись свойств конструктора (constructor property promotion) — функцию, которая упрощает определение и инициализацию свойств класса, позволяя объявлять и присваивать значения свойствам прямо в сигнатуре конструктора. В этом руководстве показано, как использовать сокращённую запись свойств конструктора в пользовательских модулях Drupal (требуется PHP 8.0+), в частности для упрощения внедрения зависимостей (dependency injection) в сервисах и контроллерах. Мы сравним традиционный паттерн Drupal (использовался в PHP 7 и ранних версиях Drupal 9) с современным подходом PHP 8+, используя полные примеры кода для обоих вариантов. К концу статьи вы увидите, как этот современный синтаксис уменьшает шаблонный код, делает его чище и соответствует текущим лучшим практикам.

Drupal 10 (который требует PHP 8.1+) начал внедрять современные функции PHP в ядро, поэтому разработчикам пользовательских модулей рекомендуется поступать так же. Начнём с обзора традиционного паттерна внедрения зависимостей в Drupal и затем рефакторим его с помощью сокращённой записи свойств конструктора.

Традиционное внедрение зависимостей в Drupal (до PHP 8)

В сервисах и контроллерах Drupal традиционный паттерн внедрения зависимостей состоит из трёх шагов:

  1. Объявить каждую зависимость как свойство класса (обычно protected) с соответствующим docblock-комментарием.

  2. Добавить типизированные параметры зависимостей в конструктор и присвоить их свойствам класса внутри конструктора.

  3. Для контроллеров (и некоторых классов-плагинов) реализовать статический метод create(ContainerInterface $container), чтобы получать сервисы из сервис-контейнера Drupal и создавать экземпляр класса.

В результате получается довольно много шаблонного кода. Например, рассмотрим простой пользовательский сервис, которому нужны фабрика конфигурации и фабрика логгеров. Традиционно код будет выглядеть так:

Пример традиционного сервисного класса

<?php

namespace Drupal\example;

/**
 * Пример сервиса, который логирует имя сайта.
 */
class ExampleService {
  /**
   * Сервис фабрики конфигураций.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Сервис фабрики каналов логгера.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * Конструктор объекта ExampleService.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   Фабрика конфигураций.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   Фабрика каналов логгера.
   */
  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
    // Сохраняем внедрённые сервисы.
    $this->configFactory = $config_factory;
    $this->loggerFactory = $logger_factory;
  }

  /**
   * Логирует имя сайта в качестве примера.
   */
  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

В этом примере мы объявляем два свойства $configFactory и $loggerFactory и присваиваем им значения в конструкторе. Соответствующий сервис должен быть определён также в YAML-файле сервисов модуля (с нужными зависимостями в качестве аргументов), например:

# example.services.yml
services:
  example.example_service:
    class: Drupal\example\ExampleService
    arguments:
      - '@config.factory'
      - '@logger.factory'

Когда Drupal создает этот сервис, она передаст заданные аргументы в конструктор в указанном порядке.

Пример традиционного класса контроллера

Контроллеры в Drupal также используют внедрение зависимостей. Обычно класс контроллера наследуется от ControllerBase (для удобных методов) и реализует внедрение зависимостей через метод create(). Метод create() является фабрикой, которая извлекает сервисы из контейнера и вызывает конструктор. Например:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Контроллер для маршрутов Example.
 */
class ExampleController extends ControllerBase {
  /**
   * Сервис менеджера типов сущностей.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Сервис строковых переводов.
   *
   * @var \Drupal\Core\StringTranslation\TranslationInterface
   */
  protected $stringTranslation;

  /**
   * Конструктор объекта ExampleController.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Менеджер типов сущностей.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   Сервис переводов.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
    $this->entityTypeManager = $entity_type_manager;
    $this->stringTranslation = $string_translation;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    // Извлекаем необходимые сервисы из контейнера и передаём их в конструктор.
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  /**
   * Строит простой ответ страницы.
   */
  public function build(): array {
    // Пример использования внедрённых сервисов.
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

Использование сокращённой записи свойств конструктора (PHP 8+) в Drupal

Сокращённая запись свойств конструктора упрощает описанную выше схему: вы объявляете и присваиваете значения свойствам прямо в параметрах конструктора. В PHP 8 можно указать модификаторы видимости (а также, например, readonly) для параметров конструктора, и PHP автоматически создаёт и инициализирует эти свойства. Не нужно отдельно объявлять свойства и присваивать им значения в теле конструктора — всё делается автоматически.

Важно, что это всего лишь синтаксический сахар. Это не меняет способ внедрения зависимостей в Drupal; вы всё равно регистрируете сервисы в YAML (или используете автосвязывание), и для контроллеров всё так же реализуете фабричный метод create() (если только не регистрируете контроллер как сервис). Разница только в стиле написания класса. В итоге получается значительно меньше шаблонного кода, как видно по задаче в ядре Drupal, где десятки строк деклараций и присваиваний были сведены к нескольким строкам в конструкторе.

Давайте рефакторим наши примеры, используя сокращённую запись свойств конструктора.

Сервисный класс с сокращённой записью свойств конструктора

Вот переписанный ExampleService с использованием синтаксиса promoted properties PHP 8:

<?php

namespace Drupal\example;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * Пример сервиса, который логирует имя сайта (с использованием promoted properties).
 */
class ExampleService {
  /**
   * Конструктор объекта ExampleService с внедрёнными сервисами.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Фабрика конфигураций.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   Фабрика каналов логгера.
   */
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected LoggerChannelFactoryInterface $loggerFactory
  ) {
    // Тело не требуется — свойства назначаются автоматически.
  }

  /**
   * Логирует имя сайта в качестве примера.
   */
  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

Класс контроллера с сокращённой записью свойств конструктора

Теперь рассмотрим ExampleController после рефакторинга с использованием promoted properties:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Контроллер для маршрутов Example (с использованием promoted properties).
 */
final class ExampleController extends ControllerBase {
  /**
   * Конструктор ExampleController.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Менеджер типов сущностей.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
   *   Сервис переводов.
   */
  public function __construct(
    private EntityTypeManagerInterface $entityTypeManager,
    private TranslationInterface $stringTranslation
  ) {
    // Не нужно присваивать свойства вручную — всё автоматически.
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    // Передаём сервисы из контейнера в конструктор.
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  /**
   * Строит простой ответ страницы.
   */
  public function build(): array {
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

Преимущества сокращённой записи свойств конструктора

Использование promoted properties в Drupal-классах даёт несколько преимуществ:

  • Меньше шаблонного кода: Пишете значительно меньше кода. Нет необходимости объявлять свойства и присваивать им значения вручную, что особенно заметно при большом количестве зависимостей. Это делает модули чище и проще в поддержке.

  • Более наглядный и лаконичный код: Все зависимости класса видны в одном месте — в сигнатуре конструктора, а не разбросаны между объявлениями свойств и телом конструктора. Это повышает читаемость и сразу даёт понять, какие сервисы нужны классу.

  • Меньше комментариев: Благодаря типам в конструкторе можно опустить избыточные аннотации @var и @param (если назначение свойств и параметров понятно из их названия). Код становится практически самодокументируемым. Всё неочевидное всё равно можно прокомментировать, но повторения становится меньше.

  • Современный синтаксис PHP: Использование promoted properties позволяет поддерживать код актуальным в соответствии с лучшими практиками PHP. Ядро Drupal 10+ уже использует этот синтаксис для нового кода, поэтому применение его в пользовательских модулях делает ваш код более согласованным с ядром и сообществом. Это также облегчает последующую миграцию (например, начиная с PHP 8.1+ можно использовать readonly для truly immutable зависимостей).

Производительность и функциональность не меняются по сравнению с традиционным внедрением — сокращённая запись свойств конструктора является лишь языковым удобством. Вы всё так же получаете полностью типизированные свойства, которыми можно пользоваться в классе (например, $this->entityTypeManager в примере контроллера). Внутренне результат идентичен более длинному коду — только достигается проще.

Заключение

Сокращённая запись свойств конструктора — простая, но мощная возможность PHP 8, которую могут использовать разработчики Drupal для упрощения написания пользовательских модулей. Устраняя шаблонный код, вы сосредотачиваетесь на логике класса, а не на "проводке" зависимостей. Мы показали, как перевести типичный сервисный и контроллерный класс Drupal на promoted properties и сравнили с традиционным подходом. В результате получаете более компактный и поддерживаемый код без потери понятности или функциональности. По мере того как Drupal движется к современным требованиям PHP, использование таких функций, как promoted properties, поможет вашему коду быть чище, современнее и соответствовать лучшим практикам. Используйте современный синтаксис — и разработка на Drupal станет проще и элегантнее.