Why you should migrate your Symfony configs to PHP
Yesterday, I had a quick discussion on Slack in the Symfony Support channel where somebody was asking about splitting up their services.yaml
file into multiple included files.
This is something I used to do in my own applications, because, to be honest, I had a LOT of YAML configuration and I really needed a way to organise it properly.
Splitting up YAML configs
When you split up configuration across multiple YAML files, you can then include it back into the main file using the following syntax:
# services.yaml
imports:
- { resource: session-storage.yaml }
- { resource: command-handlers.yaml }
In this example, I’m splitting out individual infrastructure concerns into their own config files. It’s a valid way of managing the configuration, but not one I recommend any more.
When you are hard-coding all of your configuration, this works a treat, but there is a much better way thanks to all the hard work done by the Symfony team.
So what’s the problem with YAML configuration?
Lets back up for a moment, and consider YAML itself as a configuration file format.
When you configure your Symfony services, it is all based on class name strings. This means your IDE does not necessarily know that a certain string is actually pointing to a specific class in your codebase. Don’t get me wrong, PHPStorm is pretty good at managing this, but in my experience it misses things quite often when refactoring.
For example, you move a class to a different namespace, you also need to update the namespace in the YAML config as well. If you forget, boom your app breaks. This quickly gets tiresome. The more configuration you have, the more of a hassle this becomes.
Symfony’s solution - Auto-wiring and auto-configuration
To help alleviate this maintenance burden, Symfony’s developers came up with the auto-configuration and auto-wiring functionality. This enables you to strip out the majority of your YAML service configuration boilerplate, and rely on Symfony to wire up services for you automatically. Thanks to the compiled container, there isn’t even any performance overhead for this.
Once you have migrated your YAML configuration to take advantage of auto-wiring, based on the default service configuration, you will end up with a services.yaml
that looks somewhat like this (taken directly from the Symfony docs):
# config/services.yaml
services:
# default configuration for services in *this* file
_defaults:
# Automatically injects dependencies in your services.
autowire: true
# Automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# HERE YOU PUT YOUR REMAINING YAML CONFIG FOR STUFF THAT CANNOT BE AUTO-WIRED
You can clearly see that this reduces YAML configuration down to a tiny subset of the original hard-coded version.
This is a huge improvement, and will make refactoring your codebase a much smoother experience, preventing you from making lots of silly errors when making changes.
Why there is still a problem with this? Well, in most apps the auto-wiring will take care of most of the services you have, but there will always be exceptions that still have to be manually hard-coded into the YAML file. So you will still likely end up with a couple of hundred lines of YAML.
So what’s the alternative?
Convert to PHP configuration
Using PHP configuration instead of YAML has several advantages.
The first and most important benefit, in my opinion, is that PHP is of course natively supported by your IDE. This means when you perform a refactoring, such as renaming a class name, your IDE can intelligently also update the class in the services.php
file.
This means you immediately eliminate an entire class of errors and bugs caused by a mismatch between your YAML config and your PHP code.
Secondly, you can now put your service configuration under the watchful eye of the various code quality tools such as PHPStan/Psalm, PHPCS/ECS and even Rector.
So how do you go about making the switch to PHP configuration files?
The first step is to update your Kernel
class, and tell it to look for PHP as well as YAML files. We want both for the time being so you can make the migration in a gradual manner.
// Kernel.php
/**
* Override method from MicroKernelTrait to add support for PHP configuration
*/
private function configureContainer(
ContainerConfigurator $container,
LoaderInterface $loader,
ContainerBuilder $builder
): void {
$configDir = $this->getConfigDir();
$container->import($configDir . '/{packages}/*.yaml');
$container->import($configDir . '/{packages}/' . $this->environment . '/*.yaml');
$container->import($configDir . '/{packages}/*.php');
$container->import($configDir . '/{packages}/' . $this->environment . '/*.php');
$container->import($configDir . '/{services}.php');
$container->import($configDir . '/{services}_' . $this->environment . '.php');
}
Here we’ve allows files in the packages
directory to be either .php
or .yaml
- because bundles still install their configuration in .yaml
format for the time being we don’t want any surprises when their configuration is not loaded after installation.
We also specify that the services*
files will only be PHP, so here it instructs the kernel to look for services.php
, and the environment specific versions such as services_dev.php
and services_test.php
etc.
You then need to migrate the actual configuration to PHP. The default YAML service configuration shown above, can we represented in PHP instead by something like this:
<?php
// config/services.php
declare(strict_types=1);
namespace SymfonyComponentDependencyInjectionLoaderConfigurator;
return function (ContainerConfigurator $configurator): void
{
$parameters = $configurator->parameters();
$parameters->set('company_name', 'ACME Inc.');
$services = $configurator->services()
->defaults()
->autowire()
->autoconfigure()
;
$services->load('App\', '../src/*');
// Your remaining custom config goes here
};
If you want to define specific rules for some services, here are a few examples:
// Pass specific arguments to a service
$services->set(MyServiceThatNeedsArgumentsPassing::class)
->arg('$endpoint', '%env(ENDPOINT_URL)%')
;
// Tag a service
$services->set(MyServiceThatShouldBeTagged::class)
->tag('my_tag_name', ['priority' => 50])
;
// Create a service alias
$services->alias(MyAliasedService::class, MyServiceToBeAliased::class);
It’s all very straightforward, fully comprehended by your IDE, can easily be refactored along with the rest of your codebase. Finally you are free from debugging those pesky YAML errors.
What about bundle and package configs?
Yes, when we discussed the changes to the Kernel.php
file above, we also added support for PHP configuration of your app’s bundles. The concept is basically the same - you use .php
as the file extension and then convert the configuration to PHP. You can do this all at once, or slowly file by file over a period of time.
To ease your migration, Tomas Votruba has created symplify/config-transformer which will allow you to automate the conversion to PHP. However, this only converts to an array based PHP configuration format:
// config/packages/security.php
use SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;
return static function (ContainerConfigurator $container) {
$array = [
'firewalls' => [
'main' => [
'pattern' => '^/*',
'lazy' => true,
'anonymous' => [],
],
],
'access_control' => [
['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
],
];
$container->extension('security', $array);
}
This can then easily be converted to use the new Config Builder objects that provide a fluent interface to your PHP configurations:
// config/packages/security.php
use SymfonyConfigSecurityConfig;
return static function (SecurityConfig $security) {
$security->firewall('main')
->pattern('^/*')
->lazy(true)
->anonymous();
$security
->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']);
};
Conclusion
Symfony is certainly moving towards PHP configuration, with XML and YAML formats likely to be phased out over the coming years.
It’s definitely a good thing. YAML has been a solid choice for configuration files for a long time, but with the advent of the new Config Builder classes, it’s now easier than ever to work with PHP configuration.
You also get seamless integration with your IDE in the same language as your actual codebase, and can put your configuration under the same code quality tools as your main production code and test suite.
Finally, just not having to deal with an additional language syntax in your project is also a refreshing improvement.
Further Reading
Tomas Votruba explains more advantages of PHP configs over YAML in 10 Cool Features You Get after switching from YAML to PHP Configs