genealabs / laravel-governor
Managing policy and control in Laravel.
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- ramsey/uuid: ^4.0
Requires (Dev)
- doctrine/dbal: ^3.0|^4.0
- fakerphp/faker: ^1.23
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
- symfony/thanks: ^1.2
This package is auto-updated.
Last update: 2026-06-20 01:53:38 UTC
README
Manage authorization with granular role-based permissions in your Laravel apps.
Table of Contents
Goal
Provide a simple method of managing ACL in a Laravel application built on the Laravel Authorization functionality. By leveraging Laravel's native Authorization functionality there is no additional learning or implementation curve. All you need to know is Laravel, and you will know how to use Governor for Laravel.
Requirements
| Laravel | PHP | Package |
|---|---|---|
| 10.x | 8.2+ | latest |
| 11.x | 8.2+ | latest |
| 12.x | 8.2+ | latest |
| 13.x | 8.3+ | latest |
Additional Requirements
- Bootstrap 3 (needs to be included in your layout file)
- FontAwesome 4 (needs to be included in your layout file)
Installation
The user with the lowest primary key will be set up as the SuperAdmin. If you're starting on a new project, be sure to add an initial user now. If you already have users, you can update the role-user entry to point to your intended user, if the first user is not the intended SuperAdmin. Now let's get the package installed.
Install via composer:
composer require genealabs/laravel-governor
Implementation
-
First we need to update the database by running the migrations and data seeders:
php artisan migrate --path="vendor/genealabs/laravel-governor/database/migrations" php artisan db:seed --class=LaravelGovernorDatabaseSeeder -
If you have seeders of your own, run them now:
php artisan db:seed
-
Next, assign permissions (this requires you have users already populated):
php artisan db:seed --class=LaravelGovernorPermissionsTableSeeder
-
Now we need to make the assets available:
php artisan governor:publish --assets
-
Lastly, add the
Governingtrait to the User model of your app:use GeneaLabs\LaravelGovernor\Traits\Governing; class User extends Authenticatable { use Governing; }
For non-User models that should be governed (have ownership tracking and permission-scoped queries), use the
Governabletrait instead:use GeneaLabs\LaravelGovernor\Traits\Governable; class BlogPost extends Model { use Governable; }
Upgrading
The following upgrade guides should help navigate updates with breaking changes.
From 0.11.5+ to 0.12 [Breaking]
The role_user pivot table has replaced the composite key with a primary key, as Laravel does not fully support composite keys. Run:
php artisan db:seed --class="LaravelGovernorUpgradeTo0120"
From 0.11 to 0.11.5 [Breaking]
The primary keys of the package's tables have been renamed. (This should have been a minor version change, instead of a patch, as this was a breaking change.) Run:
php artisan db:seed --class="LaravelGovernorUpgradeTo0115"
From 0.10 to 0.11 [Breaking]
The following traits have changed:
Governablehas been renamed toGoverning.Governedhas been renamed toGovernable.- the
governor_created_byhas been renamed togovernor_owned_by. Run migrations to update your tables.php artisan db:seed --class="LaravelGovernorUpgradeTo0110" - replace any reference in your app from
governor_created_bytogovernor_owned_by.
From 0.6 to Version 0.10 [Breaking]
To upgrade from version previous to 0.10.0, first run the migrations and
seeders, then run the update seeder:
php artisan migrate --path="vendor/genealabs/laravel-governor/database/migrations" php artisan db:seed --class="LaravelGovernorDatabaseSeeder" php artisan db:seed --class="LaravelGovernorUpgradeTo0100"
to 0.6 [Breaking]
- If you were extending
GeneaLabs\LaravelGovernor\Policies\LaravelGovernorPolicy, change to extendGeneaLabs\LaravelGovernor\Policies\BasePolicy; - Support for version of Laravel lower than 5.5 has been dropped.
Configuration
Publish the configuration file if you need to customize defaults:
php artisan governor:publish --config
Config Options
| Key | Type | Default | Description |
|---|---|---|---|
layout-view |
string |
'layouts.app' |
Blade layout used to render Governor's built-in admin views. Must include Bootstrap 3 and FontAwesome 4. |
content-section |
string |
'content' |
Name of the @yield section in your layout where Governor renders page content. |
auth-model-primary-key-type |
string |
'bigInteger' |
Primary key type of your User model. Used when Governor adds the governor_owned_by column. Accepts 'integer' or 'bigInteger'. |
models.auth |
string |
config('auth.providers.users.model') |
Fully-qualified class name of your User model. |
models.action |
string |
Action::class |
Model class for actions. Override to extend the default. |
models.assignment |
string |
Assignment::class |
Model class for role assignments. |
models.entity |
string |
Entity::class |
Model class for entities. |
models.group |
string |
Group::class |
Model class for entity groups. |
models.ownership |
string |
Ownership::class |
Model class for ownership levels. |
models.permission |
string |
Permission::class |
Model class for permissions. |
models.role |
string |
Role::class |
Model class for roles. |
models.team |
string |
Team::class |
Model class for teams. |
models.invitation |
string |
TeamInvitation::class |
Model class for team invitations. |
user-name-property |
string |
'name' |
Property on the User model used for display in the admin UI. |
url-prefix |
string |
'/genealabs/laravel-governor/' |
URL prefix for all Governor web routes. |
superadmins |
string|null |
env('GOVERNOR_SUPERADMINS') |
JSON array of users to auto-create as SuperAdmins during seeding. Format: [{"name":"...","email":"...","password":"..."}] |
admins |
string|null |
env('GOVERNOR_ADMINS') |
JSON array of users to auto-create as Admins during seeding. Same format as superadmins. |
entity-aliases |
array |
[] |
Map of raw entity names to display names in the UI. E.g. ['User' => 'Team Member']. |
cache.enabled |
bool |
false |
Enable cross-request caching of lookup tables (roles, actions, entities, permissions). |
cache.ttl |
int|null |
3600 |
Cache TTL in seconds. Set to null for "forever" (until manually invalidated). |
Caching
Governor can cache lookup table queries across requests to reduce database load. This is disabled by default.
To enable caching, publish the config file and update the cache section:
'cache' => [ 'enabled' => true, 'ttl' => 3600, // seconds, or null for "forever" ],
Cache is automatically invalidated when any lookup table model (Action, Entity, Ownership, Permission, Role) is created, updated, or deleted. Invalidation is coarse-grained: a change to any single lookup table flushes the cache for all lookup tables. This keeps the invalidation logic simple and reliable, which is appropriate given that lookup tables change infrequently.
Views
If you would like to customize the views, publish them:
php artisan governor:publish --views
and edit them in resources/views/vendor/genealabs-laravel-governor.
Tables
Tables will automatically be updated with a governor_owned_by column that
references the user that created the entry. There is no more need to run
separate migrations or work around packages that have models without a
created_by property.
Admin Views
The easiest way to integrate Governor into your app is to add menu items to the relevant section of your app's menu (restrict access using Laravel Authorization methods). The following named routes are available:
- Role Management:
genealabs.laravel-governor.roles.index - User-Role Assignments:
genealabs.laravel-governor.assignments.index - Teams:
genealabs.laravel-governor.teams.index - Groups:
genealabs.laravel-governor.groups.index
For example:
<li><a href="{{ route('genealabs.laravel-governor.roles.index') }}">Governor</a></li>
403 Unauthorized
We recommend making a custom 403 error page to let the user know they don't have access. Otherwise the user will just see the default error message. See https://laravel.com/docs/errors#custom-http-error-pages for more details on how to set those up.
API Reference
Traits
Governor provides two traits for your Eloquent models. Use Governing on your
User model (it includes Governable automatically). Use Governable on any
other model that should be governed.
Governing Trait
Namespace: GeneaLabs\LaravelGovernor\Traits\Governing
Use on: Your User model
This trait adds role management, team membership, and permission resolution to
the User model. It includes the Governable trait, so you do not need to add
both.
hasRole(string $name): bool
Check whether the user has a specific role.
- Parameters:
$name— the role name (e.g.'SuperAdmin','Member') - Returns:
trueif the user has the given role or is a SuperAdmin
if ($user->hasRole('Editor')) { // user has the Editor role }
roles(): BelongsToMany
Eloquent relationship to the user's assigned roles.
- Returns:
BelongsToManyrelationship toRoleviagovernor_role_user
$roleNames = $user->roles->pluck('name'); // ['SuperAdmin', 'Member']
ownedTeams(): HasMany
Eloquent relationship to teams owned by this user.
- Returns:
HasManyrelationship toTeamwheregovernor_owned_bymatches the user
$myTeams = $user->ownedTeams;
teams(): BelongsToMany
Eloquent relationship to teams the user is a member of.
- Returns:
BelongsToManyrelationship toTeamviagovernor_team_user
$teamNames = $user->teams->pluck('name');
permissions (Accessor)
All permissions associated with the user's roles.
- Returns:
CollectionofPermissionmodels
$permissions = $user->permissions;
effectivePermissions (Accessor)
Deduplicated permissions across all roles, collapsed to the highest ownership
level per entity/action pair. If any role grants "any" ownership, the
effective permission is "any"; if the best is "own", it returns "own".
- Returns:
CollectionofPermissionmodels withrole_nameandteam_nameset tonull
$effective = $user->effectivePermissions;
Governable Trait
Namespace: GeneaLabs\LaravelGovernor\Traits\Governable
Use on: Any Eloquent model that should have ownership tracking and
permission-scoped queries
ownedBy(): BelongsTo
Eloquent relationship to the user who owns this model.
- Returns:
BelongsTorelationship to the auth model viagovernor_owned_by
$owner = $blogPost->ownedBy;
teams(): MorphToMany
Eloquent polymorphic relationship to teams associated with this model.
- Returns:
MorphToManyrelationship toTeamviagovernor_teamables
$teams = $blogPost->teams;
Query Scopes
All scopes filter query results based on the authenticated user's permissions.
If the user has "any" ownership for the relevant action, the query is
unmodified. If the user has "own" ownership, results are limited to records
they own or belong to their teams. If neither, the query returns no results.
| Scope | Filters by action | Usage |
|---|---|---|
scopeDeletable |
delete |
BlogPost::deletable()->get() |
scopeForceDeletable |
forceDelete |
BlogPost::forceDeletable()->get() |
scopeRestorable |
restore |
BlogPost::restorable()->get() |
scopeUpdatable |
update |
BlogPost::updatable()->get() |
scopeViewable |
view |
BlogPost::viewable()->get() |
scopeViewAnyable |
viewAny |
BlogPost::viewAnyable()->get() |
These scopes can be applied directly to any query builder instance, making them useful in Nova or other admin panels:
Nova example:
use Laravel\Nova\Resource as NovaResource; use Laravel\Nova\Http\Requests\NovaRequest; abstract class Resource extends NovaResource { public static function indexQuery(NovaRequest $request, $query) { return $query->viewAnyable(); } }
Policies
BasePolicy
Namespace: GeneaLabs\LaravelGovernor\Policies\BasePolicy
All Governor policies must extend this class. It provides automatic permission
checking for standard Laravel policy methods. Policies are auto-detected from
your app/Policies directory (and any paths configured via policy_paths in
config) — you do not need to register them manually.
Default Policy Methods
These methods are implemented automatically. Override them only if you need custom behavior:
| Method | Signature | Description |
|---|---|---|
create |
create(?Model $user): bool |
Can the user create a new instance? Checks create action permission. |
update |
update(?Model $user, Model $model): bool |
Can the user update this model? Checks update action with ownership. |
viewAny |
viewAny(?Model $user): bool |
Can the user list all instances? Checks viewAny action permission. |
view |
view(?Model $user, Model $model): bool |
Can the user view this model? Checks view action with ownership. |
delete |
delete(?Model $user, Model $model): bool |
Can the user delete this model? Checks delete action with ownership. |
restore |
restore(?Model $user, Model $model): bool |
Can the user restore this model? Checks restore action with ownership. |
forceDelete |
forceDelete(?Model $user, Model $model): bool |
Can the user force-delete this model? Checks forceDelete action with ownership. |
SuperAdmins bypass all permission checks and always return true.
Guest users (unauthenticated) are assigned the Guest role if it exists.
Custom Policy Actions
You can define custom actions beyond the standard CRUD operations by adding
public methods to your policy class. Governor automatically detects any public
methods that are not inherited from BasePolicy and registers them as custom
actions in the permissions system.
class BlogPostPolicy extends BasePolicy { public function publish(?Model $user, Model $model): bool { return $this->authorizeCustomAction($user, $model); } public function archive(?Model $user, Model $model): bool { return $this->authorizeCustomAction($user, $model); } }
Custom actions are registered with names in the format
App\Models\BlogPost:publish and appear in the role permission editor.
To check custom actions in your application:
$user->can('publish', $blogPost);
Creating a Policy
Create a policy class that extends BasePolicy. That's it — no methods are
required for the default CRUD actions to work:
<?php namespace App\Policies; use GeneaLabs\LaravelGovernor\Policies\BasePolicy; class BlogPostPolicy extends BasePolicy { // All standard CRUD permissions are handled automatically. // Add custom methods only for non-standard actions. }
Checking authorization uses standard Laravel authorization:
// In controllers $this->authorize('update', $blogPost); // In Blade templates @can('delete', $blogPost) <button>Delete</button> @endcan // Directly on the user $user->can('create', App\Models\BlogPost::class); $user->can('update', $blogPost);
Models
Governor ships with the following Eloquent models. All can be swapped via the
models config key.
Role
Table: governor_roles
Primary Key: name (string, non-incrementing)
| Attribute | Type | Description |
|---|---|---|
name |
string |
Unique role name (e.g. SuperAdmin, Member, Editor) |
description |
string |
Human-readable description (min 25 chars) |
Relationships:
permissions(): HasMany— permissions assigned to this roleusers(): BelongsToMany— users assigned to this role
Team
Table: governor_teams
| Attribute | Type | Description |
|---|---|---|
name |
string |
Team name |
description |
string|null |
Optional description |
governor_owned_by |
int|null |
ID of the user who owns the team |
Relationships:
members(): BelongsToMany— team members (custom relation that prevents detaching the owner)invitations(): HasMany— pending team invitationspermissions(): HasMany— team-level permissionsownedBy(): BelongsTo— the user who owns the teamteams(): MorphToMany— parent teams (polymorphic)
Permission
Table: governor_permissions
| Attribute | Type | Description |
|---|---|---|
role_name |
string|null |
Role this permission belongs to |
entity_name |
string |
Entity (model) this permission applies to |
action_name |
string |
Action name (create, update, view, delete, etc.) |
ownership_name |
string |
Ownership level: no, own, or any |
team_id |
int|null |
Team this permission belongs to (for team-level permissions) |
Relationships:
role(): BelongsTo— the associated roleteam(): BelongsTo— the associated team (if team-level)
Entity
Table: governor_entities
Primary Key: name (string, non-incrementing)
Entities represent governed model types. They are auto-created when policies are detected.
| Attribute | Type | Description |
|---|---|---|
name |
string |
Entity name (derived from policy class name) |
policy_class |
string|null |
Fully-qualified policy class name |
Relationships:
group(): BelongsTo— optional entity grouppermissions(): HasMany— permissions for this entity
Methods:
displayName(): string— returns the entity alias (from config) or the raw name
Action
Table: governor_actions
Primary Key: name (string, non-incrementing)
Standard actions (create, update, view, viewAny, delete, restore,
forceDelete) are seeded automatically. Custom actions are registered when
policies define additional public methods.
Ownership
Table: governor_ownerships
Primary Key: name (string, non-incrementing)
Ownership levels control the scope of a permission: no (denied), own (only
records owned by the user or their team), any (all records).
Group
Table: governor_groups
Primary Key: name (string, non-incrementing)
Groups organize entities in the permission editor UI.
Artisan Commands
governor:publish
Publish package assets, config, views, or migrations.
php artisan governor:publish --assets # Publish frontend assets php artisan governor:publish --config # Publish config file php artisan governor:publish --views # Publish Blade views php artisan governor:publish --migrations # Publish migration files
Flags can be combined:
php artisan governor:publish --config --views
governor:setup
Assign the SuperAdmin role to an existing user. Requires either --superadmin
(email) or --user (ID).
php artisan governor:setup --superadmin=admin@example.com php artisan governor:setup --user=1
The command will fail if:
- Neither option is provided
- Both options are provided
- The user does not exist
- The
SuperAdminrole has not been seeded yet
REST API Endpoints
Governor provides two JSON API endpoints, prefixed with
api + your configured url-prefix. These require the auth:api middleware
(e.g. Laravel Passport or Sanctum).
Check User Ability
Route: GET /api/{url-prefix}/user-can/{ability}
Route Name: genealabs.laravel-governor.api.user-can.show
Check if the authenticated user can perform a given action on a model.
| Parameter | Location | Required | Description |
|---|---|---|---|
ability |
URL | Yes | Action to check: create, update, view, viewAny, delete, restore, forceDelete, or a custom action |
model |
Query | Yes | Fully-qualified model class name |
primary-key |
Query | For update, view, delete, restore, forceDelete |
Primary key of the specific model instance |
// Can the user create a Role? GET /api/genealabs/laravel-governor/user-can/create?model=GeneaLabs\LaravelGovernor\Role // Can the user update Role with primary key 1? GET /api/genealabs/laravel-governor/user-can/update?model=GeneaLabs\LaravelGovernor\Role&primary-key=1
Response: 200 OK with {"can": true} or {"can": false}
Check User Role
Route: GET /api/{url-prefix}/user-is/{role}
Route Name: genealabs.laravel-governor.api.user-is.show
Check if the authenticated user has a specific role.
| Parameter | Location | Required | Description |
|---|---|---|---|
role |
URL | Yes | Role name to check (e.g. SuperAdmin, Member) |
GET /api/genealabs/laravel-governor/user-is/SuperAdmin
Response: 200 OK with {"is": true} or {"is": false}
Web Routes
Governor registers the following resourceful web routes under the configured
url-prefix, all using the web middleware group:
| Route Name | Method | URI | Controller |
|---|---|---|---|
genealabs.laravel-governor.roles.index |
GET | {prefix}/roles |
RolesController@index |
genealabs.laravel-governor.roles.create |
GET | {prefix}/roles/create |
RolesController@create |
genealabs.laravel-governor.roles.store |
POST | {prefix}/roles |
RolesController@store |
genealabs.laravel-governor.roles.show |
GET | {prefix}/roles/{role} |
RolesController@show |
genealabs.laravel-governor.roles.edit |
GET | {prefix}/roles/{role}/edit |
RolesController@edit |
genealabs.laravel-governor.roles.update |
PUT/PATCH | {prefix}/roles/{role} |
RolesController@update |
genealabs.laravel-governor.roles.destroy |
DELETE | {prefix}/roles/{role} |
RolesController@destroy |
genealabs.laravel-governor.groups.* |
CRUD | {prefix}/groups |
GroupsController |
genealabs.laravel-governor.teams.* |
CRUD | {prefix}/teams |
TeamsController |
genealabs.laravel-governor.teams.transfer-ownership |
POST | {prefix}/teams/{team}/transfer-ownership |
TeamsController@transferOwnership |
genealabs.laravel-governor.assignments.* |
CRUD | {prefix}/assignments |
AssignmentsController |
genealabs.laravel-governor.invitations.* |
CRUD | {prefix}/invitations |
InvitationController |
Events
Governor hooks into Eloquent model events to automate ownership tracking and cache invalidation.
Ownership Tracking Events
| Event | Listener | Description |
|---|---|---|
eloquent.creating: * |
CreatingListener |
Sets governor_owned_by to the authenticated user's ID on any model with the Governable or Governing trait, if the column exists and isn't already set. |
eloquent.created: * |
CreatedListener |
After model creation, creates the governor_owned_by column on the model's table if it doesn't exist yet (auto-migration). |
eloquent.saving: * |
CreatingListener |
Same as creating — ensures ownership is set on save as well. |
Team Events
| Event | Listener | Description |
|---|---|---|
eloquent.creating: {TeamInvitation} |
CreatingInvitationListener |
Validates and prepares team invitation data before creation. |
eloquent.created: {TeamInvitation} |
CreatedInvitationListener |
Sends invitation notification to the invited user after creation. |
eloquent.created: {Team} |
CreatedTeamListener |
Automatically adds the team creator as a member after team creation. |
Cache Invalidation
| Event | Observer | Description |
|---|---|---|
created, updated, deleted on lookup models |
LookupTableObserver |
Flushes all Governor cache keys when any Action, Entity, Ownership, Permission, or Role is modified. Also refreshes the in-memory singletons (governor-actions, governor-entities, governor-permissions, governor-roles). |
Blade Components
<x-governor-menu-bar />
Renders a navigation bar component for Governor's admin interface. Include it in your layout to provide navigation between roles, assignments, teams, and groups.
<x-governor-menu-bar />
Examples
Config File
<?php return [ 'layout-view' => 'layouts.app', 'content-section' => 'content', 'auth-model-primary-key-type' => 'bigInteger', 'models' => [ 'auth' => config('auth.providers.users.model') ?? config('auth.model'), 'action' => GeneaLabs\LaravelGovernor\Action::class, 'assignment' => GeneaLabs\LaravelGovernor\Assignment::class, 'entity' => GeneaLabs\LaravelGovernor\Entity::class, 'group' => GeneaLabs\LaravelGovernor\Group::class, 'ownership' => GeneaLabs\LaravelGovernor\Ownership::class, 'permission' => GeneaLabs\LaravelGovernor\Permission::class, 'role' => GeneaLabs\LaravelGovernor\Role::class, 'team' => GeneaLabs\LaravelGovernor\Team::class, 'invitation' => GeneaLabs\LaravelGovernor\TeamInvitation::class, ], 'user-name-property' => 'name', 'url-prefix' => '/genealabs/laravel-governor/', 'superadmins' => env('GOVERNOR_SUPERADMINS'), 'admins' => env('GOVERNOR_ADMINS'), 'entity-aliases' => [], 'cache' => [ 'enabled' => false, 'ttl' => 3600, ], ];
Complete Policy with Custom Actions
<?php namespace App\Policies; use GeneaLabs\LaravelGovernor\Policies\BasePolicy; use Illuminate\Database\Eloquent\Model; class BlogPostPolicy extends BasePolicy { // Standard CRUD actions are inherited automatically. // Custom action: publish public function publish(?Model $user, Model $model): bool { return $this->authorizeCustomAction($user, $model); } // Custom action: archive public function archive(?Model $user, Model $model): bool { return $this->authorizeCustomAction($user, $model); } }
Using Permission Scopes
// Get only blog posts the user can view $posts = BlogPost::viewable()->paginate(15); // Get only blog posts the user can edit $editable = BlogPost::updatable()->get(); // Get only blog posts the user can delete $deletable = BlogPost::deletable()->get();
Checking Roles and Permissions
// Check if user has a specific role if ($user->hasRole('Editor')) { // ... } // Check policy authorization (standard Laravel) if ($user->can('update', $blogPost)) { // ... } // Check custom action if ($user->can('publish', $blogPost)) { // ... } // Get all effective permissions for a user $permissions = $user->effectivePermissions;
Team Management
// Get user's teams $teams = $user->teams; // Get teams owned by the user $ownedTeams = $user->ownedTeams; // Get team members $members = $team->members; // Transfer team ownership (via web route) POST /genealabs/laravel-governor/teams/{team}/transfer-ownership
