Symfony security is known for major changes in almost every Symfony version. What worked in the past...
# config/security.yml
security:
enable_authenticator_manager: true
...can be changed or removed.
This actually does not work in Symfony 7. We want to be warned early, as soon as the change happens, and without running our code.
Other PHP projects use @deprecation annotations that highlight deprecated methods, properties, constants, and classes right in code in your favorite IDE.
How about YAML? It does not, so we first have to migrate to PHP.
Move from YAML to PHP
It is not necessary to bother with a manual flip. Use symplify/config-transformer to automate the process instead:
composer require symplify/config-transformer --dev
vendor/bin/config-transformer switch-format config/security.yml
It parses YAML, maps it into PHP format, and prints it out:
# config/security.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('security', [
'enable_authenticator_manager' => true,
]);
};
Job done!
If it's your first PHP fluent config, don't forget to update Kernel to load PHP files too.
How the most common cases look after the switch
Parameters become set() calls:
# config/services.yaml
parameters:
app.locale: 'en'
# config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set('app.locale', 'en');
};
Service defaults and autodiscovery keep the same shape, just in methods:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/*'
exclude: '../src/{Entity,Kernel.php}'
# config/services.php
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->defaults()
->autowire()
->autoconfigure();
$services->load('App\\', __DIR__ . '/../src/*')
->exclude([__DIR__ . '/../src/Entity', __DIR__ . '/../src/Kernel.php']);
};
A single service with arguments and a tag - note the argument name is kept, so nothing depends on the order:
# config/services.yaml
services:
App\Service\PriceCalculator:
arguments:
$vatRate: 21
tags: ['app.calculator']
public: true
# config/services.php
use App\Service\PriceCalculator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(PriceCalculator::class)
->arg('$vatRate', 21)
->tag('app.calculator')
->public();
};
An alias - the @ string reference turns into a class constant:
# config/services.yaml
services:
App\Contract\Mailer: '@App\Mailer\SmtpMailer'
# config/services.php
use App\Contract\Mailer;
use App\Mailer\SmtpMailer;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->alias(Mailer::class, SmtpMailer::class);
};
Bundle extensions - framework, doctrine, twig, security and the rest - go through extension() with the nesting untouched:
# config/packages/framework.yaml
framework:
secret: '%env(APP_SECRET)%'
session:
handler_id: null
# config/packages/framework.php
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('framework', [
'secret' => '%env(APP_SECRET)%',
'session' => [
'handler_id' => null,
],
]);
};
Now the configs live in PHP, where Rector can reach them. That's where the second part starts.
Let Rector clean the service configs
Configs that grew over the years are full of noise - service names duplicated as types, positional args(), tags that autoconfigure handles anyway, or public() repeated on every single service.
Rector has a dedicated set for exactly this - SymfonySetList::CONFIGS:
# rector.php
use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/config'])
->withSets([
SymfonySetList::CONFIGS,
]);
Then run it on your config directory:
vendor/bin/rector process config
Here is what the 7 rules in the set do.
Drop the service name that only repeats the class
MergeServiceNameTypeRector
Service registered under its own type name says the same thing twice:
-$services->set(\App\SomeClass::class, \App\SomeClass::class);
+$services->set(\App\SomeClass::class);
Name the constructor arguments
ServiceArgsToServiceNamedArgRector
Positional args() break silently when a constructor parameter is added or reordered. Named arg() doesn't:
$services->set(SomeClass::class)
- ->args(['some_value']);
+ ->arg('$someCtorParameter', 'some_value');
Remove arguments that autowiring already handles
RemoveConstructorAutowireServiceRector
An argument that autowiring resolves on its own is just a line to maintain:
final class SomeClass
{
public function __construct(private SomeService $someService)
{
}
}
$services->defaults()
->autowire();
-$services->set(\App\SomeClass::class)
- ->arg('$someService', ref(\App\SomeService::class));
+$services->set(\App\SomeClass::class);
Replace manual tags with autoconfigure
ServiceTagsToDefaultsAutoconfigureRector
Tags like console.command, twig.extension, kernel.event_subscriber, monolog.logger and security.voter are handled by autoconfigure():
$services = $containerConfigurator->services();
+$services->defaults()
+ ->autoconfigure();
-$services->set(SomeCommand::class)
- ->tag('console.command');
+$services->set(SomeCommand::class);
Say "public" once, not on every service
FromServicePublicToDefaultsPublicRector
The same public() call repeated on every service moves to defaults() once:
$services = $containerConfigurator->services();
+$services->defaults()->public();
-$services->set(SomeCommand::class)
- ->public();
-
-$services->set(AnotherCommand::class)
- ->public();
-
-$services->set(NextCommand::class)
- ->public();
+$services->set(SomeCommand::class);
+$services->set(AnotherCommand::class);
+$services->set(NextCommand::class);
Turn a list of services into a single autodiscovery load
ServiceSettersToSettersAutodiscoveryRector
A list of set() calls from the same namespace collapses into a single load() glob. Rector finds the shared namespace and the directory for you:
-$services->set(FirstService::class);
-$services->set(SecondService::class);
+$services->load('App\\Services\\', __DIR__ . '/../src/Services');
Rename string service names to class-based ones
ServiceSetStringNameToClassNameRector
Legacy string service names become class-based ones, so $container->get() works by type. This one needs the container XML dump to map names to types:
# rector.php
return RectorConfig::configure()
->withSets([SymfonySetList::CONFIGS])
->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml');
-$services->set('some_name', App\SomeClass::class);
+$services->set('app\\someclass', App\SomeClass::class);
Run it, review the diff, and your service configs shrink to what actually carries information.
Happy coding!