artesaos/shield
Artesaos Shield gives Laravel apps a lightweight authorization layer for roles and permissions. Define abilities once, assign them to users, and protect routes, controllers, and Blade views with simple, expressive checks.
Start by confirming the package is installable in your Laravel version. artesaos/shield last shipped in 2016, so check its composer.json constraints before adding it to a real application. Use a scratch app or disposable branch first.
Minimal steps:
composer require artesaos/shield
# Publish config and migrations. Confirm the provider class from the package source.
php artisan vendor:publish --provider="Artesaos\Shield\ShieldServiceProvider"
php artisan migrate
Add the package trait to your authenticatable model:
use Artesaos\Shield\Traits\HasRoles; // verify exact namespace in vendor/artesaos/shield
class User extends Authenticatable
{
use HasRoles;
}
Where to look first:
config/shield.php — table names, model bindings, guard behaviour.database/migrations/ — role/permission pivot tables and foreign keys.src/ShieldServiceProvider.php — facade alias, middleware aliases, Blade directives, bindings.First use case: protect an admin route with a role.
// Seeder or tinker
$admin = Shield::createRole('admin'); // verify method name
$admin->givePermission('manage-users'); // verify method name
$user->assignRole('admin'); // verify method name
Register the middleware alias if the package does not do it automatically:
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'role' => \Artesaos\Shield\Middleware\RoleMiddleware::class,
'permission' => \Artesaos\Shield\Middleware\PermissionMiddleware::class,
];
Route::group(['middleware' => ['role:admin']], function () {
Route::get('/admin', AdminController::class . '@index');
});
Treat roles as coarse job functions and permissions as fine-grained abilities. Seed both deterministically so every environment has the same canonical names.
// database/seeders/ShieldSeeder.php
Shield::createRole('admin');
Shield::createRole('editor');
Shield::createPermission('manage-users');
Shield::createPermission('edit-posts');
Shield::createPermission('publish-posts');
Shield::role('admin')->givePermission('manage-users');
Shield::role('editor')->givePermission('edit-posts');
Use the package API rather than direct pivot-table writes so caches and events stay consistent.
Assign roles through the user model:
$user->assignRole('editor');
$user->removeRole('editor');
Check access at the right layer:
// Route-level: middleware
Route::middleware('permission:edit-posts')->group(function () {
Route::put('/posts/{post}', [PostController::class, 'update']);
});
// Controller-level: explicit authorization
public function update(Request $request, Post $post)
{
abort_unless($request->user()->hasPermission('edit-posts'), 403);
// ...
}
// Blade: presentation only
@role('admin')
<a href="/admin">Admin</a>
@endrole
@permission('edit-posts')
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endpermission
If the package registers permissions as gates, you can use Laravel’s native authorization:
// AuthServiceProvider
Gate::define('edit-posts', function ($user) {
return $user->hasPermission('edit-posts');
});
// Controller
$this->authorize('edit-posts');
// Blade
@can('edit-posts')
...
@endcan
For policies, keep the package check inside the policy method so authorization remains centralised:
public function update(User $user, Post $post)
{
return $user->hasPermission('edit-posts')
&& $post->author_id === $user->id;
}
Add a super-admin bypass once, not in every policy:
Gate::before(function ($user) {
return $user->hasRole('super-admin') ? true : null;
});
Use eager loading when listing many users or rendering a permission-heavy page:
$users = User::with('roles.permissions')->paginate();
For route groups, prefer middleware over scattered checks:
Route::middleware(['auth', 'role:admin|super-admin'])->group(function () {
// admin area
});
composer.json and running the migration and middleware paths end to end.spatie/laravel-permission.src/. Do not copy examples blindly.roles, permissions, or pivot tables. Rename tables in config and migrations if needed.down() path. If the package owns tables, you need a safe rollback plan. Foreign-key constraints on pivot tables should be deliberate, especially for role deletion.admin, api), verify that checks use the correct user and guard. A passing $user->hasRole() on the web guard does not automatically authorize an API guard user.$user->hasPermission() inside a Blade loop or API resource can query repeatedly. Eager load roles.permissions and cache per-request results if the package does not.@role and @permission hide UI; they do not protect routes or actions. Always enforce with middleware, authorize(), or a policy.Shield alias or helper function can collide with your own code. Rename the alias in config/app.php if necessary.config/shield.php, run php artisan config:clear. If the package reads config during boot, a cached config can mask changes.$user->roles, the exact permission string, and the guard. Permission names are case- and whitespace-sensitive. Verify middleware aliases with php artisan route:list.RefreshDatabase, seed the roles and permissions your test needs, and assert both the happy path and the 403 path. Test middleware and policy paths separately.Role/Permission models, and extend the package models rather than editing vendor files. Keep the integration surface narrow so replacing the package later is feasible.How can I help you explore Laravel packages today?