Skip to content

Spatie Permissions ​

Add admin, manager, and user roles. New registrations get user; seed roles before opening registration.

Installation ​

bash
composer require spatie/laravel-permission
bash
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
bash
php artisan config:clear
php artisan migrate
bash
php artisan boost:update

User Model ​

Add HasRoles to the model from step 03. Keep the split-name methods and auth traits.

File: app/Models/User.php

php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Spatie\Permission\Traits\HasRoles;

/**
 * @property int $id
 * @property string $first_name
 * @property string $last_name
 * @property string $email
 * @property Carbon|null $email_verified_at
 * @property string $password
 * @property string|null $two_factor_secret
 * @property string|null $two_factor_recovery_codes
 * @property Carbon|null $two_factor_confirmed_at
 * @property string|null $remember_token
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 */
#[Fillable(['first_name', 'last_name', 'email', 'password'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable implements PasskeyUser
{
    /** @use HasFactory<UserFactory> */
    use HasFactory, HasRoles, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'two_factor_confirmed_at' => 'datetime',
        ];
    }

    /**
     * Get the user's full name.
     */
    public function fullName(): string
    {
        return trim($this->first_name.' '.$this->last_name);
    }

    /**
     * Use the full name in passkey prompts.
     */
    public function getPasskeyDisplayName(): string
    {
        return $this->fullName();
    }

    /**
     * Get the user's initials.
     */
    public function initials(): string
    {
        return Str::upper(Str::substr($this->first_name, 0, 1).Str::substr($this->last_name, 0, 1));
    }
}

Registration ​

File: app/Actions/Fortify/CreateNewUser.php

php
<?php

namespace App\Actions\Fortify;

use App\Concerns\PasswordValidationRules;
use App\Concerns\ProfileValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    use PasswordValidationRules, ProfileValidationRules;

    /**
     * Validate and create a newly registered user.
     *
     * @param  array<string, string>  $input
     */
    public function create(array $input): User
    {
        Validator::make($input, [
            ...$this->profileRules(),
            'password' => $this->passwordRules(),
        ])->validate();

        $user = User::create([
            'first_name' => $input['first_name'],
            'last_name' => $input['last_name'],
            'email' => $input['email'],
            'password' => $input['password'],
        ]);

        $user->assignRole('user');

        return $user;
    }
}

Seeders ​

Local demo accounts only

admin@example.com and user@example.com share local-only-change-me. Keep the local environment guard; never deploy these accounts.

bash
php artisan make:seeder RoleSeeder
php artisan make:seeder UserSeeder

File: database/seeders/RoleSeeder.php

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $adminAccess = Permission::firstOrCreate(['name' => 'admin access']);
        $managerAccess = Permission::firstOrCreate(['name' => 'manager access']);
        $userAccess = Permission::firstOrCreate(['name' => 'user access']);

        Role::firstOrCreate(['name' => 'admin'])->syncPermissions([$adminAccess, $managerAccess, $userAccess]);
        Role::firstOrCreate(['name' => 'manager'])->syncPermissions([$managerAccess, $userAccess]);
        Role::firstOrCreate(['name' => 'user'])->syncPermissions([$userAccess]);
    }
}

File: database/seeders/UserSeeder.php

php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        if (! app()->environment('local')) {
            return;
        }

        $users = [
            ['first_name' => 'Local', 'last_name' => 'Admin', 'email' => 'admin@example.com', 'role' => 'admin'],
            ['first_name' => 'Regular', 'last_name' => 'User', 'email' => 'user@example.com', 'role' => 'user'],
        ];

        foreach ($users as $userData) {
            $role = $userData['role'];
            unset($userData['role']);

            User::firstOrCreate(['email' => $userData['email']], [
                ...$userData,
                'email_verified_at' => now(),
                'password' => Hash::make('local-only-change-me'),
                'remember_token' => Str::random(10),
            ])->assignRole($role);
        }
    }
}

File: database/seeders/DatabaseSeeder.php

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    use WithoutModelEvents;

    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            RoleSeeder::class,
            UserSeeder::class,
        ]);
    }
}

Seed the Database ​

assignRole('user') fails if the role is missing, after the user has already been inserted. Seed first in every environment.

UserSeeder only runs in local. Reruns preserve existing user fields but still assign the listed roles. RoleSeeder runs everywhere and replaces each role's permissions with the listed set; keep that list current.

bash
php artisan db:seed

Registration Tests ​

Seed RoleSeeder in tests that create users through registration; a fresh test database has no roles. Keep the feature guards and split-name payload.

File: tests/Feature/Auth/RegistrationTest.php

php
<?php

use Database\Seeders\RoleSeeder;
use Laravel\Fortify\Features;

beforeEach(function () {
    $this->skipUnlessFortifyHas(Features::registration());
});

test('registration screen can be rendered', function () {
    $response = $this->get(route('register'));

    $response->assertOk();
});

test('new users can register', function () {
    $this->seed(RoleSeeder::class);

    $response = $this->post(route('register.store'), [
        'first_name' => 'Test',
        'last_name' => 'User',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertSessionHasNoErrors();
    $response->assertRedirect(route('dashboard', absolute: false));
    $this->assertAuthenticated();
});

Add an assertion that the registered user has the user role; the imported test currently checks authentication and redirect only.

Verify ​

bash
composer lint
composer ci:check

Rerun php artisan db:seed locally; confirm no duplicate users or roles, then register a new account and check its user role.

Commit ​

bash
git add -A
git commit -m '04 - Spatie Permissions'

References ​