One of the most typical questions I received when preparing for this course was, "Will you use the Spatie Permissions package?"
And I thought, "Why not BOTH". So, first, I've shown you how to use gates/policies without any packages, and now it's time to show the "other side".
We will work on the same project but take its "earlier" point. Imagine we have Gates without Policies.
app/Http/Controllers/TaskController.php
class TaskController extends Controller{ public function index(): View { $tasks = Task::with('user')->get(); return view('tasks.index', compact('tasks')); } public function create(): View { Gate::authorize('create', Task::class); return view('tasks.create'); } public function store(Request $request): RedirectResponse { Gate::authorize('create', Task::class); Task::create($request->only('name', 'due_date') + ['user_id' => auth()->id()]); return redirect()->route('tasks.index'); } public function edit(Task $task): View { Gate::authorize('update', $task); return view('tasks.edit', compact('task')); } public function update(Request $request, Task $task): RedirectResponse { Gate::authorize('update', $task); $task->update($request->only('name', 'due_date')); return redirect()->route('tasks.index'); } public function destroy(Task $task): RedirectResponse { Gate::authorize('delete', $task); $task->delete(); return redirect()->route('tasks.index'); }}
And then we have @can
checks in the Blade file.
resources/views/tasks/index.blade.php
<div class="min-w-full align-middle"> @can('create', \App\Models\Task::class) <a href="{{ route('tasks.create') }}" class="underline">Add new task</a> <br /><br /> @endcan <table class="min-w-full border divide-y divide-gray-200"> <thead> ... </thead> <tbody class="bg-white divide-y divide-gray-200 divide-solid"> @foreach($tasks as $task) <tr class="bg-white"> ... <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> @can('update', $task) <a href="{{ route('tasks.edit', $task) }}" class="underline">Edit</a> @endcan @can('delete', $task) | <form action="{{ route('tasks.destroy', $task) }}" method="POST" class="inline-block" onsubmit="return confirm('Are you sure?')"> @method('DELETE') @csrf <button type="submit" class="text-red-600 underline">Delete</button> </form> @endcan </td> </tr> @endforeach </tbody> </table></div>
Now, let's install the package and configure its roles.
We'll do everything according to basic official documentation.
composer require spatie/laravel-permissionphp artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
Then, we need to add a trait to our User Model and remove our own roles implementation:
app/Models/User.php
use Spatie\Permission\Traits\HasRoles; // ... class User extends Authenticatable{ use HasFactory, Notifiable; use HasRoles; // ... protected $fillable = [ 'name', 'email', 'password', 'role_id', ]; // ... public function role(): BelongsTo { return $this->belongsTo(Role::class); }}
We also removed the role_id
and the relationship we had in the beginning. It will all be saved in the package's DB tables.
We skipped one thing in the previous lessons. Laravel Breeze comes with a registration form, and we need to assign the role after the form is submitted.
app/Http/Controllers/Auth/RegisteredUserController.php
// ... public function store(Request $request): RedirectResponse{ // ... event(new Registered($user)); $user->assignRole('User'); Auth::login($user); return redirect(route('dashboard', absolute: false));} // ...
This allows our self-registered users to instantly get access to the User
role and permissions.
Note: This uses the same role Seeder as previous lessons, so make sure you have it.
Finally, we get the main thing about roles/permissions: changing the Policy with the logic of the Spatie package, which offers functions like hasRole()
and hasAnyRole()
.
app/Policies/TaskPolicy.php
class TaskPolicy{ public function create(User $user): bool { return $user->role_id == Role::ROLE_ADMIN; return $user->hasRole('Administrator'); } public function update(User $user, Task $task): bool { return in_array($user->role_id, [Role::ROLE_ADMIN, Role::ROLE_MANAGER]) return $user->hasAnyRole(['Administrator', 'Manager']) || $task->user_id === $user->id; } public function delete(User $user, Task $task): bool { return $user->role_id == Role::ROLE_ADMIN; return $user->hasRole('Administrator'); }}
Note: This is a simplified example. Spatie recommends you check Permissions
instead of Roles
in these scenarios!
Similar changes are made in the tests: instead of assigning role_id
, we need to call the package function assignRole()
.
tests/Feature/TaskTest.php
it('allows administrator to access create task page', function () { $user = User::factory() ->create(['role_id' => Role::ROLE_ADMIN]); ->create() ->assignRole('Administrator'); actingAs($user) ->get(route('tasks.create')) ->assertOk();}); it('does not allow other users to access create task page', function (User $user) { actingAs($user) ->get(route('tasks.create')) ->assertForbidden();})->with([ fn() => User::factory()->create(['role_id' => Role::ROLE_USER]), fn() => User::factory()->create(['role_id' => Role::ROLE_MANAGER]),]);})->with([ fn () => User::factory()->create()->assignRole('User'), fn () => User::factory()->create()->assignRole('Manager'),]); it('allows administrator and manager to enter update page for any task', function (User $user) { $task = Task::factory()->create(['user_id' => User::factory()->create()->id]); actingAs($user) ->get(route('tasks.edit', $task)) ->assertOk();})->with([ fn() => User::factory()->create(['role_id' => Role::ROLE_ADMIN]), fn() => User::factory()->create(['role_id' => Role::ROLE_MANAGER]),]);})->with([ fn () => User::factory()->create()->assignRole('Administrator'), fn () => User::factory()->create()->assignRole('Manager'),]); it('allows administrator and manager to update any task', function (User $user) { $task = Task::factory()->create(['user_id' => User::factory()->create()->id]); actingAs($user) ->put(route('tasks.update', $task), [ 'name' => 'updated task name', ]) ->assertRedirect(); expect($task->refresh()->name)->toBe('updated task name');})->with([ fn() => User::factory()->create(['role_id' => Role::ROLE_ADMIN]), fn() => User::factory()->create(['role_id' => Role::ROLE_MANAGER]),]);})->with([ fn () => User::factory()->create()->assignRole('Administrator'), fn () => User::factory()->create()->assignRole('Manager'),]); it('allows user to update his own task', function () { $user = User::factory()->create(); $task = Task::factory()->create(['user_id' => $user->id]); actingAs($user) ->put(route('tasks.update', $task), [ 'name' => 'updated task name', ]); expect($task->refresh()->name)->toBe('updated task name');}); it('does no allow user to update other users task', function () { $user = User::factory()->create(); $task = Task::factory()->create(['user_id' => User::factory()->create()->id]); actingAs($user) ->put(route('tasks.update', $task), [ 'name' => 'updated task name', ]) ->assertForbidden();}); it('allows administrator to delete task', function () { $task = Task::factory()->create(['user_id' => User::factory()->create()->id]); $user = User::factory() ->create(['role_id' => Role::ROLE_ADMIN]); ->create() ->assignRole('Administrator'); actingAs($user) ->delete(route('tasks.destroy', $task)) ->assertRedirect(); expect(Task::count())->toBe(0);}); it('does not allow other users to delete tasks', function (User $user) { $task = Task::factory()->create(['user_id' => User::factory()->create()->id]); actingAs($user) ->delete(route('tasks.destroy', $task)) ->assertForbidden();})->with([ fn() => User::factory()->create(['role_id' => Role::ROLE_USER]), fn() => User::factory()->create(['role_id' => Role::ROLE_MANAGER]),]);})->with([ fn () => User::factory()->create()->assignRole('User'), fn () => User::factory()->create()->assignRole('Manager'),]);
Once these updates are done, we can run our tests and see that they all pass:
And that's all you need to know about how the Spatie Permission package works in a simple case.
Of course, we will get much deeper in this course, but the section about basic users/roles/permissions is completed for now.
Next, we move to more complex examples of team or company permissions.
Complete code in repository