docs(articles): English translation of From Magento to Symfony
en/articles/magento-to-symfony/core/di.md
new file +239
−0
DI container
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.
Basic configuration
Symfony · config/services.yaml (default)
services:
_defaults:
autowire: true # constructor arguments by type
autoconfigure: true # tags by interfaces and attributes
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
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.
preference → alias
Magento · etc/di.xml
<preference
for="Vendor\Shipping\Api\RateProviderInterface"
type="Vendor\Shipping\Model\DhlRateProvider"/>
Symfony · an attribute on the implementation
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(RateProviderInterface::class)]
final class DhlRateProvider implements RateProviderInterface
{
}
or in services.yaml:
# ...
App\Shipping\RateProviderInterface: '@App\Shipping\DhlRateProvider'
# ...
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.
virtualType and arguments → named services
Magento · a separate log file via 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 · a Monolog channel + autowiring by name
# 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,
) {}
}
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:
Symfony · explicit arguments via attributes
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,
) {}
}
To list all types available for autowiring and all named variants: bin/console debug:autowiring, for example debug:autowiring logger.
Plugin → decorator
Magento · an after plugin
<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 · a service decorator
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;
}
}
Everyone who asks for PriceCalculatorInterface gets the decorator, and it calls the original. Differences from plugins:
- You decorate a service, not a method. You have to implement the whole interface, so interfaces with many methods are awkward to decorate.
- Entities, DTOs and objects created with
newcan't be decorated: they are not services. - If a consumer type-hints a concrete
finalclass, you can't substitute a decorator. That's why depending on interfaces matters in Symfony code.
- Look for an extension point in the bundle: a config option, an event, a tag. Check
bin/console config:dump-reference bundle_name. - Decorate the service.
- Override the service: an alias to your class in services.yaml.
- A compiler pass that modifies the service definition. This is a last resort.
Don't edit code in vendor/ or copy bundle classes.
Arrays in di.xml → tags
Magento · a pool of implementations
<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 · a tag + a locator
#[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);
}
}
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.
Factory, Proxy, shared
| Magento | Symfony | Note |
|---|---|---|
| QuestionFactory::create() | new Question(...) | Entities and DTOs are created with new, passing required fields to the constructor. |
| 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. |
| Foo\Proxy in an argument | #[Autowire(lazy: true)] or lazy: true | The service is created on the first method call. |
| shared="false" | shared: false or #[Autoconfigure(shared: false)] | A new instance for every injection. |
| ObjectManager::getInstance() | none | Services are private: $container->get() won't work in code. And it shouldn't. |
Useful commands: debug:container App\Faq\Importer (a service's arguments and tags), debug:container --tag=app.export_format, lint:container (checks argument types).