dmytrof/access-permissions-bundle
Symfony 4/5 bundle for defining access permissions with security voters. Provides AbstractVoter plus CRUD helpers/traits and an AdminInterface to expose per-user allowed attributes (view/create/edit/delete) for your entities.
Install the Bundle:
composer require dmytrof/access-permissions-bundle
Enable in config/bundles.php:
Dmytrof\AccessPermissionsBundle\DmytrofAccessPermissionsBundle::class => ['all' => true],
Set Up Security:
Ensure Symfony’s security component is installed and configured in security.yaml. Example:
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
Create a Voter for an Entity:
For an Article entity, generate a voter:
// src/Security/ArticleVoter.php
use Dmytrof\AccessPermissionsBundle\Security\{AbstractVoter, CRUDVoterInterface, Traits\CRUDVoterTrait};
class ArticleVoter extends AbstractVoter implements CRUDVoterInterface
{
use CRUDVoterTrait;
protected const SUBJECT = [Article::class];
public const PREFIX = 'app.article.';
public const VIEW = self::PREFIX . 'view';
public const CREATE = self::PREFIX . 'create';
public const EDIT = self::PREFIX . 'edit';
public const DELETE = self::PREFIX . 'delete';
public const ATTRIBUTES = [self::VIEW, self::CREATE, self::EDIT, self::DELETE];
}
Implement AdminInterface in User Model:
Extend your User entity to support admin permissions:
// src/Entity/User.php
use Dmytrof\AccessPermissionsBundle\Security\AdminInterface;
class User implements UserInterface, AdminInterface
{
public function getAdminAccessAttributes(): array
{
return [
ArticleVoter::VIEW,
ArticleVoter::CREATE,
];
}
}
Protect a Controller Action:
Use denyAccessUnlessGranted in your controller:
// src/Controller/ArticleController.php
public function create(Request $request)
{
$this->denyAccessUnlessGranted(ArticleVoter::CREATE);
// Handle creation logic
}
Test Permissions:
Log in as an admin/user and verify access is enforced. Use Symfony’s debug toolbar or bin/console debug:security to inspect voters.
ArticleVoter::CREATE permission can create articles.ArticleVoter (above).ArticleVoter::CREATE to admin users via getAdminAccessAttributes().$this->denyAccessUnlessGranted(ArticleVoter::CREATE) to the create() action.AbstractVoter and use CRUDVoterTrait for standard VIEW, CREATE, EDIT, DELETE permissions.
class ArticleVoter extends AbstractVoter implements CRUDVoterInterface
{
use CRUDVoterTrait;
protected const SUBJECT = [Article::class];
public const PREFIX = 'app.article.';
}
canRoleXyz methods for role-specific rules. Example for authors:
protected function canRoleAuthorEdit(TokenInterface $token, $subject = null): bool
{
return $subject instanceof Article
&& $token->getUser() instanceof Author
&& $subject->getAuthor() === $token->getUser();
}
Order Matters: Voters check canRoleAuthorEdit → canRoleAuthor → can (fallback).user.permissions table) and return them via getAdminAccessAttributes().
public function getAdminAccessAttributes(): array
{
return $this->permissions->toArray(); // Load from DB
}
role_hierarchy in security.yaml or dynamically add roles via RolesContainer:
$rolesContainer->addRole('ROLE_AUTHOR');
denyAccessUnlessGranted with the attribute:
$this->denyAccessUnlessGranted(ArticleVoter::EDIT, $article);
$this->denyAccessUnlessGranted(ArticleVoter::DELETE, $article);
AccessAttributesChoiceType or AccessAttributesCollectionType in user forms:
$builder->add('permissions', AccessAttributesChoiceType::class, [
'voters' => [$articleVoter, $authorVoter],
]);
public function getAccessAttributes(VotersContainer $votersContainer)
{
return $votersContainer->getAttributeDescriptionsCollection()->sort()->getAsArray();
}
translations/access_attributes.en.yaml:
app:
attributes:
view: "View"
create: "Create"
subjects:
article:
label: "Articles"
attributes:
view: "View articles"
Access labels in templates:
{{ 'app.attributes.view.label'|trans }}
CRUDVoterTrait:
class CustomVoter extends AbstractVoter
{
protected function can(TokenInterface $token, $object, array $attributes): bool
{
// Custom logic
}
}
permissions JSON column to your users table or a separate user_permissions table:
/**
* @ORM\Column(type="json")
*/
private array $permissions = [];
Populate via getAdminAccessAttributes().TokenInterface and test canRoleXyz methods:
public function testCanRoleAuthorEdit()
{
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($author);
$this->assertTrue($articleVoter->canRoleAuthorEdit($token, $article));
}
login fixture and denyAccessUnlessGranted assertions:
public function testCreateArticleRequiresPermission()
{
$client = static::createClient();
$client->loginUser($nonAdminUser);
$client->request('POST', '/articles');
$this->assertResponseStatusCodeSame(403); // Forbidden
}
isAttributeEnabled() in your voter:
public function isAttributeEnabled(string $attribute, TokenInterface $token): bool
{
return $this->permissionService->check($attribute, $token->getUser());
}
getAdminAccessAttributes():
public function getAdminAccessAttributes(): array
{
return $this->permissions->matching($this->queryBuilder)->toArray();
}
OwnershipVoterTrait):
trait OwnershipVoterTrait
{
protected function isOwner(TokenInterface $token, $subject): bool
{
return $subject->getOwner() === $token->getUser();
}
}
PermissionUpdatedEvent):
// src/EventListener/PermissionListener.php
public function onPermissionUpdated(PermissionUpdatedEvent $event)
{
$this->eventDispatcher->dispatch(new PermissionChangedEvent($event->getUser()));
}
How can I help you explore Laravel packages today?