DI-контейнер

← DI-контейнер

Ashterix

docs(articles): From Magento to Symfony, a requalification guide grouped into sections

uk/articles/magento-to-symfony/core/di.md новий файл +239 −0

1 + # DI-контейнер
2 +
3 + Найбільша частина знань, яка переноситься. Конструкторна ін'єкція та ж, але контейнер компілюється в PHP, усі сервіси приватні, а більшість зв'язків виводиться з типів без конфігурації.
4 +
5 + ## Базова конфігурація
6 +
7 + **Symfony** · config/services.yaml (за замовчуванням)
8 +
9 + ```yaml
10 + services:
11 + _defaults:
12 + autowire: true # аргументи конструктора за типами
13 + autoconfigure: true # теги за інтерфейсами й атрибутами
14 +
15 + App\:
16 + resource: '../src/'
17 + exclude:
18 + - '../src/DependencyInjection/'
19 + - '../src/Entity/'
20 + - '../src/Kernel.php'
21 + ```
22 +
23 + Кожен клас у `src/` автоматично стає сервісом з id, рівним FQCN. `autoconfigure` означає: якщо клас реалізує `Command`, `EventSubscriberInterface` чи має атрибут `#[AsCommand]`, його зареєструють де треба без жодного XML. Порівняйте з Magento, де команду треба додати в масив `commands` у di.xml.
24 +
25 + ## preference → alias
26 +
27 + **Magento** · etc/di.xml
28 +
29 + ```xml
30 + <preference
31 + for="Vendor\Shipping\Api\RateProviderInterface"
32 + type="Vendor\Shipping\Model\DhlRateProvider"/>
33 + ```
34 +
35 + **Symfony** · атрибут на реалізації
36 +
37 + ```php
38 + use Symfony\Component\DependencyInjection\Attribute\AsAlias;
39 +
40 + #[AsAlias(RateProviderInterface::class)]
41 + final class DhlRateProvider implements RateProviderInterface
42 + {
43 + }
44 + ```
45 + або в services.yaml:
46 + ```yaml
47 + # ...
48 + App\Shipping\RateProviderInterface: '@App\Shipping\DhlRateProvider'
49 + # ...
50 + ```
51 +
52 + Якщо інтерфейс має одну реалізацію в `src/`, alias не потрібен взагалі. Якщо реалізацій дві, контейнер впаде на етапі компіляції з повідомленням, яку обрати. Magento в такій ситуації мовчки бере останній preference за порядком модулів.
53 +
54 + ## virtualType і аргументи → іменовані сервіси
55 +
56 + **Magento** · окремий лог-файл через virtualType
57 +
58 + ```xml
59 + <virtualType name="Vendor\Faq\Logger\Handler"
60 + type="Magento\Framework\Logger\Handler\Base">
61 + <arguments>
62 + <argument name="fileName" xsi:type="string">/var/log/faq.log</argument>
63 + </arguments>
64 + </virtualType>
65 + <virtualType name="Vendor\Faq\Logger"
66 + type="Magento\Framework\Logger\Monolog">
67 + <arguments>
68 + <argument name="name" xsi:type="string">faq</argument>
69 + <argument name="handlers" xsi:type="array">
70 + <item name="file" xsi:type="object">Vendor\Faq\Logger\Handler</item>
71 + </argument>
72 + </arguments>
73 + </virtualType>
74 + <type name="Vendor\Faq\Model\Importer">
75 + <arguments>
76 + <argument name="logger" xsi:type="object">Vendor\Faq\Logger</argument>
77 + </arguments>
78 + </type>
79 + ```
80 +
81 + **Symfony** · канал Monolog + автовайринг за іменем
82 +
83 + ```yaml
84 + # config/packages/monolog.yaml
85 + monolog:
86 + channels: ['faq']
87 + handlers:
88 + faq:
89 + type: stream
90 + path: '%kernel.logs_dir%/faq.log'
91 + channels: ['faq']
92 + ```
93 + ```php
94 + // src/Faq/Importer.php
95 + final class Importer
96 + {
97 + public function __construct(
98 + private LoggerInterface $faqLogger,
99 + ) {}
100 + }
101 + ```
102 +
103 + Назва аргументу `$faqLogger` вибирає канал `faq`. Так само працюють scoped HTTP-клієнти (`HttpClientInterface $erpClient`), пули кешу та lock-фабрики. Загальний випадок, коли треба передати конкретні значення:
104 +
105 + **Symfony** · явні аргументи атрибутами
106 +
107 + ```php
108 + use Symfony\Component\DependencyInjection\Attribute\Autowire;
109 +
110 + final class QuestionSearch
111 + {
112 + public function __construct(
113 + #[Autowire(service: 'app.search.faq_client')]
114 + private SearchClient $client,
115 + #[Autowire(env: 'int:FAQ_PAGE_SIZE')]
116 + private int $pageSize,
117 + #[Autowire('%kernel.project_dir%/var/import')]
118 + private string $importDir,
119 + ) {}
120 + }
121 + ```
122 +
123 + Список усіх типів, які можна підставити автовайрингом, і всіх іменованих варіантів: `bin/console debug:autowiring`, наприклад `debug:autowiring logger`.
124 +
125 + ## Plugin → декоратор
126 +
127 + **Magento** · after-плагін
128 +
129 + ```xml
130 + <type name="Vendor\Catalog\Model\PriceCalculator">
131 + <plugin name="vendor_promo_discount"
132 + type="Vendor\Promo\Plugin\ApplyDiscount"
133 + sortOrder="10"/>
134 + </type>
135 + ```
136 + ```php
137 + class ApplyDiscount
138 + {
139 + public function afterCalculate(
140 + PriceCalculator $subject,
141 + float $result
142 + ): float
143 + {
144 + return $result * 0.9;
145 + }
146 + }
147 + ```
148 +
149 + **Symfony** · декоратор сервісу
150 +
151 + ```php
152 + use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
153 + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
154 +
155 + #[AsDecorator(decorates: PriceCalculatorInterface::class, priority: 10)]
156 + final class DiscountedPriceCalculator implements PriceCalculatorInterface
157 + {
158 + public function __construct(
159 + #[AutowireDecorated]
160 +
161 + private PriceCalculatorInterface $inner,
162 + ) {}
163 +
164 + public function calculate(Product $product): float
165 + {
166 + return $this->inner->calculate($product) * 0.9;
167 + }
168 + }
169 + ```
170 +
171 + Усі, хто просить `PriceCalculatorInterface`, отримають декоратор, а він викликає оригінал. Відмінності від плагінів:
172 +
173 + - Декорується сервіс, а не метод. Треба реалізувати весь інтерфейс, тому інтерфейси з багатьма методами декорувати незручно.
174 + - Сутності, DTO та об'єкти, створені через `new`, декорувати не можна: вони не сервіси.
175 + - Якщо споживач типізує конкретний `final` клас, декоратор не підставити. Тому в Symfony-коді важливо залежати від інтерфейсів.
176 +
177 + :::info subtitle="Коли треба змінити чужу поведінку"
178 + 1. Пошукайте точку розширення бандла: опція конфігурації, подія, тег. Подивіться `bin/console config:dump-reference назва_бандла`.
179 + 1. Декоруйте сервіс.
180 + 1. Перевизначте сервіс: alias на ваш клас у services.yaml.
181 + 1. Compiler pass, який змінює визначення сервісу. Це крайній випадок.
182 +
183 + Правити код у vendor/ або копіювати класи бандла не можна.
184 + :::
185 +
186 + ## Масиви в di.xml → теги
187 +
188 + **Magento** · пул реалізацій
189 +
190 + ```xml
191 + <type name="Vendor\Export\Model\ExportPool">
192 + <arguments>
193 + <argument name="formats" xsi:type="array">
194 + <item name="csv" xsi:type="object">Vendor\Export\Model\Format\Csv</item>
195 + <item name="xml" xsi:type="object">Vendor\Export\Model\Format\Xml</item>
196 + </argument>
197 + </arguments>
198 + </type>
199 + ```
200 +
201 + **Symfony** · тег + локатор
202 +
203 + ```php
204 + #[AutoconfigureTag('app.export_format')]
205 + interface ExportFormat
206 + {
207 + public function export(iterable $rows): string;
208 + }
209 +
210 + #[AsTaggedItem('csv')]
211 + final class CsvFormat implements ExportFormat { /* … */ }
212 +
213 + final class ExportPool
214 + {
215 + public function __construct(
216 + #[AutowireLocator('app.export_format')]
217 + private ContainerInterface $formats,
218 + ) {}
219 +
220 + public function get(string $code): ExportFormat
221 + {
222 + return $this->formats->get($code);
223 + }
224 + }
225 + ```
226 +
227 + Новий формат — це лише новий клас, що реалізує інтерфейс. `#[AutowireIterator]` дає ітератор усіх реалізацій (з пріоритетами), `#[AutowireLocator]` — ліниву мапу за ключем.
228 +
229 + ## Factory, Proxy, shared
230 +
231 + | Magento | Symfony | Примітка |
232 + |---|---|---|
233 + | QuestionFactory::create() | new Question(...) | Сутності й DTO створюють через new, у конструкторі передають обов'язкові поля. |
234 + | Factory для сервісу з runtime-параметром | Свій клас-фабрика як сервіс | Фабрика отримує залежності через DI, а параметр — у метод create(). |
235 + | Foo\Proxy в аргументі | #[Autowire(lazy: true)] або lazy: true | Сервіс створиться при першому виклику методу. |
236 + | shared="false" | shared: false або #[Autoconfigure(shared: false)] | Новий екземпляр на кожну ін'єкцію. |
237 + | ObjectManager::getInstance() | немає | Сервіси приватні: $container->get() у коді працювати не буде. І не має. |
238 +
239 + Корисні команди: `debug:container App\Faq\Importer` (аргументи й теги сервісу), `debug:container --tag=app.export_format`, `lint:container` (перевірка типів аргументів).