---
title: 'Gestion des permissions utilisateurs avec les voters de Symfony'
description: "Centraliser les permissions d'une application Symfony avec un voter : supports(), voteOnAttribute(), l'attribut IsGranted et les quatre stratégies de décision."
pubDate: 2024-12-02
updatedDate: 2026-10-04
family: 'php-symfony'
tags: []
heroImage: /images/gestion-des-permissions-utilisateurs-avec-les-voters-de-symfony.webp
videoUrl: 'https://youtu.be/-xDGPaxond8'
faq:
  - question: 'Quand un voter est-il appelé ?'
    answer: "À chaque contrôle de permission : isGranted(), denyAccessUnlessGranted() et les règles access_control interrogent tous les voters enregistrés, qui accordent, refusent ou s'abstiennent."
  - question: 'Quelle est la signature exacte de voteOnAttribute() ?'
    answer: "À partir de Symfony 7.3 : voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool. Avant 7.3, la méthode n'a que trois paramètres, sans le Vote."
  - question: 'Faut-il enregistrer le voter dans services.yaml ?'
    answer: 'Non : un service qui étend la classe Voter est détecté et étiqueté automatiquement par Symfony.'
  - question: 'Que se passe-t-il si tous les voters s’abstiennent ?'
    answer: "L'accès est refusé : l'option allow_if_all_abstain vaut false par défaut."
  - question: 'Un voter peut-il appeler isGranted() à l’intérieur de sa décision ?'
    answer: "C'est déconseillé : la vérification ne porte pas forcément sur le même token que celui reçu par le voter. Pour consulter un autre voter, passe par AccessDecisionManager."
---

Sur une petite application, un test de rôle dans le contrôleur suffit. Dès que les règles se multiplient, un auteur qui modifie son article, un modérateur qui modère ceux des autres, la logique se disperse entre les contrôleurs, les templates et les formulaires. Les voters existent pour ça : Symfony les appelle à chaque contrôle de permission et la décision se centralise dans une seule classe.

## Tous les voters sont consultés à chaque contrôle

Quand ton code évalue une permission, avec `isGranted()`, `denyAccessUnlessGranted()` dans un contrôleur ou une règle `access_control`, Symfony interroge tous les voters enregistrés. Chacun peut accorder, refuser ou s'abstenir, et la décision finale se prend ensuite selon la stratégie configurée. Un voter n'est donc jamais consulté isolément : sa réponse participe à un vote.

## Écrire un voter

Un voter étend la classe abstraite `Voter` et implémente deux méthodes : `supports()` dit s'il se prononce sur cette paire attribut et sujet, `voteOnAttribute()` rend la décision.

```
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

final class PostVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        if (!in_array($attribute, ['EDIT', 'VIEW'], true)) {
            return false;
        }

        return $subject instanceof Post;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token,
        ?Vote $vote = null
    ): bool {
        return $subject->getAuthor() === $token->getUser();
    }
}
```

Le premier paramètre de `supports()` est un `string` : l'attribut, qui peut être un rôle comme un nom métier (`EDIT`, `VIEW`). Le sujet, lui, est typé `mixed` : c'est l'objet concerné, ou `null` quand l'attribut se suffit à lui-même.

Depuis Symfony 7.3, `voteOnAttribute()` reçoit un quatrième paramètre optionnel de type `?Vote`, qui sert à expliquer la décision avec `addReason()`. Le motif remonte alors dans les logs et les pages d'exception, ce qui aide à comprendre un refus. Sur une application en 6.4, la signature à trois paramètres reste valide.

Un comportement à connaître : tant qu'aucun attribut n'est supporté, le voter s'abstient. Dès qu'un attribut est supporté, le défaut s'inverse et devient un refus, sauf si `voteOnAttribute()` retourne `true`. Un voter qui oublie un cas refuse donc l'accès, ce qui est le comportement sûr.

L'enregistrement ne demande aucune configuration : un service qui étend `Voter` est détecté automatiquement.

## Utiliser le voter

Dans un contrôleur, l'attribut `IsGranted` fait le travail et lève une `AccessDeniedException`, donc un HTTP 403, en cas de refus :

```
#[IsGranted('EDIT', subject: 'post')]
public function edit(Post $post): Response
```

Le second argument désigne l'argument du contrôleur transmis au voter comme sujet. `is_granted()` en Twig et `denyAccessUnlessGranted()` ailleurs passent par le même chemin.

## Les quatre stratégies de décision

La stratégie se règle dans `config/packages/security.yaml`, section `access_decision_manager`. La valeur par défaut est `affirmative` :

- `affirmative` : un seul voter qui accorde suffit.
- `consensus` : la majorité l'emporte, et en cas d'égalité c'est `allow_if_equal_granted_denied` qui tranche, à vrai par défaut.
- `unanimous` : il faut qu'aucun voter ne refuse.
- `priority` : le premier voter non abstentionniste, selon l'ordre de priorité des services, décide seul.

Autre défaut à retenir : `allow_if_all_abstain` vaut `false`. Si tous les voters s'abstiennent, l'accès est refusé.

## Ce qu'il faut éviter

Appeler `isGranted()` à l'intérieur d'un voter est un raccourci tentant, et déconseillé : la vérification ne porte pas forcément sur le même token que celui reçu par le voter. Pour interroger la décision d'un autre voter, passe par `AccessDecisionManager`.

Le sujet est loin d'être théorique : une autorisation oubliée ne se voit pas dans un test qui passe. [Symfony Security Auditor](/blog/decouverte-de-symfony-security-auditor/) cherche précisément ces trous dans la logique métier, là où un voter mal branché laisse une porte ouverte. La [documentation officielle des voters](https://symfony.com/doc/current/security/voters.html) détaille chaque signature selon la version, et la [référence de configuration de la sécurité](https://symfony.com/doc/current/reference/configuration/security.html) documente les stratégies et leurs valeurs par défaut.

## Pour approfondir

**[Symfony et backend : du premier écran à la production](https://pentiminax.fr/blog/symfony-backend.md)**

Un parcours Symfony par objectif : Twig et Doctrine, projets concrets, déploiement continu, permissions et diagnostic à partir des articles du site.

[Parcourir le guide Symfony et backend](https://pentiminax.fr/blog/symfony-backend.md)

## Questions fréquentes

**Quand un voter est-il appelé ?**

À chaque contrôle de permission : isGranted(), denyAccessUnlessGranted() et les règles access_control interrogent tous les voters enregistrés, qui accordent, refusent ou s'abstiennent.

**Quelle est la signature exacte de voteOnAttribute() ?**

À partir de Symfony 7.3 : voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool. Avant 7.3, la méthode n'a que trois paramètres, sans le Vote.

**Faut-il enregistrer le voter dans services.yaml ?**

Non : un service qui étend la classe Voter est détecté et étiqueté automatiquement par Symfony.

**Que se passe-t-il si tous les voters s’abstiennent ?**

L'accès est refusé : l'option allow_if_all_abstain vaut false par défaut.

**Un voter peut-il appeler isGranted() à l’intérieur de sa décision ?**

C'est déconseillé : la vérification ne porte pas forcément sur le même token que celui reçu par le voter. Pour consulter un autre voter, passe par AccessDecisionManager.
