Symfony Voters Tutoriel : Gérer les Permissions d'Accès
Blog, Développement, Tutoriels

Symfony Voters Tutoriel : Gérer les Permissions d’Accès

Les rôles comme ROLE_ADMIN ne suffisent plus pour votre application Symfony ? Vous voulez vérifier si un utilisateur peut modifier uniquement ses propres articles ? Comment gérer ces permissions complexes sans surcharger vos contrôleurs ?

La solution de Symfony pour ce problème s’appelle les Voters. Ils permettent de centraliser toute cette logique de manière propre et réutilisable. Ce tutoriel vous montre comment créer et utiliser un Voter Symfony étape par étape, avec des exemples de code clairs.

Qu’est-ce qu’un Voter Symfony et pourquoi l’utiliser ?

Un Voter est une classe qui répond à une question simple : est-ce que l’utilisateur actuel a le droit de faire une action précise sur un objet donné ? Par exemple, l’utilisateur « Jean » ($user) a-t-il le droit de « modifier » ($attribute) cet article de blog ($subject) ?

Quand vous vérifiez une permission dans votre code, Symfony interroge tous les Voters enregistrés. Chaque Voter peut répondre de trois manières :

  • Oui (return true) : J’autorise cette action.
  • Non (return false) : Je refuse cette action.
  • Je ne sais pas (abstention) : Cette permission ne me concerne pas, je laisse les autres Voters décider.

Utiliser des Voters permet de centraliser la logique de sécurité au lieu de la disperser dans vos contrôleurs. Votre code devient plus propre, plus facile à lire et le Voter est réutilisable partout dans l’application. En plus, ce sont de simples classes PHP, donc elles sont faciles à tester unitairement. Pour les détails techniques, vous pouvez consulter la documentation officielle de Symfony sur les Voters.

Tutoriel : Créer votre premier Voter étape par étape

Le meilleur moyen de comprendre est de pratiquer. Nous allons créer un PostVoter. Sa mission sera de vérifier si un utilisateur a le droit de modifier (EDIT) un article de blog (un objet Post).

Étape 1 : Création de la classe Voter

D’abord, créez un nouveau fichier src/Security/Voter/PostVoter.php. La classe doit étendre la classe Voter abstraite de Symfony, qui simplifie le travail. Elle vous oblige à implémenter deux méthodes principales : supports() et voteOnAttribute().

Voici le code complet de notre PostVoter. Vous pouvez le copier directement.

<?php

namespace App\Security\Voter;

// N'oubliez pas les 'use' pour importer les classes nécessaires
use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

class PostVoter extends Voter
{
    public const EDIT = 'POST_EDIT';
    public const VIEW = 'POST_VIEW';

    // 1. La méthode supports() définit QUAND ce Voter doit être appelé
    protected function supports(string $attribute, mixed $subject): bool
    {
        // Si l'attribut (la permission) n'est pas l'un de ceux que nous gérons, on s'abstient
        if (!in_array($attribute, [self::EDIT, self::VIEW])) {
            return false;
        }

        // Si l'objet (le sujet) n'est pas une instance de notre classe Post, on s'abstient aussi
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    // 2. La méthode voteOnAttribute() contient la logique de permission
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        // Si l'utilisateur n'est pas connecté, on refuse l'accès
        if (!$user instanceof UserInterface) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        // La logique est implémentée ici
        switch ($attribute) {
            case self::EDIT:
                // On vérifie si l'utilisateur est l'auteur du post
                return $user === $post->getAuthor();
            case self::VIEW:
                // Pour cet exemple, tout utilisateur connecté peut voir un post
                return true;
        }

        // Si aucune des conditions n'est remplie, on refuse l'accès par sécurité
        return false;
    }
}

Étape 2 : La méthode `supports()` – Définir quand le Voter doit s’activer

Cette méthode est le gardien de votre Voter. Symfony l’appelle en premier pour savoir si ce Voter est pertinent pour la permission et l’objet en cours de vérification. Elle doit retourner true uniquement si le Voter peut gérer la demande.

Dans notre cas, elle vérifie deux choses :

  • Est-ce que l’attribut (la permission, ex: 'POST_EDIT') est bien géré par ce Voter ?
  • Est-ce que le sujet (l’objet, ex: une instance de Post) est du bon type ?

Si l’une de ces conditions est fausse, la méthode retourne false et Symfony ignore ce Voter pour cette vérification. C’est une simple étape de filtrage. La signature est supports(string $attribute, mixed $subject).

Étape 3 : La méthode `voteOnAttribute()` – Implémenter la logique de permission

