Привет! Я разрабатываю приложения на Symfony и хочу поделиться проблемами, с которыми сталкивался при использовании Symfony DI, а также дать несколько советов которые, как мне кажется, будут полезны при разработки больших приложений. Кратко я упоминал о них в этой статье, и здесь хочу развернуть мысль и поговорить подробнее.
Есть давно известная it мудрость, а том что код мы намного чаще читаем, чем пишем. На крупных проектах, работу над которыми ведёт не один разработчик или даже не одна команда, это наиболее актуально. Проект меняется быстро, следить за всеми изменениями невозможно, постоянно появляются новые классы, неймспейсы, решения, добавляются зависимости, создаются бандлы. В такой атмосфере во главу угла развития проекта я ставлю читаемость и гибкость написанного кода и di его важная часть.
Autowire
Первое препятствие на пути к этой цели это autowire. Автовайринг полезен при быстром прототипировании, так как ускоряет написание di, но в долгой перспективе скорее вредит, приведу небольшие примеры:
services:
_defaults:
autowire: true
App\Service\Service: ~
App\Component\LockInterface:
class: App\Component\Lock
final class Service {
public function __construct(private readonly LockInterface $locker)
{
}
}
Реализация успешно подтягивается в класс сервиса. Дальше сервис растёт, растёт service.yaml, возможно разбивается на более мелкие yaml, появляются новые реализации LockInterface (иначе зачем мы создавали интерфейс). И со временем взглянув на класс при чтении кода становится довольно не просто найти, а какая конкретно реализация сейчас используется.
Ситуация значительно упрощается, если явно указать реализацию: добавив всего одну строчку di при написании, мы облегчаем себе её чтение в будущем.
App\Service\Service:
arguments:
$locker: '@App\Component\Lock'
Ещё одна опасность это возможные ошибки, так как изменения реализации могут происходить неявно:
App\Component\LockInterface:
class: App\Component\MyCustomLock
Так мы одним махом меняем реализацию во всех местах использования, удобно на маленьком приложении, где мы держим в голове все места использования. Но в большой кодовой базе такое изменение может задеть места, о котором мы не подумали/забыли/не знали. Явное изменение строки di в используемом классе как минимум подсветит это. Да мы потратим больше времени, но зато изменения будет явными.
Не используйте автовайринг.
Классы вместо алиасов
В di лучше придерживаться одного стиля написания, это упрощает чтение и выбор собственно между двумя вариантами:
App\Component\Consumer:
app.component.consumer:
class: App\Component\Consumer: ~
Я предпочитаю второй, хоть он и более многословный. Причина в том, что это позволяет переиспользовать классы с разными аргументами, например так:
app.component.consumer.email_queue:
class: App\Component\Consumer:
arguments:
$queueName: 'mail'
app.component.consumer.sms_queue:
class: App\Component\Consumer:
arguments:
$queueName: 'sms'
Так не придётся добавлять новый дублирующий код или что страшнее использовать наследование. Использование обоих способов именования одновременно по необходимости вносит лишний шум в di и мешает чтению.
Используйте алиасы.
Интерфейс вместо реализации
Ещё один странный способ указания зависимостей в di, который я часто встречаю:
app.component.consumer:
class: App\Component\Consumer:
arguments:
$executor: '@App\Component\ExecutorInterface'
Есть явное указание в аргументах класса, но указание интерфейса. DI container - это про сборку проекта, то место, где мы указываем конкретные реализации интерфейсов, описанных в коде и указание здесь интрфейса просто заставляет разработчика дополнительно тратить время на чтение di в поисках реализации, а сама по себе такая строчка не несёт никакой полезной информации, то что $executor это интерфейс видно и так глядя на код.
Не делайте так.
Правила эти очень субъективные и возможно спорные, они заставляют более дисциплинированно и многословно подходить к написанию di, но по моему мнению упрощают его чтение, что приносит много пользы в долгосрочной перспективе.