PHPackages                             offload-project/laravel-mandate - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. offload-project/laravel-mandate

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

offload-project/laravel-mandate
===============================

Unified authorization management for Laravel - roles, permissions, and feature flags in a type-safe API

v3.5.0(3mo ago)61.3k↓50%MITPHPPHP ^8.2CI passing

Since Dec 16Pushed 1mo agoCompare

[ Source](https://github.com/offload-project/laravel-mandate)[ Packagist](https://packagist.org/packages/offload-project/laravel-mandate)[ Docs](https://github.com/offload-project/laravel-mandate)[ RSS](/packages/offload-project-laravel-mandate/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (12)Versions (26)Used By (0)

 [![Latest Version on Packagist](https://camo.githubusercontent.com/31b5aee47113846745251a7d96cc2330f3f1d5f12e256c110e65a2c65b3d7b88/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6d616e646174652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/offload-project/laravel-mandate) [![GitHub Tests Action Status](https://camo.githubusercontent.com/5fb7f2077a9bf0f53b3fd4d1d0e0faed1e477cc855501c11c7ecd91384d89dd7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6d616e646174652f74657374732e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265)](https://github.com/offload-project/laravel-mandate/actions) [![Total Downloads](https://camo.githubusercontent.com/5a7e7c0210d6fdc862b69a4d4ab621742eda531e7c0188f231c0e0399811e129/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6d616e646174652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/offload-project/laravel-mandate)

Laravel Mandate
===============

[](#laravel-mandate)

A role-based access control (RBAC) package for Laravel with a clean, intuitive API.

Features
--------

[](#features)

- **Roles &amp; Permissions** — Assign roles to users, grant permissions to roles or directly to users
- **Capabilities** — Group permissions into semantic capabilities for cleaner authorization logic
- **Multi-Tenancy** — Scope roles and permissions to context models (Team, Organization, Project)
- **Feature Integration** — Delegate feature access checks to external packages (Flagged, etc.)
- **Wildcard Permissions** — Pattern matching with `article:*` or `*.edit` syntax
- **Multiple Guards** — Scope authorization to different authentication guards
- **Laravel Gate** — Automatic registration with Laravel's authorization system
- **Blade Directives** — `@role`, `@permission`, `@capability`, and more
- **Route Middleware** — Protect routes with `permission:`, `role:`, or `role_or_permission:`
- **Fluent Builder** — Expressive chained authorization checks
- **Query Scopes** — Filter models by role or permission
- **UUID/ULID Support** — Use any primary key type for models and morph columns
- **Caching** — Built-in permission caching with automatic invalidation
- **Events** — Hook into role, permission, and capability changes
- **Artisan Commands** — Create and manage roles, permissions, and capabilities from CLI
- **Code-First Definitions** — Define permissions, roles, and capabilities in PHP classes with attributes

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage](#usage)
    - [Roles](#roles)
    - [Permissions](#permissions)
    - [Assigning Permissions to Roles](#assigning-permissions-to-roles)
    - [Using PHP Enums](#using-php-enums)
- [Protecting Routes](#protecting-routes)
- [Blade Directives](#blade-directives)
- [Fluent Authorization Builder](#fluent-authorization-builder)
- [Laravel Gate Integration](#laravel-gate-integration)
- [Query Scopes](#query-scopes)
- [Artisan Commands](#artisan-commands)
- [Configuration](#configuration)
    - [UUID / ULID Primary Keys](#uuid--ulid-primary-keys)
    - [Custom Column Names](#custom-column-names)
    - [Wildcard Permissions](#wildcard-permissions)
- [Capabilities](#capabilities)
- [Context Model (Multi-Tenancy)](#context-model-multi-tenancy)
- [Feature Integration](#feature-integration)
- [Code-First Definitions](#code-first-definitions)
    - [Programmatic Sync](#programmatic-sync)
- [Multiple Guards](#multiple-guards)
- [Events](#events)
- [Exceptions](#exceptions)
- [Extending Models](#extending-models)
- [Testing](#testing)
- [Upgrading from 1.x](#upgrading-from-1x)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11.x or 12.x or 13.x

Installation
------------

[](#installation)

```
composer require offload-project/laravel-mandate
```

```
# Core migrations (permissions, roles, pivot tables)
php artisan vendor:publish --tag=mandate-migrations
php artisan migrate
```

That's it. No configuration required for most applications.

**Publish the config file for customization:**

```
php artisan vendor:publish --tag=mandate-config
```

**Optional migrations** (publish only what you need):

```
# Capabilities feature (semantic permission groups)
php artisan vendor:publish --tag=mandate-migrations-capabilities

# Metadata columns (label/description for permissions, roles, capabilities)
php artisan vendor:publish --tag=mandate-migrations-meta
```

Quick Start
-----------

[](#quick-start)

Add the trait to any Eloquent model that needs roles and permissions (User, Team, etc.):

```
use OffloadProject\Mandate\Concerns\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}
```

Create roles and permissions, then assign them:

```
use OffloadProject\Mandate\Models\Permission;
use OffloadProject\Mandate\Models\Role;

// Create a role with permissions
$admin = Role::create(['name' => 'admin']);
$admin->grantPermission(Permission::create(['name' => 'article:edit']));

// Assign to a user
$user->assignRole('admin');

// Check authorization
$user->hasPermission('article:edit'); // true
$user->hasRole('admin');               // true
```

---

Usage
-----

[](#usage)

### Roles

[](#roles)

```
// Assign roles
$user->assignRole('editor');
$user->assignRole(['editor', 'moderator']);

// Remove roles
$user->removeRole('editor');

// Replace all roles
$user->syncRoles(['editor', 'moderator']);

// Check roles
$user->hasRole('admin');                       // Has this role?
$user->hasAnyRole(['admin', 'editor']);        // Has any of these?
$user->hasAllRoles(['admin', 'editor']);       // Has all of these?
$user->hasExactRoles(['editor', 'moderator']); // Has exactly these (no more, no less)?

// Get role info
$user->getRoleNames(); // Collection: ['editor', 'moderator']
$user->getRoleIds();   // Collection: [1, 2]
```

### Permissions

[](#permissions)

```
// Grant permissions directly to a user
$user->grantPermission('article:publish');
$user->grantPermission(['article:publish', 'article:delete']);

// Revoke permissions
$user->revokePermission('article:publish');

// Replace all direct permissions
$user->syncPermissions(['article:view', 'article:edit']);

// Check permissions (checks both direct and role-based)
$user->hasPermission('article:edit');
$user->hasAnyPermission(['article:edit', 'article:delete']);
$user->hasAllPermissions(['article:edit', 'article:delete']);

// Check only direct permissions (ignores role-based)
$user->hasDirectPermission('article:edit');

// Get all permissions
$user->getAllPermissions();     // Direct + via roles
$user->getDirectPermissions(); // Direct only
$user->getPermissionsViaRoles(); // Via roles only

// Get permission info
$user->getPermissionNames(); // Collection: ['article:view', 'article:edit']
$user->getPermissionIds();   // Collection: [1, 2]
```

### Assigning Permissions to Roles

[](#assigning-permissions-to-roles)

```
$role = Role::findByName('editor');

$role->grantPermission('article:edit');
$role->grantPermission(['article:edit', 'article:publish']);

$role->revokePermission('article:publish');

$role->syncPermissions(['article:view', 'article:edit']);

$role->hasPermission('article:edit'); // true
```

### Using PHP Enums

[](#using-php-enums)

Define permissions or roles as enums for type safety:

```
enum Permission: string
{
    case ViewArticles = 'article:view';
    case EditArticles = 'article:edit';
    case DeleteArticles = 'article:delete';
}

// Use enum values anywhere
$user->grantPermission(Permission::EditArticles);
$user->hasPermission(Permission::EditArticles); // true
```

### Using IDs

[](#using-ids)

All methods that accept names also accept IDs directly — integer, UUID, or ULID:

```
// Pass integer IDs
$user->grantPermission(1);
$user->syncPermissions([1, 2, 3]);
$user->assignRole(1);
$user->syncRoles([1, 2]);

// UUID and ULID string IDs are also detected automatically
$user->grantPermission('550e8400-e29b-41d4-a716-446655440000');
$user->assignRole('01ARZ3NDEKTSV4RRFFQ69G5FAV');

// Works on roles and capabilities too
$role->grantPermission($permission->id);
$role->syncPermissions([$id1, $id2]);
$role->assignCapability($capability->id);
$capability->grantPermission($permission->id);
```

---

Protecting Routes
-----------------

[](#protecting-routes)

### Middleware

[](#middleware)

```
// Single permission
Route::get('/articles', [ArticleController::class, 'index'])
    ->middleware('permission:article:view');

// Multiple permissions (user must have ANY)
Route::get('/admin', [AdminController::class, 'index'])
    ->middleware('permission:admin:access|admin:view');

// Role-based
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('role:admin');

// Role OR permission (user needs any one)
Route::get('/reports', [ReportController::class, 'index'])
    ->middleware('role_or_permission:admin|report:view');
```

### Route Macros

[](#route-macros)

Fluent syntax for route definitions:

```
Route::get('/articles', [ArticleController::class, 'index'])
    ->permission('article:view');

Route::get('/admin', [AdminController::class, 'index'])
    ->role('admin');

Route::get('/reports', [ReportController::class, 'index'])
    ->roleOrPermission('admin|report:view');
```

---

Blade Directives
----------------

[](#blade-directives)

### Role Checks

[](#role-checks)

```
@role('admin')
    {{-- User has admin role --}}
@endrole

@hasrole('admin')
    {{-- Alias for @role --}}
@endhasrole

@unlessrole('guest')
    {{-- User does NOT have guest role --}}
@endunlessrole

@hasanyrole('admin|editor')
    {{-- User has admin OR editor --}}
@endhasanyrole

@hasallroles(['admin', 'editor'])
    {{-- User has admin AND editor --}}
@endhasallroles

@hasexactroles(['editor', 'moderator'])
    {{-- User has exactly these roles --}}
@endhasexactroles
```

### Permission Checks

[](#permission-checks)

```
@permission('article:edit')
    Edit
@endpermission

@haspermission('article:edit')
    {{-- Alias for @permission --}}
@endhaspermission

@unlesspermission('article:edit')
    {{-- User does NOT have permission --}}
@endunlesspermission

@hasanypermission(['article:edit', 'article:delete'])
    {{-- User has any of these --}}
@endhasanypermission

@hasallpermissions(['article:edit', 'article:publish'])
    {{-- User has all of these --}}
@endhasallpermissions
```

---

Fluent Authorization Builder
----------------------------

[](#fluent-authorization-builder)

For complex authorization checks, use the fluent builder:

```
use OffloadProject\Mandate\Facades\Mandate;

// Simple checks
Mandate::for($user)->can('article:edit');       // Single permission
Mandate::for($user)->is('admin');               // Single role

// Chained with OR
Mandate::for($user)
    ->hasRole('admin')
    ->orHasPermission('article:edit')
    ->check();

// Chained with AND
Mandate::for($user)
    ->hasPermission('article:view')
    ->andHasRole('editor')
    ->check();

// Multiple conditions
Mandate::for($user)
    ->hasAnyRole(['admin', 'editor'])
    ->orHasPermission('article:manage')
    ->check();

// With context (multi-tenancy)
Mandate::for($user)
    ->inContext($team)
    ->hasPermission('project:manage')
    ->check();

// Alternative endings
Mandate::for($user)->hasRole('admin')->allowed(); // Alias for check()
Mandate::for($user)->hasRole('admin')->denied();  // Inverse of check()
```

---

Laravel Gate Integration
------------------------

[](#laravel-gate-integration)

Mandate registers permissions with Laravel's Gate automatically:

```
// In controllers
$this->authorize('article:edit');

// Anywhere
Gate::allows('article:edit');
Gate::denies('article:edit');

// In Blade (works alongside Mandate directives)
@can('article:edit')
    Edit
@endcan
```

---

Query Scopes
------------

[](#query-scopes)

Filter models by role or permission:

```
// Users with specific role
User::role('admin')->get();
User::role(['admin', 'editor'])->get();

// Users without specific role
User::withoutRole('banned')->get();

// Users with specific permission
User::permission('article:edit')->get();

// Users without specific permission
User::withoutPermission('admin:access')->get();
```

---

Artisan Commands
----------------

[](#artisan-commands)

```
# Generate a permission class (code-first)
php artisan mandate:permission ArticlePermissions
php artisan mandate:permission ArticlePermissions --guard=api

# Generate a role class (code-first)
php artisan mandate:role SystemRoles

# Generate a capability class (code-first)
php artisan mandate:capability ContentCapabilities

# Create directly in database (use --db flag)
php artisan mandate:permission article:edit --db
php artisan mandate:role editor --db --permissions=article:edit,article:view
php artisan mandate:capability manage-posts --db --permissions=post:create,post:edit

# Assign a role to a subject (user, team, etc.)
php artisan mandate:assign-role 1 admin
php artisan mandate:assign-role 1 admin --model="App\Models\Team"

# Assign a capability to a role
php artisan mandate:assign-capability editor manage-posts

# Display all roles and permissions
php artisan mandate:show

# Clear permission cache
php artisan mandate:cache-clear

# Migrate from Spatie Laravel Permission
php artisan mandate:upgrade-from-spatie --dry-run              # Preview changes
php artisan mandate:upgrade-from-spatie                        # Run migration
php artisan mandate:upgrade-from-spatie --create-capabilities  # Also create capabilities from prefixes
php artisan mandate:upgrade-from-spatie --convert-permission-sets  # Convert 1.x #[PermissionsSet] to capabilities
```

---

Configuration
-------------

[](#configuration)

Publish the config file for customization:

```
php artisan vendor:publish --tag=mandate-config
```

### Key Options

[](#key-options)

OptionDefaultDescription`model_id_type``'int'`Primary key type: `'int'`, `'uuid'`, or `'ulid'``morph_id_type``null`Morph column ID type (defaults to `model_id_type`)`models.permission``Permission::class`Custom permission model`models.role``Role::class`Custom role model`models.capability``Capability::class`Custom capability model`cache.expiration``86400` (24h)Cache TTL in seconds`wildcards.enabled``false`Enable wildcard permissions`capabilities.enabled``false`Enable capabilities feature`capabilities.direct_assignment``false`Allow direct capability-to-user assignment`context.enabled``false`Enable context model support (multi-tenancy)`context.global_fallback``true`Check global when context check fails`features.enabled``false`Enable feature integration`features.models``[]`Model classes considered Feature contexts`features.on_missing_handler``'deny'`Behavior when handler is not bound`register_gate``true`Register with Laravel Gate`events``false`Fire events on changes`column_names.subject_morph_name``'subject'`Base name for subject morph columns`column_names.context_morph_name``'context'`Base name for context morph columns### UUID / ULID Primary Keys

[](#uuid--ulid-primary-keys)

Mandate supports UUID or ULID primary keys for all its models. Configure before running migrations:

```
// config/mandate.php
'model_id_type' => 'uuid', // or 'ulid', default is 'int'
```

This affects:

- `permissions`, `roles`, and `capabilities` tables (primary keys)
- All pivot tables (foreign keys)

```
// With UUID enabled, IDs are automatically generated
$permission = Permission::create(['name' => 'article:edit']);
$permission->id; // "550e8400-e29b-41d4-a716-446655440000"

$role = Role::create(['name' => 'admin']);
$role->id; // "550e8400-e29b-41d4-a716-446655440001"
```

#### Morph Column ID Type

[](#morph-column-id-type)

Morph columns (`subject_id`, `context_id`) reference external models like User or Team, which may use a different ID type than Mandate's own models. By default, `morph_id_type` uses the same value as `model_id_type`. Set it separately if your external models use a different ID type:

```
// config/mandate.php
'model_id_type' => 'int',   // Mandate's own PKs (permissions, roles, capabilities)
'morph_id_type' => 'uuid',  // Subject/context morphs (User, Team, etc.)
```

This affects:

- Subject morph columns on pivot tables (`permission_subject`, `role_subject`, `capability_subject`)
- Context morph columns on `permissions` and `roles` tables (when context is enabled)
- Context morph columns on pivot tables (when context is enabled)

> **Note:** Set both `model_id_type` and `morph_id_type` before running migrations. Changing them later requires recreating the tables.

### Custom Column Names

[](#custom-column-names)

Customize morph column names by setting the base name. Mandate automatically appends `_id` and `_type` suffixes:

```
// config/mandate.php
'column_names' => [
    'subject_morph_name' => 'subject',  // Creates subject_id, subject_type
    'context_morph_name' => 'context',  // Creates context_id, context_type
],
```

For example, to use `user` instead of `subject`:

```
'column_names' => [
    'subject_morph_name' => 'user',  // Creates user_id, user_type columns
],
```

This affects pivot tables (`permission_subject`, `role_subject`, `capability_subject`) and context columns on permissions/roles tables.

> **Note:** Set column names before running migrations. Changing them later requires recreating the tables.

### Wildcard Permissions

[](#wildcard-permissions)

Enable pattern-based permission matching:

```
// config/mandate.php
'wildcards' => [
    'enabled' => true,
],
```

```
// Grant wildcard permission
$user->grantPermission('article:*');

// Now matches all article permissions
$user->hasPermission('article:view');   // true
$user->hasPermission('article:edit');   // true
$user->hasPermission('article:delete'); // true
```

Wildcard syntax:

- `*` matches all at that level: `article:*` matches `article:view`, `article:edit`
- Multiple parts: `article:view,edit` matches both `article:view` and `article:edit`

---

Capabilities
------------

[](#capabilities)

Capabilities are semantic groupings of permissions that can be assigned to roles or directly to subjects. This is an optional feature that must be explicitly enabled.

### Enabling Capabilities

[](#enabling-capabilities)

First, publish and run the capability migrations:

```
php artisan vendor:publish --tag=mandate-migrations-capabilities
php artisan migrate
```

Then enable in config:

```
// config/mandate.php
'capabilities' => [
    'enabled' => true,
    'direct_assignment' => false, // Allow assigning capabilities directly to users
],
```

### Creating Capabilities

[](#creating-capabilities)

```
use OffloadProject\Mandate\Models\Capability;

// Create a capability with permissions
$capability = Capability::create(['name' => 'manage-posts']);
$capability->grantPermission(['post:create', 'post:edit', 'post:delete', 'post:publish']);

// Or create permissions on the fly
$capability = Capability::create(['name' => 'manage-users']);
$capability->grantPermission(Permission::findOrCreate('user:view'));
$capability->grantPermission(Permission::findOrCreate('user:edit'));
```

### Assigning Capabilities to Roles

[](#assigning-capabilities-to-roles)

```
$role = Role::findByName('editor');

// Assign capabilities
$role->assignCapability('manage-posts');
$role->assignCapability(['manage-posts', 'manage-comments']);

// Remove capabilities
$role->removeCapability('manage-comments');

// Sync capabilities (replace all)
$role->syncCapabilities(['manage-posts']);

// Check capabilities
$role->hasCapability('manage-posts'); // true
```

### Checking Capabilities on Users

[](#checking-capabilities-on-users)

```
// User gets capabilities through their roles
$user->assignRole('editor');

// Check capabilities
$user->hasCapability('manage-posts');
$user->hasAnyCapability(['manage-posts', 'manage-users']);
$user->hasAllCapabilities(['manage-posts', 'manage-comments']);

// Get all capabilities
$user->getAllCapabilities();        // Direct + via roles
$user->getCapabilitiesViaRoles();   // Via roles only

// Get capability info
$user->getCapabilityNames(); // Collection: ['manage-posts', 'manage-users']
$user->getCapabilityIds();   // Collection: [1, 2]
```

### Permission Resolution Through Capabilities

[](#permission-resolution-through-capabilities)

When you check if a user has a permission, Mandate checks all paths:

1. **Direct permission** - assigned directly to the user
2. **Via role** - role has the permission
3. **Via capability (through role)** - role has a capability that has the permission
4. **Via capability (direct)** - user has a capability directly (if `direct_assignment` enabled)

```
// All of these work automatically
$user->hasPermission('post:edit');         // Checks all paths
$user->hasPermissionViaRole('post:edit');  // Checks role + role capabilities
$user->hasPermissionViaCapability('post:edit'); // Checks capabilities only
```

### Direct Capability Assignment

[](#direct-capability-assignment)

Enable direct assignment to allow assigning capabilities directly to user:

```
// config/mandate.php
'capabilities' => [
    'enabled' => true,
    'direct_assignment' => true,
],
```

```
// Assign capabilities directly to users
$user->assignCapability('manage-posts');
$user->removeCapability('manage-posts');
$user->syncCapabilities(['manage-posts', 'manage-comments']);

// Check direct capabilities
$user->hasDirectCapability('manage-posts');
$user->getAllCapabilities(); // Includes both direct and via roles
```

### Blade Directives for Capabilities

[](#blade-directives-for-capabilities)

```
@capability('manage-posts')
    {{-- User has manage-posts capability --}}
@endcapability

@hascapability('manage-posts')
    {{-- Alias for @capability --}}
@endhascapability

@hasanycapability('manage-posts|manage-users')
    {{-- User has any of these capabilities --}}
@endhasanycapability

@hasallcapabilities(['manage-posts', 'manage-users'])
    {{-- User has all of these capabilities --}}
@endhasallcapabilities
```

### Artisan Commands for Capabilities

[](#artisan-commands-for-capabilities)

```
# Generate a capability class (code-first)
php artisan mandate:capability ContentCapabilities

# Create capability directly in database
php artisan mandate:capability manage-posts --db
php artisan mandate:capability manage-posts --db --guard=api
php artisan mandate:capability manage-posts --db --permissions=post:create,post:edit,post:delete

# Assign capability to a role
php artisan mandate:assign-capability editor manage-posts
php artisan mandate:assign-capability editor manage-posts --guard=api
```

---

Context Model (Multi-Tenancy)
-----------------------------

[](#context-model-multi-tenancy)

Context Model enables scoping roles and permissions to a specific model (like Team, Organization, or Project). This allows for resource-specific authorization in multi-tenant applications.

### Enabling Context Support

[](#enabling-context-support)

```
// config/mandate.php
'context' => [
    'enabled' => true,
    'global_fallback' => true, // Check global permissions when context check fails
],
```

Run the context migration after enabling:

```
php artisan migrate
```

### Assigning Roles and Permissions with Context

[](#assigning-roles-and-permissions-with-context)

Pass a context model as the second parameter:

```
// Assign a role within a specific team
$user->assignRole('manager', $team);

// Grant permission within a specific project
$user->grantPermission('task:edit', $project);

// Assign global role (works across all contexts)
$user->assignRole('admin'); // No context = global
```

### Checking Roles and Permissions with Context

[](#checking-roles-and-permissions-with-context)

```
// Check if user has role in specific context
$user->hasRole('manager', $team);         // true
$user->hasRole('manager', $otherTeam);    // false (if not assigned there)

// Check permission with context
$user->hasPermission('task:edit', $project);

// Check multiple roles/permissions with context
$user->hasAnyRole(['manager', 'admin'], $team);
$user->hasAllPermissions(['task:view', 'task:edit'], $project);
```

### Global Fallback

[](#global-fallback)

When `global_fallback` is enabled (default), checking permissions with a context will also check global permissions:

```
// Global permission (no context)
$user->grantPermission('report:view');

// With global fallback enabled, this returns true
$user->hasPermission('report:view', $team);

// Disable global fallback to check only context-specific
// config: 'context.global_fallback' => false
$user->hasPermission('report:view', $team); // false (no context-specific grant)
```

### Getting Permissions and Roles for Context

[](#getting-permissions-and-roles-for-context)

```
// Get roles in a specific context
$user->getRolesForContext($team);         // Returns roles for this team
$user->getRoleNames($team);               // Role names in this team

// Get permissions for context
$user->getAllPermissions($team);          // Direct + via roles for this team
$user->getPermissionNames($team);         // Permission names in this team
```

### Finding Contexts

[](#finding-contexts)

Query which contexts a user has specific roles or permissions in:

```
// Get all teams where user is a manager
$teams = $user->getRoleContexts('manager');

// Get all projects where user can edit tasks
$projects = $user->getPermissionContexts('task:edit');
```

### Using the Mandate Facade with Context

[](#using-the-mandate-facade-with-context)

```
use OffloadProject\Mandate\Facades\Mandate;

// Check with context
Mandate::hasRole($user, 'manager', $team);
Mandate::hasPermission($user, 'task:edit', $project);

// Get data with context
Mandate::getRoles($user, $team);
Mandate::getPermissions($user, $project);
Mandate::getRoleIds($user, $team);
Mandate::getPermissionIds($user, $project);

// Check if context is enabled
Mandate::contextEnabled(); // true/false
```

### Context Configuration Options

[](#context-configuration-options)

OptionDefaultDescription`context.enabled``false`Enable context model support`context.global_fallback``true`Check global when context-specific check fails---

Feature Integration
-------------------

[](#feature-integration)

Feature Integration enables Mandate to delegate feature access checks to an external package (like Flagged) when a Feature model is used as a context. This allows combining feature flags with permission checks.

### How It Works

[](#how-it-works)

When you check a permission or role with a Feature model as the context, Mandate first verifies the subject can access the feature before evaluating permissions. This ensures users only get permissions for features they have access to.

### Enabling Feature Integration

[](#enabling-feature-integration)

Feature integration requires context support to be enabled:

```
// config/mandate.php
'context' => [
    'enabled' => true,
],

'features' => [
    'enabled' => true,
    'models' => [
        App\Models\Feature::class,
    ],
    'on_missing_handler' => 'deny', // 'allow', 'deny', or 'throw'
],
```

### Implementing the Feature Access Handler

[](#implementing-the-feature-access-handler)

Your feature management package must implement the `FeatureAccessHandler` contract:

```
use Illuminate\Database\Eloquent\Model;
use OffloadProject\Mandate\Contracts\FeatureAccessHandler;

class FlaggedFeatureHandler implements FeatureAccessHandler
{
    public function isActive(Model $feature): bool
    {
        // Check if feature is globally active
        return $feature->is_active;
    }

    public function hasAccess(Model $feature, Model $subject): bool
    {
        // Check if subject has been granted access to the feature
        return $feature->subjects()->where('id', $subject->id)->exists();
    }

    public function canAccess(Model $feature, Model $subject): bool
    {
        // Combined check: feature must be active AND subject must have access
        return $this->isActive($feature) && $this->hasAccess($feature, $subject);
    }
}
```

Register the handler in a service provider:

```
use OffloadProject\Mandate\Contracts\FeatureAccessHandler;

$this->app->bind(FeatureAccessHandler::class, FlaggedFeatureHandler::class);
```

### Permission Checks with Feature Context

[](#permission-checks-with-feature-context)

When you pass a Feature model as context, Mandate automatically checks feature access first:

```
$feature = Feature::find(1);

// First checks if user can access the feature via FeatureAccessHandler
// Then checks if user has the permission within that feature context
$user->hasPermission('edit', $feature);

// Same automatic check for roles
$user->hasRole('editor', $feature);
```

If feature access is denied, the permission/role check returns `false` immediately without evaluating the actual permission.

### Bypassing Feature Checks

[](#bypassing-feature-checks)

For admin scenarios where you need to check permissions regardless of feature access:

```
// Pass bypassFeatureCheck: true to skip the feature access check
$user->hasPermission('edit', $feature, bypassFeatureCheck: true);
$user->hasRole('editor', $feature, bypassFeatureCheck: true);
```

### Using the Mandate Facade

[](#using-the-mandate-facade)

```
use OffloadProject\Mandate\Facades\Mandate;

// Check if feature integration is enabled
Mandate::featureIntegrationEnabled();

// Check if a model is a Feature context
Mandate::isFeatureContext($model);

// Get the feature access handler
$handler = Mandate::getFeatureAccessHandler();

// Feature access checks
Mandate::isFeatureActive($feature);
Mandate::hasFeatureAccess($feature, $user);
Mandate::canAccessFeature($feature, $user);
```

### Missing Handler Behavior

[](#missing-handler-behavior)

Configure what happens when no `FeatureAccessHandler` is bound:

ValueBehavior`deny`Return `false` (fail closed) - **Default**`allow`Return `true` (fail open)`throw`Throw `FeatureAccessException````
// config/mandate.php
'features' => [
    'on_missing_handler' => 'deny',
],
```

### Non-Feature Contexts

[](#non-feature-contexts)

When checking permissions with a non-Feature context (like Team or Project), feature integration is bypassed entirely:

```
$team = Team::find(1);

// No feature check - works like normal context
$user->hasPermission('edit', $team);
```

### Feature Configuration Options

[](#feature-configuration-options)

OptionDefaultDescription`features.enabled``false`Enable feature integration`features.models``[]`Model classes considered Feature contexts`features.on_missing_handler``'deny'`Behavior when handler is not bound---

Code-First Definitions
----------------------

[](#code-first-definitions)

Code-first allows you to define permissions, roles, and capabilities in PHP classes using attributes, then sync them to the database. This provides better IDE support, version control, and type safety.

### Enabling Code-First

[](#enabling-code-first)

```
// config/mandate.php
'code_first' => [
    'enabled' => true,
    'paths' => [
        'permissions' => app_path('Permissions'),
        'roles' => app_path('Roles'),
        'capabilities' => app_path('Capabilities'),
    ],
],
```

### Defining Permissions

[](#defining-permissions)

Create a class with string constants for each permission:

```
