Использование сокращённой записи свойств конструктора PHP (Constructor Property Promotion) в пользовательских модулях Drupal
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 традиционный паттерн внедрения зависимостей состоит из трёх шагов:
- 
	Объявить каждую зависимость как свойство класса (обычно protected) с соответствующим docblock-комментарием.
- 
	Добавить типизированные параметры зависимостей в конструктор и присвоить их свойствам класса внутри конструктора. 
- 
	Для контроллеров (и некоторых классов-плагинов) реализовать статический метод 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 станет проще и элегантнее.