DI-контейнер
Найбільша частина знань, яка переноситься. Конструкторна ін'єкція та ж, але контейнер компілюється в PHP, усі сервіси приватні, а більшість зв'язків виводиться з типів без конфігурації.
Базова конфігурація
Symfony · config/services.yaml (за замовчуванням)
services:
_defaults:
autowire: true # аргументи конструктора за типами
autoconfigure: true # теги за інтерфейсами й атрибутами
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
Кожен клас у src/ автоматично стає сервісом з id, рівним FQCN. autoconfigure означає: якщо клас реалізує Command, EventSubscriberInterface чи має атрибут #[AsCommand], його зареєструють де треба без жодного XML. Порівняйте з Magento, де команду треба додати в масив commands у di.xml.
preference → alias
Magento · etc/di.xml
<preference
for="Vendor\Shipping\Api\RateProviderInterface"
type="Vendor\Shipping\Model\DhlRateProvider"/>
Symfony · атрибут на реалізації
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(RateProviderInterface::class)]
final class DhlRateProvider implements RateProviderInterface
{
}
або в services.yaml:
# ...
App\Shipping\RateProviderInterface: '@App\Shipping\DhlRateProvider'
# ...
Якщо інтерфейс має одну реалізацію в src/, alias не потрібен взагалі. Якщо реалізацій дві, контейнер впаде на етапі компіляції з повідомленням, яку обрати. Magento в такій ситуації мовчки бере останній preference за порядком модулів.
virtualType і аргументи → іменовані сервіси
Magento · окремий лог-файл через virtualType
<virtualType name="Vendor\Faq\Logger\Handler"
type="Magento\Framework\Logger\Handler\Base">
<arguments>
<argument name="fileName" xsi:type="string">/var/log/faq.log</argument>
</arguments>
</virtualType>
<virtualType name="Vendor\Faq\Logger"
type="Magento\Framework\Logger\Monolog">
<arguments>
<argument name="name" xsi:type="string">faq</argument>
<argument name="handlers" xsi:type="array">
<item name="file" xsi:type="object">Vendor\Faq\Logger\Handler</item>
</argument>
</arguments>
</virtualType>
<type name="Vendor\Faq\Model\Importer">
<arguments>
<argument name="logger" xsi:type="object">Vendor\Faq\Logger</argument>
</arguments>
</type>
Symfony · канал Monolog + автовайринг за іменем
# config/packages/monolog.yaml
monolog:
channels: ['faq']
handlers:
faq:
type: stream
path: '%kernel.logs_dir%/faq.log'
channels: ['faq']
// src/Faq/Importer.php
final class Importer
{
public function __construct(
private LoggerInterface $faqLogger,
) {}
}
Назва аргументу $faqLogger вибирає канал faq. Так само працюють scoped HTTP-клієнти (HttpClientInterface $erpClient), пули кешу та lock-фабрики. Загальний випадок, коли треба передати конкретні значення:
Symfony · явні аргументи атрибутами
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class QuestionSearch
{
public function __construct(
#[Autowire(service: 'app.search.faq_client')]
private SearchClient $client,
#[Autowire(env: 'int:FAQ_PAGE_SIZE')]
private int $pageSize,
#[Autowire('%kernel.project_dir%/var/import')]
private string $importDir,
) {}
}
Список усіх типів, які можна підставити автовайрингом, і всіх іменованих варіантів: bin/console debug:autowiring, наприклад debug:autowiring logger.
Plugin → декоратор
Magento · after-плагін
<type name="Vendor\Catalog\Model\PriceCalculator">
<plugin name="vendor_promo_discount"
type="Vendor\Promo\Plugin\ApplyDiscount"
sortOrder="10"/>
</type>
class ApplyDiscount
{
public function afterCalculate(
PriceCalculator $subject,
float $result
): float
{
return $result * 0.9;
}
}
Symfony · декоратор сервісу
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
#[AsDecorator(decorates: PriceCalculatorInterface::class, priority: 10)]
final class DiscountedPriceCalculator implements PriceCalculatorInterface
{
public function __construct(
#[AutowireDecorated]
private PriceCalculatorInterface $inner,
) {}
public function calculate(Product $product): float
{
return $this->inner->calculate($product) * 0.9;
}
}
Усі, хто просить PriceCalculatorInterface, отримають декоратор, а він викликає оригінал. Відмінності від плагінів:
- Декорується сервіс, а не метод. Треба реалізувати весь інтерфейс, тому інтерфейси з багатьма методами декорувати незручно.
- Сутності, DTO та об'єкти, створені через
new, декорувати не можна: вони не сервіси. - Якщо споживач типізує конкретний
finalклас, декоратор не підставити. Тому в Symfony-коді важливо залежати від інтерфейсів.
- Пошукайте точку розширення бандла: опція конфігурації, подія, тег. Подивіться
bin/console config:dump-reference назва_бандла. - Декоруйте сервіс.
- Перевизначте сервіс: alias на ваш клас у services.yaml.
- Compiler pass, який змінює визначення сервісу. Це крайній випадок.
Правити код у vendor/ або копіювати класи бандла не можна.
Масиви в di.xml → теги
Magento · пул реалізацій
<type name="Vendor\Export\Model\ExportPool">
<arguments>
<argument name="formats" xsi:type="array">
<item name="csv" xsi:type="object">Vendor\Export\Model\Format\Csv</item>
<item name="xml" xsi:type="object">Vendor\Export\Model\Format\Xml</item>
</argument>
</arguments>
</type>
Symfony · тег + локатор
#[AutoconfigureTag('app.export_format')]
interface ExportFormat
{
public function export(iterable $rows): string;
}
#[AsTaggedItem('csv')]
final class CsvFormat implements ExportFormat { /* … */ }
final class ExportPool
{
public function __construct(
#[AutowireLocator('app.export_format')]
private ContainerInterface $formats,
) {}
public function get(string $code): ExportFormat
{
return $this->formats->get($code);
}
}
Новий формат — це лише новий клас, що реалізує інтерфейс. #[AutowireIterator] дає ітератор усіх реалізацій (з пріоритетами), #[AutowireLocator] — ліниву мапу за ключем.
Factory, Proxy, shared
| Magento | Symfony | Примітка |
|---|---|---|
| QuestionFactory::create() | new Question(...) | Сутності й DTO створюють через new, у конструкторі передають обов'язкові поля. |
| Factory для сервісу з runtime-параметром | Свій клас-фабрика як сервіс | Фабрика отримує залежності через DI, а параметр — у метод create(). |
| Foo\Proxy в аргументі | #[Autowire(lazy: true)] або lazy: true | Сервіс створиться при першому виклику методу. |
| shared="false" | shared: false або #[Autoconfigure(shared: false)] | Новий екземпляр на кожну ін'єкцію. |
| ObjectManager::getInstance() | немає | Сервіси приватні: $container->get() у коді працювати не буде. І не має. |
Корисні команди: debug:container App\Faq\Importer (аргументи й теги сервісу), debug:container --tag=app.export_format, lint:container (перевірка типів аргументів).