Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Merklin's avatar

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:

  1. I have 2 roles: Project owner and Team Leader
  2. Any user is a project owner for projects created by him and a team leader in other projects.
  3. 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?

0 likes
0 replies

Please or to participate in this conversation.