Back to Course |
Roles and Permissions in Laravel 11

Managing Users: Staff / Doctors / Patients

The Clinic Owner's role involves managing users on their team and handling the creation of doctor/staff/patient users.

So, let's create two functions—list and create users—similarly to how we did it for the teams.

First, the Policy:

php artisan make:policy UserPolicy

app/Policies/UserPolicy.php

use App\Models\User;
use App\Enums\Permission;
use Illuminate\Auth\Access\HandlesAuthorization;
 
class UserPolicy
{
use HandlesAuthorization;
 
public function viewAny(User $user): bool
{
return $user->hasPermissionTo(Permission::LIST_USER);
}
 
public function create(User $user): bool
{
return $user->hasPermissionTo(Permission::CREATE_USER);
}
}

Now, we can use that ' viewAnyandcreatein the Controller withGate::authorize()`, right?

But first, let's create a Form Request.

php artisan make:request StoreUserRequest

Here are the validation rules:

app/Http/Requests/StoreUserRequest.php

use Illuminate\Validation\Rules\Password;
use Illuminate\Foundation\Http\FormRequest;
 
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', Password::defaults()],
'role_id' => ['required', 'integer', 'exists:roles,id'],
];
}
 
public function authorize(): bool
{
return true;
}
}

Next, the Controller.

php artisan make:controller UserController

Here's the code for the methods.

app/Http/Controllers/UserController.php

use App\Enums\Role;
use App\Models\User;
use Illuminate\View\View;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\StoreUserRequest;
use Illuminate\Database\Eloquent\Builder;
use Spatie\Permission\Models\Role as RoleModel;
 
class UserController extends Controller
{
public function index(): View
{
Gate::authorize('viewAny', User::class);
 
$users = User::with('roles')
->whereHas('roles', function (Builder $query) {
return $query->whereIn('name', [Role::ClinicAdmin->value, Role::Doctor->value, Role::Staff->value]);
})
->whereRelation('teams', 'team_id', '=', auth()->user()->current_team_id)
->get();
 
return view('user.index', compact('users'));
}
 
public function create(): View
{
Gate::authorize('create', User::class);
 
$roles = RoleModel::whereIn('name', [Role::ClinicAdmin->value, Role::Doctor->value, Role::Staff->value])
->pluck('name', 'id');
 
return view('user.create', compact('roles'));
}
 
public function store(StoreUserRequest $request): RedirectResponse
{
Gate::authorize('create', User::class);
 
$user = User::create($request->except('role_id'));
 
$user->assignRole($request->integer('role_id'));
 
return redirect()->route('users.index');
}
}

Looks pretty self-explanatory, right? There are a few conditions in Eloquent queries, but they look similar to the TeamController we had created earlier, using Gate::authorize() for permissions.

Next, the route for this Controller:

routes/web.php

Route::middleware('auth')->group(function () {
// ...
 
Route::resource('teams', TeamController::class)
->only(['index', 'create', 'store']);
 
Route::resource('users', UserController::class)
->only(['index', 'create', 'store']);
});

Finally, the menu item in the top navigation, visible only to those with permissions:

resources/views/layouts/navigation.blade.php

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
@can(\App\Enums\Permission::LIST_TEAM)
<x-nav-link :href="route('teams.index')" :active="request()->routeIs('teams.*')">
{{ __('Clinics') }}
</x-nav-link>
@endcan
@can(\App\Enums\Permission::LIST_USER)
<x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
{{ __('Users') }}
</x-nav-link>
@endcan

And that's it, here's the result, again!

However, even though we have the visual result, let's still write the automated tests for this scenario.

php artisan make:test UserTest

tests/Feature/UserTest.php

use App\Models\User;
use App\Enums\Role as RoleEnum;
use Illuminate\Support\Collection;
use Spatie\Permission\Models\Role as RoleModel;
use function Pest\Laravel\actingAs;
 
it('allows clinic owner and admin to view users list', function () {
$clinicOwner = User::factory()->clinicOwner()->create();
$clinicAdmin = User::factory()->clinicAdmin()->create();
 
$doctor = User::factory()->doctor()->create();
$staff = User::factory()->staff()->create();
 
$patient = User::factory()->patient()->create();
 
actingAs($clinicOwner)
->get(route('users.index'))
->assertOk()
->assertViewHas('users', function (Collection $users) use ($clinicAdmin, $doctor, $staff, $patient): bool {
return $users->contains(fn (User $user) => $user->name === $clinicAdmin->name
|| $user->name === $doctor->name
|| $user->name === $staff->name
) && $users->doesntContain(fn (User $user) => $user->name === $patient->name);
});
 
actingAs($clinicAdmin)
->get(route('users.index'))
->assertOk()
->assertViewHas('users', function (Collection $users) use ($clinicAdmin, $doctor, $staff, $patient): bool {
return $users->contains(fn (User $user) => $user->name === $clinicAdmin->name
|| $user->name === $doctor->name
|| $user->name === $staff->name
) && $users->doesntContain(fn (User $user) => $user->name === $patient->name);
});
});
 
it('forbids users without access to enter users list page', function (User $user) {
actingAs($user)
->get(route('users.index'))
->assertForbidden();
})->with([
fn() => User::factory()->masterAdmin()->create(),
fn() => User::factory()->doctor()->create(),
fn() => User::factory()->staff()->create(),
]);
 
it('forbids users without access to enter create user page', function (User $user) {
actingAs($user)
->get(route('users.create'))
->assertForbidden();
})->with([
fn() => User::factory()->masterAdmin()->create(),
fn() => User::factory()->doctor()->create(),
fn() => User::factory()->staff()->create(),
]);
 
it('allows clinic owner to create a new user and assign a role', function (RoleEnum $role) {
$clinicOwner = User::factory()->clinicOwner()->create();
 
actingAs($clinicOwner)
->post(route('users.store'), [
'name' => 'New User',
'email' => 'new@user.com',
'password' => 'password',
'role_id' => RoleModel::where('name', $role->value)->first()->id,
]);
 
$newUser = User::where('email', 'new@user.com')->first();
 
expect($newUser->hasRole($role))->toBeTrue();
})->with([
RoleEnum::ClinicAdmin,
RoleEnum::Doctor,
RoleEnum::Staff,
]);
 
it('allows clinic admin to create a new user and assign a role', function (RoleEnum $role) {
$clinicAdmin = User::factory()->clinicAdmin()->create();
 
actingAs($clinicAdmin)
->post(route('users.store'), [
'name' => 'New User',
'email' => 'new@user.com',
'password' => 'password',
'role_id' => RoleModel::where('name', $role->value)->first()->id,
]);
 
$newUser = User::where('email', 'new@user.com')->first();
 
expect($newUser->hasRole($role))->toBeTrue();
})->with([
RoleEnum::ClinicAdmin,
RoleEnum::Doctor,
RoleEnum::Staff,
]);

Here's the result: