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` (перевірка типів аргументів).