PermissionSubjectResolver implements PermissionSubjectResolverInterface
Resolves a permission `subject` (e.g. `roles.permissions:list`) to the (`object`, `action`) couple Casbin actually enforces against.
The Casbin policy table stores (subject_user, domain, object, action, effect)
— the permission subject from the seed (the human-readable label) is not
carried into Casbin. To answer the question "does this user hold the
permission identified by roles.permissions:list?", a translation step from
the label to the (object, action) couple is required.
That translation is read from the ArangoDB permissions collection, where
every row already exposes subject, object and action. The resolver
loads the full table once and caches the result in Memcached so subsequent
lookups are O(1) memory accesses.
Cache lifecycle:
- Lazy: the map is built on the first resolve() call after a cold cache. No work is done at boot.
- TTL safety net: a TTL (default 1 hour) prevents stale state if an explicit invalidation point is ever missed.
- Surgical invalidation: the catalog only changes through three paths,
and each one calls invalidate():
php bin/console.php auth:materialize(andphp bin/console.php auth:import)POST /permissionsDELETE /permissions/{key}
The resolver is stateless per request beyond the in-process Memcached connection — it is safe to share a single instance across the container.
Tags
Table of Contents
Interfaces
- PermissionSubjectResolverInterface
Constants
- CACHE_KEY : string = 'auth.permissions.subject_map'
- Memcached key for the cached subject → (object, action) map.
- DEFAULT_TTL : int = 3600
- Default cache TTL in seconds (1 hour). May be overridden via the constructor — typical override is 60s in dev to confirm invalidation paths during a chantier, or 0 to bypass the cache entirely (every lookup hits ArangoDB).
Properties
- $cache : Memcached
- $logger : LoggerInterface|null
- $permissionsModel : Documents
- $ttl : int
Methods
- __construct() : mixed
- Creates a new PermissionSubjectResolver.
- getMap() : array<string, array{object: string, action: string}>
- Returns the full subject → (object, action) map.
- invalidate() : void
- Drops the cached map.
- resolve() : array{object: string, action: string}|null
- Returns the `(object, action)` couple bound to a permission subject, or `null` when the subject is unknown.
- loadFromDatabase() : array<string, array{object: string, action: string}>
- Reads the full `permissions` collection and projects it as a map.
Constants
CACHE_KEY
Memcached key for the cached subject → (object, action) map.
public
string
CACHE_KEY
= 'auth.permissions.subject_map'
Hardcoded — the catalog is global and must not be partitioned per caller / per role / per anything. Exposing it as a config knob would only invite drift between writers (the controllers that invalidate) and readers (the resolver itself).
DEFAULT_TTL
Default cache TTL in seconds (1 hour). May be overridden via the constructor — typical override is 60s in dev to confirm invalidation paths during a chantier, or 0 to bypass the cache entirely (every lookup hits ArangoDB).
public
int
DEFAULT_TTL
= 3600
Properties
$cache
protected
Memcached
$cache
$logger
protected
LoggerInterface|null
$logger
= null
$permissionsModel
protected
Documents
$permissionsModel
$ttl
protected
int
$ttl
= self::DEFAULT_TTL
Methods
__construct()
Creates a new PermissionSubjectResolver.
public
__construct(Documents $permissionsModel, Memcached $cache[, int $ttl = self::DEFAULT_TTL ][, LoggerInterface|null $logger = null ]) : mixed
Parameters
- $permissionsModel : Documents
-
The ArangoDB
permissionscollection model. - $cache : Memcached
-
The shared Memcached connection — same one used by JWKS / route caches.
- $ttl : int = self::DEFAULT_TTL
-
Cache TTL in seconds.
0disables the cache (debugging only). - $logger : LoggerInterface|null = null
-
Optional logger for hot-reload telemetry.
getMap()
Returns the full subject → (object, action) map.
public
getMap() : array<string, array{object: string, action: string}>
Exposed for tests and for callers that need to enumerate the catalog (e.g. doctor commands, debug endpoints). Not intended for hot paths — the per-subject resolve() should be preferred since the map grows with the seed.
Return values
array<string, array{object: string, action: string}>invalidate()
Drops the cached map.
public
invalidate() : void
Called by the three points that mutate the permissions catalog —
auth:materialize, auth:import, and PermissionsController writes.
The next resolve() after invalidation triggers a fresh load.
resolve()
Returns the `(object, action)` couple bound to a permission subject, or `null` when the subject is unknown.
public
resolve(string $subject) : array{object: string, action: string}|null
The first call after a cold cache materializes the full map; every subsequent call inside the TTL window is a Memcached lookup followed by an in-memory array dereference.
Parameters
- $subject : string
-
The permission subject label, e.g.
roles.permissions:list.
Return values
array{object: string, action: string}|nullloadFromDatabase()
Reads the full `permissions` collection and projects it as a map.
private
loadFromDatabase() : array<string, array{object: string, action: string}>
On read failure the map is returned empty — every subsequent
resolve() falls open (returns null), which the caller must
interpret as "subject unknown" and translate into the safe default
for the caller's policy (typically: deny the projection, since
gating cannot be evaluated).