ticketpark/dynamic-role-bundle
A Symfony bundle that adds support for dynamic roles, letting applications define and manage security roles at runtime instead of hardcoding them in configuration. Useful when roles need to be created, stored, or changed without redeploying.
Reality check first. Before writing any code, verify the package is installable in your project at all: the last release is from December 2017, which pins it to the Symfony 2.8/3.x era (AppKernel, services.yml, likely no symfony/flex recipe, possibly PSR-0). On Symfony 4+ you will usually need composer require ticketpark/dynamic-role-bundle --ignore-platform-reqs plus manual bundle registration in config/bundles.php, and even then the service definitions (constructor signatures on Symfony\Component\Security\Core\Role\RoleHierarchy, TokenStorage, EntityManager) will likely break. Budget an afternoon for a compatibility spike before promising anything to a product owner.
Minimal steps once it installs:
composer require ticketpark/dynamic-role-bundlenew Ticketpark\DynamicRoleBundle\TicketparkDynamicRoleBundle() in your kernel/bundles file.config/packages/ticketpark_dynamic_role.yaml or app/config/config.yml) — check Resources/config/ in the vendor directory for the actual keys; expect a DBAL connection reference and a table name.bin/console doctrine:query:sql "CREATE TABLE ..." or write your own migration mirroring the bundle's expected columns.UserInterface implementation to read roles from the bundle's repository/provider instead of (or in addition to) $this->roles.Where to look first: vendor/ticketpark/dynamic-role-bundle/Resources/config/ for service wiring, then the bundle's DependencyInjection/ directory for the config tree. The README is thin; the DI extension is the documentation.
First use case: an admin UI where a "support" or "regional-manager" role is granted to a user at runtime without a deploy. The whole point is that role assignment is data, not code — security.yaml lists role names as strings, and the DB decides who holds them.
Decorate getRoles(), don't replace it. The idiomatic shape is:
public function getRoles(): array
{
$roles = $this->staticRoles; // from your User row
foreach ($this->dynamicRoleProvider->forUser($this) as $role) {
$roles[] = $role; // or the bundle's role object -> __toString()
}
return array_unique($roles);
}
Keep static roles as the source of truth for things that must never disappear due to a DB outage (e.g. ROLE_ADMIN bootstrap accounts), and treat DB roles as additive.
Request-scoped memoization is mandatory. getRoles() is called by the token refresher, every isGranted() check, the profiler, and the security data collector. A naive DBAL read there produces dozens of identical queries per request and will show up immediately in the Symfony profiler's query count. Cache the per-user role set on the object (private ?array $cachedDynamicRoles = null) and, if you need cross-request caching, key a PSR-6 cache on user id with an explicit invalidation hook in your admin grant/revoke service.
Admin grant/revoke service, not raw SQL in a controller. Write one small service around the bundle's repository with grant(UserInterface $u, string $role), revoke(...), rolesFor(UserInterface $u). That gives you a single place to invalidate the cache above and a single place to emit a domain event (audit log of who granted what — you will be asked for this).
DBAL means no Unit of Work. There is no flush(), no dirty checking, no automatic updated_at. If you need auditing columns, use DB-level DEFAULT CURRENT_TIMESTAMP triggers or write them explicitly in your grant service. Correspondingly, wrap multi-row grants in an explicit transaction via the DBAL Connection the bundle is configured with.
Composition with the role hierarchy. Put hierarchy shape in security.yaml (ROLE_MANAGER: [ROLE_SUPPORT]) and keep membership in the DB. That way a hierarchy change is a deploy (reviewed, intentional) while a membership change is a support action. Do not try to make the hierarchy itself dynamic — see gotchas.
Test at the functional level. Because the persistence is raw DBAL, unit tests with mocks prove nothing. Use a KernelTestCase, boot the kernel, seed the roles table, and assert $this->client->getContainer()->get('security.authorization_checker')->isGranted(...). SQLite in-memory works only if the schema SQL is portable; test on the same driver family as production.
Migration tip: if you adopt this, immediately copy the bundle's expected table definition into your own Doctrine migration and stop relying on any doctrine:schema tooling the bundle offers. That decouples you from the abandoned package and makes replacing it later a data-compatible swap.
Role hierarchy is compile-time. security.role_hierarchy is baked into a RoleHierarchy-like service at container build. If you were hoping the DB would also drive hierarchy edges, you will need to decorate the security.role_hierarchy service — and be aware that a decorator doing DB reads runs on every authorization decision. Prefer flat role grants over dynamic hierarchy.
Cache invalidation is the real bug factory. On Symfony 4+ the security token and any security.role_hierarchy caching mean a granted role may not take effect until re-login. Document a "roles applied after next login" expectation, or force $tokenStorage->setToken(null) / re-authenticate after a grant. Users will report "I gave them the role and it doesn't work" more than any other issue.
Abandonment risk is the headline gotcha. Zero stars, zero dependents, last release 2017. There is no security-response path. Treat the bundle as reference code you vendor, not a dependency: either fork it into src/ or reimplement the ~200 lines of DBAL reads yourself. The value it provides is genuinely small — a table, a repository, a getRoles() integration.
License is MIT, so forking is fine — keep the copyright header, and add a README note explaining why you vendored it. Future maintainers will thank you for the paper trail.
Debugging dynamic roles: bin/console debug:container security.role_hierarchy and debug:container security.authorization_checker to confirm wiring; doctrine:query:sql "SELECT * FROM <role_table> WHERE ..." to confirm data; the profiler's Security panel to see the actual token roles. If isGranted() fails but the profiler shows the role on the token, the problem is hierarchy or a voter, not the bundle.
Extension points to look for: the bundle almost certainly exposes its repository/provider as a public service so you can decorate it. Decoration is the right lever for caching and auditing — it keeps your code out of the bundle's internals and survives a patch bump (not that one is coming).
Don't put this on the request hot path for anonymous users. Guard the lookup so it only executes when a token actually has a user; otherwise every 404 page hits the roles table.
Verdict for day-to-day use: acceptable as a stopgap for a small, trusted admin surface where you control the table and have a fork in-repo. Not acceptable as a dependency you composer update and forget — the 2017 date means every Symfony minor bump is a coin flip.
How can I help you explore Laravel packages today?