
Symfony:symfony Voters Skill
- 437 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
Implement Symfony security voters for object-level authorization so users access only records and actions their roles and ownership rules permit.
About
Teaches Symfony Security voters for attribute- and object-based authorization: writing voters, combining role and ownership logic, invoking them from controllers and services, and unit testing access decisions for sensitive CRUD operations.
- Voter attribute matching
- Object-level grants
- Integration with isGranted
- Role versus ownership checks
- Testable voter units
Symfony:Symfony Voters by the numbers
- 437 all-time installs (skills.sh)
- Ranked #543 of 2,222 Security skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonysymfony-votersAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 437 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
What it does
Implement Symfony security voters for object-level authorization so users access only records and actions their roles and ownership rules permit.
Files
Symfony Voters (Symfony)
Use when
- Hardening access-control or validation boundaries.
- Aligning voters/security expressions with domain rules.
Default workflow
1. Map actor/resource/action decision matrix. 2. Implement voter/constraint logic at the right boundary. 3. Wire checks at controllers and API operations. 4. Test allowed/forbidden/invalid paths comprehensively.
Guardrails
- Avoid policy logic duplication across layers.
- Do not leak privileged state via error detail.
- Preserve explicit deny behavior for sensitive actions.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- Security boundary updates.
- Integration points enforcing decisions.
- Negative-path test results.
References
reference.mddocs/complexity-tiers.md
Symfony Voters Reference (Symfony)
Use this reference for implementation details and review criteria specific to symfony-voters.
Anatomy of a voter
Extend Voter and implement supports() and voteOnAttribute().
<?php
// src/Security/Voter/PostVoter.php
namespace App\Security\Voter;
use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class PostVoter extends Voter
{
public const VIEW = 'POST_VIEW';
public const EDIT = 'POST_EDIT';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT], true)
&& $subject instanceof Post;
}
// The optional `?Vote $vote = null` 4th argument lets the voter attach
// human-readable reasons (surfaced via access_decision()).
// Symfony 7.3+/8.x — recent; verify the minimum version. Keeping the
// signature with `?Vote $vote = null` is backward-compatible.
protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token,
?Vote $vote = null,
): bool {
$user = $token->getUser();
if (!$user instanceof User) {
$vote?->addReason('The user is not authenticated.');
return false;
}
/** @var Post $subject */
return match ($attribute) {
self::VIEW => $subject->isPublished() || $subject->getAuthor() === $user,
self::EDIT => $this->canEdit($subject, $user, $vote),
default => false,
};
}
private function canEdit(Post $post, User $user, ?Vote $vote): bool
{
if ($post->getAuthor() !== $user) {
$vote?->addReason('Only the author may edit this post.');
$vote?->extraData['author_id'] = $post->getAuthor()->getId();
return false;
}
return true;
}
}Using voters
Controller attribute (preferred) — the second arg names the request attribute holding the subject:
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(PostVoter::EDIT, 'post', message: 'You cannot edit this post.', statusCode: 403)]
public function edit(Post $post): Response { /* ... */ }Imperatively in a controller:
$this->denyAccessUnlessGranted(PostVoter::EDIT, $post);Priority
Voter already implements CacheableVoterInterface (via supports()), so isGranted() only calls voters whose supportsType()/supportsAttribute() match. To order voters explicitly:
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(priority: 10)] // higher priority runs first
final class PostVoter extends Voter {}Checking a role inside a voter
Never call Security::isGranted() inside a voter — it runs with the wrong token context and can recurse. Inject AccessDecisionManagerInterface and use the token you were handed:
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
public function __construct(
private readonly AccessDecisionManagerInterface $accessDecisionManager,
) {}
// inside voteOnAttribute():
if ($this->accessDecisionManager->decide($token, ['ROLE_SUPER_ADMIN'])) {
return true; // admins bypass ownership checks
}Access decision strategies
# config/packages/security.yaml
security:
access_decision_manager:
strategy: affirmative # affirmative (default) | consensus | unanimous | priority
allow_if_all_abstain: false| Strategy | Grants access when… |
|---|---|
affirmative (default) | any voter grants |
consensus | more voters grant than deny |
unanimous | no voter denies |
priority | the first non-abstaining voter decides |
Custom strategy: strategy_service implementing AccessDecisionStrategyInterface.
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- ./vendor/bin/phpunit --filter=Voter
- php bin/console debug:container security
- ./vendor/bin/phpstan analyse
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.