Si supports() a retourné true, Symfony appelle voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token). C’est ici que vous écrivez votre logique de permission. Vous avez accès à tout ce qu’il faut : l’attribut ($attribute), le sujet ($subject) et l’utilisateur actuel via le $token.

Point important : La première chose à faire est de récupérer l’utilisateur et de vérifier s’il est bien connecté. Si $token->getUser() ne retourne pas un objet implémentant UserInterface, l’utilisateur est anonyme et il faut refuser l’accès.

Dans notre exemple, pour l’attribut POST_EDIT, la logique est très simple : on compare si l’utilisateur connecté ($user) est bien l’auteur de l’article de blog ($post->getAuthor()). Si c’est le cas, on retourne true (accès autorisé), sinon on retourne false (accès refusé).

Comment utiliser le Voter dans votre application ?

Maintenant que notre Voter est créé, Symfony le détecte automatiquement. Il ne reste plus qu’à l’appeler là où on en a besoin, principalement depuis un contrôleur ou un template Twig.

Depuis un contrôleur avec `denyAccessUnlessGranted()`

Le moyen le plus courant d’utiliser un Voter est depuis un contrôleur. La méthode denyAccessUnlessGranted est parfaite pour ça. Elle vérifie la permission et, si le Voter retourne false, elle lève automatiquement une exception AccessDeniedHttpException (page 403 Forbidden).

Voici un exemple dans une méthode `edit` d’un `PostController`.

<?php

namespace App\Controller;

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PostController extends AbstractController
{
    #[Route('/post/{id}/edit', name: 'post_edit')]
    public function edit(Post $post): Response
    {
        // Symfony appelle notre PostVoter en coulisses
        // Il passe 'POST_EDIT' comme $attribute et $post comme $subject
        $this->denyAccessUnlessGranted('POST_EDIT', $post);

        // Si le code continue ici, c'est que l'accès est autorisé
        // ... logique pour afficher le formulaire d'édition

        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);
    }
}

Dans les templates Twig avec la fonction `is_granted()`

Vous pouvez aussi vérifier les permissions directement dans un template Twig. C’est très utile pour afficher ou masquer des éléments de l’interface, comme un bouton « Modifier ».

La fonction is_granted() appelle votre Voter de la même manière. Voici comment afficher un bouton uniquement si l’utilisateur a la permission POST_EDIT sur l’objet post.

{# templates/post/show.html.twig #}

<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>

{# On vérifie la permission avant d'afficher le lien #}
{% if is_granted('POST_EDIT', post) %}
    <a href="{{ path('post_edit', {id: post.id}) }}">
        Modifier l'article
    </a>
{% endif %}

Cette approche est bien plus propre que de vérifier le rôle de l’utilisateur ou son ID dans le template. La logique reste centralisée dans le Voter.

Bonnes pratiques et cas d’usage avancés

Pour aller plus loin, voici quelques bonnes pratiques à adopter :

  • Utilisez des constantes pour les permissions : Au lieu d’écrire 'POST_EDIT' partout, définissez des constantes publiques dans votre classe Voter (public const EDIT = 'POST_EDIT';). C’est plus propre et évite les fautes de frappe.
  • Injectez des dépendances : Votre Voter est un service comme un autre. Vous pouvez y injecter d’autres services via le constructeur, par exemple le service Security pour faire des vérifications de rôles plus complexes en complément de votre logique.
  • Comprenez la stratégie de décision : Par défaut, si au moins un Voter dit « oui », l’accès est accordé. Vous pouvez changer cette stratégie dans votre fichier config/packages/security.yaml (ex: strategy: unanimous pour exiger que personne ne dise « non »).

FAQ – Questions fréquentes sur les Voters Symfony

Quelle est la différence entre un rôle et un Voter ?

C’est la différence entre « qui vous êtes » et « ce que vous avez le droit de faire ». Un rôle (`ROLE_ADMIN`) définit un statut global. Un Voter définit une permission sur un objet spécifique. Un admin a le rôle, mais le Voter peut lui interdire de supprimer son propre compte, par exemple.

Comment tester un Voter ?

Un Voter est une simple classe PHP. Vous pouvez donc écrire des tests unitaires très facilement. Il suffit d’instancier votre Voter, de créer des objets Post et User de test, et de vérifier que les méthodes supports() et voteOnAttribute() retournent bien true ou false dans différents cas de figure.

Un Voter peut-il fonctionner sans objet (sujet) ?

Oui. Un Voter peut gérer des permissions globales qui ne sont pas liées à un objet. Par exemple, pour vérifier si un utilisateur a le droit de créer un nouvel article. Dans ce cas, vous appelez la permission avec un sujet null : is_granted('CREATE_POST'). Votre méthode supports() devra alors accepter un sujet null pour cet attribut.

Vous pourriez également aimer...