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".
Starting Point
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.
Installing Spatie Permissions Package
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.
Modifying Registration to Assign Default Role
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.
Updating Policy
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!
Updating Tests
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
I'm not sure how clear this line is:
from the docs it says:
Yes, you assign roles with permissions to the users, since that is a better workflow. But once that is done - you still check for permission, not the role level. Since roles can have permissions removed.
Imagine this scenario:
You have a role
Assistant
with permission tocreate_users
. You add the check ofassistant
role to your code - everything is okay. But then, you drop thecreate_users
permission from the role.Now you have to go back into your code and modify the condition. If you have checked for the permission - it would've been fine immediately and you would not have to change any code.
Hope that clarifies what we and the docs meant! :)
perfect, thanks for the detailed reply
If you'll get the error that "guard_name" can't be null, please add method "->nullable()" to the certain Spatie-migration. And remember that you need to use Spatie Role model inside of the RoleSeeder: use Spatie\Permission\Models\Role;