Permission Registry
Contract for registering administrative permissions, dependency metadata, and layer-aware grouping.
Overview
App\Services\PermissionRegistry is the framework registry for administrative permissions used by roles, authorization setup, and permission-aware UI grouping.
Core binds it as a singleton in AppServiceProvider and seeds it from PermissionSeeder::permissions(). The shipped catalogue is large and still evolving, so this page documents the metadata contract, dependency behavior, and discovery helpers rather than reproducing the entire seeder inline.
Public surface
| Method | Purpose |
|---|---|
register() |
Register one permission definition |
registerMany() |
Register multiple definitions atomically |
get() |
Return metadata for one permission or null |
all() |
Return the full keyed metadata map |
keys() |
Return the registered permission keys |
grouped() |
Group permissions by domain group |
byLayer() |
Filter permissions by authorisation layer |
groupedByLayer() |
Group permissions by layer, then group |
dependenciesFor() |
Resolve the recursive dependency closure |
validate() |
Reject unknown permission keys with ValidationException |
has() |
Check whether a permission key exists |
This metadata registry does not publish a separate registered-value interface. Its current framework contract is the typed public surface on this page plus the documented registration shape and runtime behavior below.
Accepted registration shape
Each permission is identified by a unique dotted key such as settings.view, settings.manage, or webhooks.manage.
The accepted metadata shape is:
[
'label' => 'View Settings',
'description' => 'View system settings',
'group' => 'Settings',
'sub_group' => null, // optional
'layer' => 'action', // optional, defaults to action
'dependencies' => ['settings.access'], // optional
]
The current shipped metadata uses three layers:
areaactionfield
This metadata shape is a PHPDoc and static-analysis contract. The registry does not perform runtime non-empty or type validation on the metadata fields: empty strings and dependency entries are stored unchanged. Missing required keys or incompatible values fail through ordinary PHP or downstream consumer behaviour. Runtime registration validation is limited to duplicate permission keys.
Identity, collisions, and dependency behavior
Permission identity is the permission key itself.
register()throwsInvalidArgumentExceptionwhen a key is already registeredregisterMany()performs duplicate checks before mutating state, so collisions leave the registry unchanged
Dependencies are additive metadata and are expected to be a list of strings, but the registry does not validate that shape or require those strings to identify registered permissions.
Unknown dependency keys are accepted. When dependenciesFor() reaches one, the unknown key remains in the returned closure and traversal of that branch stops. Calling dependenciesFor() with an unknown starting key still returns an empty array.
Dependency cycles are also accepted. Resolution tracks keys it has already added, so traversal terminates rather than throwing. Because the starting key is not pre-seeded into that set, a cycle back to the root includes the starting permission key in the returned closure.
Grouping, ordering, and discovery guidance
The registry exposes two discovery views:
grouped()returnsgroup => permissionsgroupedByLayer()returnslayer => group => permissions
That second shape is what the role and permissions editor uses to render area, action, and field sections.
The registry does not apply an extra sort pass. Group and permission order follow the order in which PermissionSeeder::permissions() registered them.
Representative shipped permissions include:
| Permission | Layer | Group | Current meaning |
|---|---|---|---|
settings.access |
area |
Settings |
Entry gate for the settings area |
settings.view |
action |
Settings |
Read system settings |
settings.manage |
action |
Settings |
Update settings |
costs.view |
field |
Global |
Field-level visibility for cost data |
webhooks.manage |
action |
System |
Manage outbound webhooks |
Use keys(), groupedByLayer(), and dependenciesFor() for discovery instead of copying the full seeder output into callers.
Lookup and failure behavior
get($key)returns the metadata array ornullhas($key)is the non-throwing existence checkvalidate($permissions)throwsValidationExceptionwhen any supplied key is unknowndependenciesFor($key)returns[]for unknown keys and for keys without dependencies
The registry itself does not authorize anything. It publishes the canonical metadata and dependency graph that higher-level authorization flows consume.
Runtime consumption
Current consumers include:
CreateRoleandUpdateRole, which callvalidate()before persisting permission selections- the Livewire role editor, which uses
groupedByLayer()for the area/action/field UI PermissionSimulator, which resolves metadata and dependencies to explain effective grants
Worked example
The current core shape for a dependent permission looks like this:
$registry->register('settings.manage', [
'label' => 'Manage Settings',
'description' => 'Create, update, and delete system settings',
'group' => 'Settings',
'dependencies' => ['settings.view'],
]);
With the companion settings.view and settings.access definitions registered:
validate(['settings.manage'])succeedsdependenciesFor('settings.manage')returnssettings.viewandsettings.accessgroupedByLayer()['action']['Settings']includessettings.manage