Использование сокращённой записи свойств конструктора 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 станет проще и элегантнее.