docs(articles): English translation of From Magento to Symfony
en/articles/magento-to-symfony/core/di.md
new file +239
โ0
1
+
# DI container
2
+
3
+
The biggest chunk of knowledge that carries over. Constructor injection is the same, but the container is compiled into PHP, all services are private, and most wiring is inferred from types without any configuration.
4
+
5
+
## Basic configuration
6
+
7
+
**Symfony** ยท config/services.yaml (default)
8
+
9
+
```yaml
10
+
services:
11
+
_defaults:
12
+
autowire: true # constructor arguments by type
13
+
autoconfigure: true # tags by interfaces and attributes
14
+
15
+
App\:
16
+
resource: '../src/'
17
+
exclude:
18
+
- '../src/DependencyInjection/'
19
+
- '../src/Entity/'
20
+
- '../src/Kernel.php'
21
+
```
22
+
23
+
Every class in `src/` automatically becomes a service whose id is its FQCN. `autoconfigure` means: if a class implements `Command`, `EventSubscriberInterface` or has the `#[AsCommand]` attribute, it gets registered where needed without any XML. Compare that with Magento, where you have to add a command to the `commands` array in 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** ยท an attribute on the implementation
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
+
or in services.yaml:
46
+
```yaml
47
+
# ...
48
+
App\Shipping\RateProviderInterface: '@App\Shipping\DhlRateProvider'
49
+
# ...
50
+
```
51
+
52
+
If an interface has a single implementation in `src/`, you don't need an alias at all. If there are two implementations, the container fails at compile time with a message asking which one to pick. In the same situation Magento silently takes the last preference by module order.
53
+
54
+
## virtualType and arguments โ named services
55
+
56
+
**Magento** ยท a separate log file via 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** ยท a Monolog channel + autowiring by name
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
+
The argument name `$faqLogger` selects the `faq` channel. Scoped HTTP clients (`HttpClientInterface $erpClient`), cache pools and lock factories work the same way. The general case, when you need to pass specific values:
104
+
105
+
**Symfony** ยท explicit arguments via attributes
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
+
To list all types available for autowiring and all named variants: `bin/console debug:autowiring`, for example `debug:autowiring logger`.
124
+
125
+
## Plugin โ decorator
126
+
127
+
**Magento** ยท an after plugin
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** ยท a service decorator
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
+
Everyone who asks for `PriceCalculatorInterface` gets the decorator, and it calls the original. Differences from plugins:
172
+
173
+
- You decorate a service, not a method. You have to implement the whole interface, so interfaces with many methods are awkward to decorate.
174
+
- Entities, DTOs and objects created with `new` can't be decorated: they are not services.
175
+
- If a consumer type-hints a concrete `final` class, you can't substitute a decorator. That's why depending on interfaces matters in Symfony code.
176
+
177
+
:::info subtitle="When you need to change someone else's behavior"
178
+
1. Look for an extension point in the bundle: a config option, an event, a tag. Check `bin/console config:dump-reference bundle_name`.
179
+
1. Decorate the service.
180
+
1. Override the service: an alias to your class in services.yaml.
181
+
1. A compiler pass that modifies the service definition. This is a last resort.
182
+
183
+
Don't edit code in vendor/ or copy bundle classes.
184
+
:::
185
+
186
+
## Arrays in di.xml โ tags
187
+
188
+
**Magento** ยท a pool of implementations
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** ยท a tag + a locator
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
+
A new format is just a new class implementing the interface. `#[AutowireIterator]` gives you an iterator over all implementations (with priorities), `#[AutowireLocator]` gives a lazy map by key.
228
+
229
+
## Factory, Proxy, shared
230
+
231
+
| Magento | Symfony | Note |
232
+
|---|---|---|
233
+
| QuestionFactory::create() | new Question(...) | Entities and DTOs are created with new, passing required fields to the constructor. |
234
+
| Factory for a service with a runtime parameter | Your own factory class as a service | The factory gets its dependencies via DI, and the parameter via the create() method. |
235
+
| Foo\Proxy in an argument | #[Autowire(lazy: true)] or lazy: true | The service is created on the first method call. |
236
+
| shared="false" | shared: false or #[Autoconfigure(shared: false)] | A new instance for every injection. |
237
+
| ObjectManager::getInstance() | none | Services are private: $container->get() won't work in code. And it shouldn't. |
238
+
239
+
Useful commands: `debug:container App\Faq\Importer` (a service's arguments and tags), `debug:container --tag=app.export_format`, `lint:container` (checks argument types).