Role Scope & the Morph Map
A #[Role] can be scoped to a model — "key account manager for this customer", "auditor for this territory". The scope is expressed as a class name at declaration time and stored as a stable morph key at sync time. That translation depends on the host registering a morph map, and getting it wrong is a silent footgun. This page is the prerequisite read.
Declaring a scoped role
use LaravelUi5\Sdk\Security\Attributes\Role;
#[Role(role: 'key_account', note: 'Key account manager for a customer.', scope: Customer::class)]
class SalesModule implements Ui5ModuleInterface
{
// ...
}note is required: a human-readable description of the role's purpose. scope: Customer::class says: this role is held per Customer, not globally. The actual grants then carry a context id (which customer), and row-level reads narrow to the rows the actor holds the role on.
Dissolution at sync time
When ui5:sync runs, RolesWorker resolves the scope FQCN → morph key through Laravel's Relation::morphMap() and stores the key, not the class path. If your morph map registers 'customer' => \App\Models\Customer::class, the stored scope is customer.
If the model is not in the morph map, the worker falls back to storing the raw FQCN. That works — but it silently couples your persisted role scopes to a class path. The day the class moves namespace, the stored keys no longer match, and grants quietly stop resolving. So:
Register your morph map before running
ui5:sync. Every model used as a#[Role]scope (or a#[Scoped*]context) must have a stable morph alias.
// AppServiceProvider::boot()
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'customer' => \App\Models\Customer::class,
'territory' => \App\Models\Territory::class,
]);The read side
Scoped roles are written by RolesWorker (sync) and read back by:
RoleAssignmentQuery(in Security) — the role-assignment rows, the one sanctioned cross-seam read.#[ScopedByRole(role, contextType)]on aScopedAbstractEntitySet, applied byRoleScopeApplier— narrows an OData entity set to the rows the actor holds the role on.contextTypeis the morph key, and the applier expects a"{contextType}_id"column (e.g.partner→partner_id). See Scoped Entity Sets.
The morph key is the shared vocabulary across all three: RolesWorker writes it, RoleAssignmentQuery filters on it, RoleScopeApplier matches it against the contextType. Keep the morph map stable and the chain holds.
See also
- Scoped Entity Sets — the OData read side (
#[ScopedByRole]) - Sync Pipeline — where
RolesWorkerdissolves the scope - Domain Model — roles, abilities, groups