A very powerful feature in the Symfony framework is the autoconfiguration system. Enabled by default, it classifies our services in the container, based on the nature of our classes.
How it works
In the service container, we can find a lot of services, coming from different places: Symfony components, libraries, or even services that we have written in our application.
In the service container, it then becomes necessary to classify these services. The goal is simple: if I have an action to perform, it may be useful to have a collection of services that allow me to perform this action, rather than manually looking for the service that will answer my problem.
Enabling
It only takes one line in the configuration files to enable the autoconfiguration.
In the config/services.yaml file, a directive is present by default: autoconfigure: true.
Example: Voters
In Symfony, the Voters system allows the security component to make a decision based on a given situation, and to manage permissions: does the user have the right to perform an action on a resource?
To create a voter, Symfony recommends to create a class that implements VoterInterface or, even better, that inherits from a Voter abstract class.
For example, if I wanted to setup an access control for the records of a Project entity, I could create a ProjectVoter class that inherits from Voter:
<?php
namespace App\Security\Voter;
use App\Entity\Project;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
class ProjectVoter extends Voter
{
protected function supports($attribute, $subject): bool
{
return in_array($attribute, ['VIEW', 'EDIT'])
&& $subject instanceof Project;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
// Make a decision: return true or false
}
}
Once our class is written, we can inspect it in the service container:
php bin/console debug:container ProjectVoter
We can see that among the properties of our service, there is a Tags entry, which contains security.voter in this case.
The tag system
The autoconfiguration mechanism, when reading our voter, has classified our service with the other voters of the security component. To place it in the right place, it has simply discovered that our service inherited from the Voter abstract class.
The classification is done by a tag system: a label to better identify the nature of a service.
We mentioned it earlier, but the Voter abstract class implements the VoterInterface interface. This interface is actually marked for autoconfiguration. In the Symfony security bundle, we can find the security.voter tag:
// security-bundle/DependencyInjection/SecurityExtension.php
$container->registerForAutoconfiguration(VoterInterface::class)
->addTag('security.voter');
When compiling the service container, the AddSecurityVotersPass class retrieves the tagged services and registers them in an AccessDecisionManager:
// security-bundle/DependencyInjection/Compiler/AddSecurityVoterPass.php
class AddSecurityVotersPass implements CompilerPassInterface
{
//...
public function process(ContainerBuilder $container): void
{
//...
// Retrieve the voters
$voters = $this->findAndSortTaggedServices('security.voter', $container);
// Build an $voterServices array containing the services (with or without debug)...
// Transfer the array of voters to the AccessDecisionManager
$container->getDefinition('security.access.decision_manager')
->replaceArgument(0, new IteratorArgument($voterServices));
}
}
AccessDecisionManager has a collectResults method that will actually perform the votes and retrieve the results:
// security-core/Authorization/AccessDecisionManager.php
private function collectResults(TokenInterface $token, array $attributes, mixed $object, AccessDecision $accessDecision): \Traversable
{
foreach ($this->getVoters($attributes, $object) as $voter) {
$vote = new Vote();
$result = $voter->vote($token, $object, $attributes, $vote);
//...
}
}
We can see here that the vote method of each voter will be called!
Polymorphism
In the AccessDecisionManager class, for each registered voter, we call the vote method and collect the results.
From the AccessDecisionManager class point of view, this is not a problem, since all voters included in this class are considered as instances of a type implementing VoterInterface:
// security-core/Authorization/AccessDecisionManager.php
final class AccessDecisionManager implements AccessDecisionManagerInterface
{
//...
/**
* @param iterable<mixed, VoterInterface> $voters An array or an iterator of VoterInterface instances
*/
public function __construct(
private iterable $voters = [],
//...
) {
//...
}
}
So, in our case, the Voter abstract class implements VoterInterface: we therefore have a definition for the vote method. And in this method, the supports and voteOnAttribute methods are eventually called.
This mechanism allows us to define our own code and hook our own permission logic into the Symfony security component. In the security component, the behavior is polymorphic: when calling the voters, we consider them all as instances implementing an abstraction: it doesn’t matter how the vote method is written, we know that if the VoterInterface is implemented, then the class must provide an implementation of the vote method.
The Voter abstract class declares two abstract methods: supports and voteOnAttribute: this forces the children to provide an implementation of these two methods. With the autoconfiguration mechanism and the fact that our voters inherit from the Voter class, all our classes implementing a permission mechanism will be automatically registered and classified in the security component!
Other examples
Symfony also detects other implementations and inheritances in order to classify our services. We can even use PHP8 attributes to trigger the autoconfiguration.
Normalizers
Normalizers are responsible for transforming a data into an array. In the Symfony serialization component, we already have built-in normalizers. But if we want, we can create our own normalizer.
In this case, we just have to create a class that implements NormalizerInterface interface, and then implement the 3 methods of the contract: normalize, supportsNormalization and getSupportedTypes.
The autoconfiguration mechanism will automatically detect the classes implementing NormalizerInterface and register them with the serializer.normalizer tag to make it available during a normalization process.
Console commands
In a Symfony application, we can create our own console commands.
To do this, we just have to create a class and annotate it with a #[AsCommand] PHP8 attribute. Symfony will automatically add the console.command tag to the class, and the command will be available with php bin/console.