Сокращаем размер конфигов Symfony до минимума

Источник: «2 Tricks to get your Symfony configs lines to minimum»
Считаю, что в каждом приложении Symfony можно уместить сервисный конфиг в 5 строк. Я поделюсь двумя техниками, которые использую последние пару лет для достижения наилучшей архитектуры с наименьшим количеством строк.

Конфиги — одна из самых недооценённых частей проектов Symfony, и заслуживают быть сделанными правильно. Подобно здоровому стволу дерева, дающему силу ветвям и листьям, ясные конфиги сохраняют архитектуру дизайна ясной и легко развивающейся.

Но прежде чем приступать к модернизации, необходим прочный фундамент. Это означает, что у нас есть конфиги на PHP, мы используем сгенерированные конфиги и load() для регистрации сервисов.

У вас всё ещё есть конфиги с более чем 10 строками? Или более 100, или 200… строк? Можно сделать лучше. Я поделюсь двумя техниками, которые использую последние пару лет для достижения наилучшей архитектуры с наименьшим количеством строк.

1. От именованных сервисов к уникальным типам

Во времена Symfony 2.8 предполагалось придумывать строковые имена для сервисов, чтобы передавать их в качестве аргументов другим сервисам:

$services->set('app.data_analyser', DataAnalyser::class);

$services->set('app.homepage_controller', HomepageController::class)
->arg('$dataAnalyser', 'app.data_analyser');

Начиная с Symfony 3.0, в этом нет реальной необходимости, поскольку каждый сервис либо уникален, например, наш DataAnalyser, либо имеет собирательный тип, например, подписчики событий.

Удалите имена сервисов, чтобы их ссылки автоматически подключались по уникальному типу:

-$services->set('app.data_analyser', DataAnalyser::class);
+$services->set(DataAnalyser::class);

-$services->set('app.homepage_controller', HomepageController::class)
- ->arg('$dataAnalyser', 'app.data_analyser');
+$services->set(HomepageController::class);

Таким образом, можно убрать большую часть бесполезного кода в конфигах. Это очевидный путь к единственному уникальному типу.

Но что, если есть несколько экземпляров одного типа?

$services->set('app.data_analyser', DataAnalyser::class)
->arg('$scope', 'production');

$services->set('app.dev_data_analyser', DataAnalyser::class)
->arg('$scope', 'dev');

Необходимо использовать строковые имена, чтобы можно было передавать эти сервисы в разные места:

$services->set(HomepageController::class);
->arg('$dataAnalyser', 'app.data_analyser');

$services->set(AnalyseCommand::class);
->arg('$dataAnalyser', 'app.dev_data_analyser');

Но так ли это? Это типичный паттерн фабрики. Создаётся несколько экземпляров одного и того же типа, но с разными значениями в __construct().

Избегайте использования паттерна фабрика в конфигах

Конфиги — не то место, где следует использовать паттерн фабрика. HomepageController всегда будет принимать один и тот же экземпляр DataAnalyser, а AnalyseCommand всегда будет принимать другой экземпляр DataAnalyser.

Что можно увидеть, прочитав конфиг?

Я вижу в этом неправильное применение паттерна фабрика для кодирования конфига.

Какой же выход?

Сделать DataAnalyser абстрактным и создать уникальные дочерние типы:

final class AppDataAnalyser extends DataAnalyser
{
public function __construct()
{
parent::__construct('production');
}
}

final class DevDataAnalyser extends DataAnalyser
{
public function __construct()
{
parent::__construct('dev');
}
}

Теперь есть 2 уникальных экземпляра, определённых в PHP-коде, вне конфигурации. Теперь эти сервисы:

Можно добавить их в контроллеры:

 final class HomepageController
{
public function __construct(
- private DataAnalyser $dataAnalyser,
+ private AppDataAnalyser $dataAnalyser,
) {
}
}

И можно очистить конфиги от всего того кода, который использовался:

-$services->set('app.data_analyser', DataAnalyser::class)
- ->arg('$scope', 'production');
-
-$services->set('app.dev_data_analyser', DataAnalyser::class)
- ->arg('$scope', 'dev');
-
-$services->set(HomepageController::class);
- ->arg('$dataAnalyser', 'app.dev_data_analyser');
-
-$services->set(AnalyseCommand::class);
- ->arg('$dataAnalyser', 'app.data_analyser');

Такой подход эффективен для обеспечения надёжности конфигов и дизайна приложений.

2. От ручного биндинга к атрибуту #[Autowire].

Автоматическое подключение по типу хорошо известно. Но иногда нужно передать скалярное значение, например имя маршрута или ключ API:

$services = $containerConfigurator->services();

$services->set(DataAnalyser::class)
->arg('$environment', '%kernel.environment%')
->arg('$secret', '%env(LOGGER_SECRET)%');

На каждую скалярную строку конфиг становится на 1 строку длиннее. 1 опасная строка, зависящая от нечёткого порядка аргументов или имени.

Атрибуты PHP 8.0 приходят на помощь

Функция #[Autowire] была добавлена в Symfony 6.1. Сначала были сомнения, стоит ли переносить логику в сам сервис. Конфигурация должна быть в конфигурационном файле, верно?

После нескольких экспериментов я изменил мнение. Теперь сервисы чётко определяют свои зависимости; не нужно прыгать туда-сюда в файл конфигурации, чтобы узнать о них.

Чтобы автоматически подключить параметр к сервису, добавьте Autowire и передайте param или env как значение аргумента:

+use \Symfony\Component\DependencyInjection\Attribute\Autowire;

final readonly class DataAnalyser
{
public __construct(
+ #[Autowire(param: 'kernel.environment')]
private $environment,
+ #[Autowire(env: 'LOGGER_SECRET')]
private $loggerSecret,
) {
}
}

Как только добавим автоматическое подключение параметров в сервисы, сможем отказаться от ручной регистрации. Можно пойти ещё дальше и отказаться от самой строки служб, поскольку мы уже регистрируем все службы через load():

 $services = $containerConfigurator->services();

-$services->set(DataAnalyser::class)
- ->arg('$environment', '%kernel.environment%')
- ->arg('$secret', '%env(LOGGER_SECRET)%');

Вот и всё! В качестве бонуса, такой код, основанный на атрибутах, может быть также проанализирован статическим анализом. Можно создать правило PHPStan, проверяющее, определён ли параметр/env, и предупреждающее об этом на ранней стадии CI.

Дополнительные материалы

Предыдущая Статья

Работа с NULL в SQL

Следующая Статья

Использование CSS анимации, основанной на прокрутке, для индикации прогресса прокрутки на основе секций