Skip to content

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 ​

php
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.

php
// 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 a ScopedAbstractEntitySet, applied by RoleScopeApplier — narrows an OData entity set to the rows the actor holds the role on. contextType is 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 ​