Права на конкретную запись
Роли отвечают на вопрос «кто этот пользователь». Но часто нужно другое: может ли он изменить именно эту статью? Такие правила выносят в избиратель (voter) — класс, который по действию и объекту отвечает «да» или «нет».
Создание
php bin/console make:voter PostVoter// 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\Voter;
class PostVoter extends Voter
{
public const EDIT = 'POST_EDIT';
public const DELETE = 'POST_DELETE';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::DELETE], true)
&& $subject instanceof Post;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return $subject->getAuthor() === $user;
}
}supports() решает, относится ли вопрос к этому избирателю, voteOnAttribute() даёт ответ. Гость получает отказ сразу.
Проверка в контроллере
#[Route('/blog/{id}/edit', name: 'blog_edit')]
public function edit(Post $post): Response
{
$this->denyAccessUnlessGranted(PostVoter::EDIT, $post);
// …
}Если избиратель ответил «нет», пользователь получит ошибку 403, а гость отправится на страницу входа.
Проверка атрибутом
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/blog/{id}/delete', name: 'blog_delete', methods: ['POST'])]
#[IsGranted(PostVoter::DELETE, subject: 'post')]
public function delete(Post $post, EntityManagerInterface $em): Response
{
$em->remove($post);
$em->flush();
return $this->redirectToRoute('blog_index');
}subject — имя аргумента метода, который передаётся избирателю.
Проверка в шаблоне
{% if is_granted('POST_EDIT', post) %}
<a href="{{ path('blog_edit', {id: post.id}) }}">Редактировать</a>
{% endif %}Разные правила для разных действий
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return match ($attribute) {
self::EDIT => $subject->getAuthor() === $user,
self::DELETE => $subject->getAuthor() === $user && !$subject->isPublished(),
default => false,
};
}Администратору можно всё
Избиратель — сервис и может запрашивать зависимости. Роли проверяет сервис Security:
use Symfony\Bundle\SecurityBundle\Security;
public function __construct(private Security $security) {}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
if ($this->security->isGranted('ROLE_ADMIN')) {
return true;
}
// обычные правила
}Частая ошибка
Проверки if ($post->getAuthor() !== $this->getUser()), разбросанные по контроллерам, легко забыть в одном из методов — и чужую статью сможет изменить любой. Правило должно жить в одном месте, в избирателе, а контроллеры и шаблоны только спрашивают is_granted.