The original post continued with config builder classes. They were deprecated in Symfony 7.4, and the StringExtensionToConfigBuilderRector rule was removed from Rector as well. Those parts are replaced with the SymfonySetList::CONFIGS set.
Symfony configs in YAML are a pile of strings. No IDE autocomplete, no static analysis, and no @deprecated warning when an option is on its way out.
PHP configs give us a way in. Today, we'll show you how to get there without a single manual flip, and how to let Rector clean up the service definitions once you're there.
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.
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.
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.
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.
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);
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');
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);
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);
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);
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');
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!