Un mécanisme très puissant du framework Symfony est l’autoconfiguration. Activée par défaut, elle se charge de classifier nos services dans le container, en fonction de la nature de nos classes.
Fonctionnement
Dans un container de services, on trouve énormément de services, d’origines différentes : composants Symfony, librairies, ou encore des services que nous avons écrits dans notre application.
Dans le container de services, il devient alors nécessaire de classer ces services. L’intérêt est simple : si j’ai une action particulière à effectuer, cela peut être utile de disposer d’une collection de services me permettant d’effectuer cette action, plutôt que d’aller chercher à la main le service qui répondra à ma problématique.
Activation
Il suffit d’une ligne dans les fichiers de configuration pour activer l’autoconfiguration.
Dans le fichier config/services.yaml, une directive est présente par défaut : autoconfigure: true.
Exemple : les Voters
Dans Symfony, le système de Voters permet au composant de sécurité de prendre une décision face à une situation donnée, et de gérer les permissions : est-ce que l’utilisateur a le droit d’effectuer une action sur une ressource ?
Pour créer un voter, Symfony nous indique qu’il faut créer une classe qui implémente VoterInterface ou, encore mieux, qui hérite d’une classe abstraite Voter.
Si je voulais contrôler les accès aux enregistrements d’une entité Project par exemple, je pourrais donc créer une classe ProjectVoter qui hérite de 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
{
// Calculer et prendre une décision : retourner true ou false
}
}
Une fois notre classe écrite, on peut l’inspecter dans le container de services :
php bin/console debug:container ProjectVoter
On constate alors que parmi les propriétés de notre service, se trouve une entrée Tags, qui contient la valeur security.voter.
Le système de tags
Le mécanisme d’autoconfiguration, à la lecture de notre voter, a alors classé notre service avec les autres voters du composant de sécurité. Pour le classer au bon endroit, il a simplement découvert que notre service héritait de la classe abstraite Voter.
La classification s’effectue par un système de tags : une étiquette pour savoir de quelle nature est ce service.
Nous l’avons cité plus haut, mais la classe abstraite Voter implémente elle-même l’interface VoterInterface. C’est en réalité cette interface qui est marquée pour l’autoconfiguration. Dans le bundle de sécurité de Symfony, on retrouve le tag security.voter :
// security-bundle/DependencyInjection/SecurityExtension.php
$container->registerForAutoconfiguration(VoterInterface::class)
->addTag('security.voter');
À la compilation du container de services, la classe AddSecurityVotersPass récupère les services taggés et les enregistre dans un AccessDecisionManager :
// security-bundle/DependencyInjection/Compiler/AddSecurityVoterPass.php
class AddSecurityVotersPass implements CompilerPassInterface
{
//...
public function process(ContainerBuilder $container): void
{
//...
// Récupère les voters
$voters = $this->findAndSortTaggedServices('security.voter', $container);
// Construit un tableau $voterServices contenant les services (avec ou sans debug)...
// Passe le tableau des voters au AccessDecisionManager
$container->getDefinition('security.access.decision_manager')
->replaceArgument(0, new IteratorArgument($voterServices));
}
}
AccessDecisionManager déclare une méthode collectResults qui lui permettra de déclencher les votes et récupérer les résultats :
// 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);
//...
}
}
On voit que c’est la méthode vote de chaque voter qui sera appelée !
Polymorphisme
Dans la classe AccessDecisionManager, pour chaque voter enregistré, on appelle la méthode vote et on collecte les résultats.
Du point de vue de la classe AccessDecisionManager, cela ne pose aucun problème, puisque tous les voters inclus dans cette classe sont considérés comme des instances d’un type implémentant 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 = [],
//...
) {
//...
}
}
Ainsi, dans notre cas, la classe abstraite Voter implémente VoterInterface : on a donc une définition pour la méthode vote. Et dans cette méthode, sont finalement appelées les méthodes supports et voteOnAttribute.
Ce mécanisme nous permet donc de définir notre propre code et d’ajouter notre propre logique de permission au sein du composant de sécurité de Symfony. Dans le composant de sécurité, le comportement est polymorphique : au moment d’appeler les voters, on les considère tous comme des instances implémentant une abstraction : peu importe comment la méthode vote est écrite, on sait que si l’interface VoterInterface est implémentée, alors la classe est obligée de fournir une implémentation de la méthode vote.
La classe abstraite Voter déclare elle-même deux méthodes abstraites supports et voteOnAttribute : cela force les enfants à fournir une implémentation de ces deux méthodes. Avec l’autoconfiguration et le fait que nos voters héritent de la classe Voter, toutes nos classes implémentant un mécanisme de permission seront automatiquement enregistrées et classées dans le composant de sécurité !
Autres exemples
Symfony détecte beaucoup d’autres implémentations et héritages afin de classer nos services. On peut même utiliser des attributs PHP8 pour déclencher l’autoconfiguration.
Normalizers
Les normalizers sont chargés de transformer une donnée en un tableau. Dans le composant de sérialisation de Symfony, on trouve déjà certains normalizers. Mais si on le souhaite, on peut créer notre propre normalizer.
Dans ce cas, il suffit de créer une classe qui implémente implémente l’interface NormalizerInterface, puis de définir les 3 méthodes du contrat : normalize, supportsNormalization et getSupportedTypes.
Le mécanisme d’autoconfiguration détectera tout seul les classes implémentant NormalizerInterface et les enregistrera avec le tag serializer.normalizer pour les consulter lors d’une normalisation.
Commandes de la console
Dans une application Symfony, on peut créer nos propres commandes.
Pour ce faire, il suffit de créer une classe et de l’annoter d’un attribut PHP8 #[AsCommand]. Symfony lui attribuera automatiquement le tag console.command, et la commande sera disponible avec php bin/console.