Many-to-many polymorphic relation
Hello. This is my first attempt to something like this and I am not sure if I am doing the things right.
I have 3 models: User, Project and Task. I also have installed spatie/laravel-permissions and set use HasRoles; for the user model. And here comes the tricky part:
- I have 2 roles: Project owner and Team Leader
- Any user is a project owner for projects created by him and a team leader in other projects.
- To manage this, I have a pivot table
projects_users:
Schema::create('projects_users', function (Blueprint $table): void {
$table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignUuid('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
$table->unique(['user_id', 'project_id', 'role_id']);
});
Now, the Team Leader role, has a set of default permissions but I want to give the project Owner a way to edit them. Also, regarding tasks CRUD I need to know if the user (Team Leader) has the permission to perform them.
What I have come up is this:
- Create table
projects_user_permissions:
Schema::create('projects_user_permissions', function (Blueprint $table): void {
$table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignUuid('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('permission_id')->constrained('permissions')->cascadeOnDelete();
$table->unique(['user_id', 'project_id', 'permission_id'], 'unique_ids');
});
To manage the project users and the Team leaders permissions I have these relations in my User model:
public function usersInOwnedProjects(): HasMany
{
return $this->hasMany(ProjectsUser::class, 'user_id', 'id');
}
public function getAllUsersInOwnedProjects(): User|Builder
{
$owned_project_ids = $this->ownProjects->pluck('id');
return self::whereHas('usersInOwnedProjects', static function ($query) use ($owned_project_ids): void {
$query->whereIn('project_id', $owned_project_ids)
->where('user_id', '!=', auth()->id());
})->with([
'usersInOwnedProjects' => function ($query): void {
$query->select('user_id', 'role_id', 'project_id');
},
]);
}
public function projectRole(int $projectId): HasOneThrough
{
return $this->hasOneThrough(
Role::class,
ProjectsUser::class,
'user_id',
'id',
'',
'role_id'
)->where('projects_users.project_id', $projectId);
}
public function hasProjectRole(int $projectId, string $roleName): bool
{
$role_id = Role::where('name', $roleName)->first()->id;
return $this->projectRole($projectId)->where('role_id', $role_id)->exists();
}
public function checkIfHasRoleForAnyProject(string $roleName): bool
{
return $this->hasOneThrough(
Role::class,
ProjectsUser::class,
'user_id',
'id',
'',
'role_id'
)->where('projects_users.role_id', Role::where('name', $roleName)->first()->id)
->exists();
}
public function projectPermissions(int $projectId): HasManyThrough
{
return $this->hasManyThrough(
Permission::class,
ProjectsUserPermission::class,
'user_id',
'id',
'',
'permission_id'
)->where('projects_user_permissions.project_id', $projectId);
}
public function hasProjectPermission(int $projectId, string $permissionName): bool
{
return $this->projectPermissions($projectId)->where('name', $permissionName)->exists();
}
public function checkIfHasPermissionForAnyProject(string $permissionName): bool
{
return $this->hasManyThrough(
Permission::class,
ProjectsUserPermission::class,
'user_id',
'id',
'',
'permission_id'
)->where('projects_user_permissions.permission_id', Permission::where('name', $permissionName)->first()->id)
->exists();
}
and the pivots files I use:
final class ProjectsUser extends Pivot
{
public function users(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function projects(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function tasks(): HasManyThrough
{
return $this->hasManyThrough(Task::class, Project::class);
}
public function role(): array|Role|null
{
return Role::find($this->role_id);
}
}
final class ProjectsUserPermission extends Pivot
{
public function users(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function projects(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function tasks(): HasManyThrough
{
return $this->hasManyThrough(Task::class, Project::class);
}
public function permission(): array|Permission|null
{
return Permission::find($this->permission_id);
}
}
It is working, but I think it's very complicated and there should be a more elegant way to do this.
Any thoughts?
Please or to participate in this conversation.