symfony-api-response
Symfony REST API response standard {result, error, extra, status}: thin controllers, #[MapRequestPayload] / #[MapQueryString]…
Symfony security in hexagonal architecture: #[IsGranted] on every endpoint, role hierarchy, resource voters with subject loading, voter vote reasons (7.3+), security user adapter, user provider, JSON 401/403. Works on Symfony 6.4, 7.x, 8.x. Use when: security, voter, role,
Claude Code
$ npx -y skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters --agent claude-code
Codex
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a codex -y
Cursor
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a cursor -y
Gemini CLI
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a gemini-cli -y
GitHub Copilot
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a github-copilot -y
OpenCode
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a opencode -y
Windsurf
$ npx skills add aligundogdu/symfony-hexagonal-skill --skill symfony-security-voters -a windsurf -y
name: symfony-security-voters description: "Symfony security in hexagonal architecture: #[IsGranted] on every endpoint, role hierarchy, resource voters with subject loading, voter vote reasons (7.3+), security user adapter, user provider, JSON 401/403. Works on Symfony 6.4, 7.x, 8.x. Use when: security, voter, role, authorization, access control, permission, IsGranted, role hierarchy, RBAC, firewall, authentication"
You are an expert in Symfony security with Voters pattern within hexagonal architecture.
| Feature | Requires | Fallback | |---|---|---| | `#[IsGranted('ATTR', subject: 'argName')]` | Symfony 6.2 | `AuthorizationCheckerInterface::isGranted()` | | `?Vote $vote = null` 4th voter parameter (reasons in profiler / logs) | Symfony 7.3 (part of the signature in 8.0) | Declare it anyway: harmless before 7.3 | | `#[IsGranted]` `methods:` option, `#[CurrentUser]` union types | Symfony 7.4 | separate routes / single user class | | `#[IsGranted]` with a static closure | Symfony 7.3 + PHP 8.5 | Voter | | `subject: new Expression('...')` | Symfony 6.2 | string argument name | | `UserInterface::eraseCredentials()` | deprecated 7.3 | do not keep plain passwords on the user |
**No endpoint is publicly accessible without explicit `#[IsGranted]` or Voter check.** Public endpoints use `#[IsGranted('PUBLIC_ACCESS')]` so the decision is visible.
#[Route('/api/users', methods: ['GET'])]
#[IsGranted('ROLE_USER_LIST')] // ALWAYS required
public function list(): JsonResponse { /* ... */ }Define roles in `security.yaml`:
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_USER_CREATE, ROLE_USER_EDIT, ROLE_USER_DELETE, ROLE_USER_LIST, ROLE_USER_VIEW]
ROLE_MANAGER: [ROLE_USER_LIST, ROLE_USER_VIEW, ROLE_ORDER_MANAGE]
ROLE_USER: [ROLE_USER_VIEW]#[IsGranted('ROLE_USER_CREATE')]
public function create(): JsonResponse { /* ... */ }When authorization depends on the resource ("can this user edit THIS order?"). Controllers only know the ID, so the voter receives the ID and loads what it needs through a **read port**: Presentation never touches Domain entities.
namespace App\Infrastructure\{Module}\Security;
use App\Domain\{Module}\Port\{Entity}ReadRepositoryInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** @extends Voter<string, string> */
final class {Entity}Voter extends Voter
{
public const VIEW = '{MODULE}_VIEW';
public const EDIT = '{MODULE}_EDIT';
public function __construct(
private readonly {Entity}ReadRepositoryInterface $repository,
) {
}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT], true) && is_string($subject);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
$view = $this->repository->findById($subject);
if ($view === null) {
$vote?->addReason('Resource not found');
return false;
}
return match ($attribute) {
self::VIEW, self::EDIT => $view->ownerId === $token->getUserIdentifier(),
default => false,
};
}
}#[Route('/{id}', methods: ['PUT'])]
#[IsGranted('{MODULE}_EDIT', subject: 'id')] // 'id' = controller argument name
public function update(string $id, #[MapRequestPayload] Update{Entity}Request $request): JsonResponse
{
$this->commandBus->dispatch(new Update{Entity}($id /* , ... */));
return $this->success(null);
}Use the attribute string (`'ORDER_EDIT'`) in Presentation instead of importing the voter class from Infrastructure.
See `references/` for detailed guides:
A Claude Code plugin that enforces hexagonal architecture (ports & adapters) in Symfony projects. Works with both new projects (full scaffolding) and existing projects (progressive, module-by-module refactoring).
Symfony REST API response standard {result, error, extra, status}: thin controllers, #[MapRequestPayload] / #[MapQueryString]…
Symfony CQRS with Messenger: commands, queries, handlers, command.bus / query.bus / event.bus configuration, typed bus wrappers,…
Doctrine persistence for Symfony hexagonal architecture (ORM 2.x/3.x, DBAL 3/4, DoctrineBundle 2/3): repository adapters, XML…
Symfony DDD domain modeling in pure PHP: entities, aggregates, value objects, domain events, domain exceptions, ID generation…
Symfony hexagonal architecture (ports & adapters) setup for Symfony 6.4 LTS, 7.4 LTS and 8.x: project structure, module…
Symfony Messenger async processing and Symfony Scheduler instead of cron: transports, routing (#[AsMessage] 7.2+), retry with…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.