Back to Course |
Roles and Permissions in Laravel 11

Teams/Users DB: Factories and Seeders

In this lesson, we will take care of the Teams DB structure.

In the previous lesson, we installed the Spatie Permissions package. Next, we must enable teams in the configuration file.

config/permission.php:

// ...
 
'teams' => true,
 
// ...

Next, we create the Team Model and Migration.

php artisan make:model Team -mf

app/Models/Team.php:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
 
class Team extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
];
}

database/migrations/xxx_create_teams_table.php:

Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});

Also, let's define the factory, which will be used later in the tests.

database/factories/TeamFactory.php:

use Illuminate\Database\Eloquent\Factories\Factory;
 
class TeamFactory extends Factory
{
public function definition(): array
{
return [
'name' => 'Clinic ' . fake()->word(),
];
}
}

Users in Teams: Relationships

We need to implement two rules:

  • User may belong to multiple teams (clinic owner)
  • Only one team is active (current) at any point

First, let's create the second part, as it's easier. We will save the current team ID in the users table.

php artisan make:migration add_current_team_id_to_users_table

database/migrations/xxx_add_current_team_id_to_users_table.php:

Schema::table('users', function (Blueprint $table) {
$table->foreignId('current_team_id')
->nullable()
->constrained('teams')
->nullOnDelete();
});

And then the Model's fillable and relationship:

app/Models/User.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class User extends Authenticatable
{
// ...
 
protected $fillable = [
'name',
'email',
'password',
'current_team_id',
];
 
public function currentTeam(): BelongsTo
{
return $this->belongsTo(Team::class, 'current_team_id');
}

Now, with multiple teams, you might be tempted to create a separate team_user pivot table, but with the Spatie Permission package, you don't have to.

There's already a DB table called model_has_roles with the team_id column, so that's exactly where we save each user's teams.

We just need to create a separate method in the Model to get those teams.

app/Models/User.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class User extends Authenticatable
{
// ...
 
public function teams(): BelongsToMany
{
return $this->belongsToMany(
Team::class,
config('permission.table_names.model_has_roles'),
'model_id'
);
}
 
public function belongsToTeam(Team $team): bool
{
return $this->teams->contains(fn ($t) => $t->id === $team->id);
}
}

Also, as you can see, we created a helper method, belongsToTeam(), that we will use in the Controller a bit later.


Users: Factories and Seeders

Next, we will modify the UserFactory to have a Factory state for every role.

In these states, we will create a team and set the team as active for that user. Finally, we will assign a role within that current team.

database/factories/UserFactory.php:

use App\Enums\Role;
use App\Models\User;
use App\Models\Team;
use Illuminate\Database\Eloquent\Factories\Factory;
 
class UserFactory extends Factory
{
// ...
 
private string $clinicDefaultName = 'Clinic 123';
 
public function masterAdmin(): static
{
return $this->afterCreating(function (User $user) {
// Although Master admin doesn't have a team
// We need to create a "Fake" team
// Because of spatie/laravel-permission DB structure
$team = Team::create([
'name' => 'Master Admin Team',
]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::MasterAdmin);
});
}
 
public function clinicOwner(): static
{
return $this->afterCreating(function (User $user) {
$team = Team::create([
'name' => $this->clinicDefaultName,
]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::ClinicOwner);
});
}
 
public function clinicAdmin(): static
{
return $this->afterCreating(function (User $user) {
$team = Team::firstOrCreate(['name' => $this->clinicDefaultName]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::ClinicAdmin);
});
}
 
public function doctor(): static
{
return $this->afterCreating(function (User $user) {
$team = Team::firstOrCreate(['name' => $this->clinicDefaultName]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::Doctor);
});
}
 
public function staff(): static
{
return $this->afterCreating(function (User $user) {
$team = Team::firstOrCreate(['name' => $this->clinicDefaultName]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::Staff);
});
}
 
public function patient(): static
{
return $this->afterCreating(function (User $user) {
$team = Team::firstOrCreate(['name' => $this->clinicDefaultName]);
 
$user->update(['current_team_id' => $team->id]);
 
setPermissionsTeamId($team->id);
 
$user->assignRole(Role::Patient);
});
}
}

Notice: see how we're re-using the Enum values here again? If we didn't, it would increase the possibility of typos.

Now, we can seed some demo users using those states from above.

database/seeders/UserSeeder.php:

use App\Models\User;
use Illuminate\Database\Seeder;
 
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()
->masterAdmin()
->create([
'name' => 'Master Admin',
'email' => 'master@admin.com',
]);
 
User::factory()
->clinicOwner()
->create([
'name' => 'Clinic Owner',
'email' => 'owner@clinic.com',
]);
 
User::factory()
->clinicAdmin()
->create([
'name' => 'Clinic Admin',
'email' => 'admin@clinic.com',
]);
 
User::factory()
->staff()
->create([
'name' => 'Staff User',
'email' => 'staff@clinic.com',
]);
 
User::factory()
->patient()
->create([
'name' => 'Regular Patient',
'email' => 'user@clinic.com',
]);
 
User::factory(5)
->patient()
->create();
 
User::factory(5)
->doctor()
->create();
 
User::factory(5)
->staff()
->create();
}
}

database/seeders/DatabaseSeeder.php:

use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RoleAndPermissionSeeder::class,
UserSeeder::class,
]);
}
}

After this seeder, here's what we have in the database:

Users:

Model_has_roles:

So, we have the DB structure ready with models/migrations/factories/seeders. It's time to build the actual features.