Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Many‑to‑Many Relationship Support (e.g., HasTenants trait) #1313

Open
Saad5400 opened this issue Feb 15, 2025 · 4 comments
Open

Add Many‑to‑Many Relationship Support (e.g., HasTenants trait) #1313

Saad5400 opened this issue Feb 15, 2025 · 4 comments
Assignees
Labels
feature New feature or request

Comments

@Saad5400
Copy link

Description
It would be great if the tenancy package provided a trait that automatically handles tenant associations for any model using a polymorphic many‑to‑many pivot table (e.g., tenantables). This approach would allow developers to attach multiple tenants to a variety of models without having to maintain a dedicated tenant_id column on each table. The trait would:

  1. Define a polymorphic many‑to‑many relationship to the Tenant model.
  2. Include a global scope that filters models by the current tenant.
  3. Automatically attach the current tenant upon creation if none is already set.

Why this should be added

  • Flexibility: Instead of limiting tenant associations to a single model type (like User), a polymorphic approach allows multiple models (e.g., User, Post, Invoice) to share the same pivot structure.
  • Reusability: Developers don’t need to replicate tenant relationship logic across different models or maintain multiple pivot tables.
  • Cleaner Database Schema: Polymorphic relationships reduce schema clutter by avoiding a dedicated tenant ID column on every table that needs tenancy support.
  • Simplicity: A shared trait and scope make setup easier, lowering the barrier to using multi‑tenancy across an application’s entire domain.

This feature would offer a more generalized, out‑of‑the‑box approach for developers needing multi‑tenant logic on multiple models.

@Saad5400 Saad5400 added the feature New feature or request label Feb 15, 2025
@stancl
Copy link
Member

stancl commented Feb 15, 2025

Did ChatGPT write this text? Despite all of those words I have no idea what you're actually trying to say.

@Saad5400
Copy link
Author

Yeah I was being lazy, my bad.

I'm trying to do a Many users to Many tenants. Where one user could have access to many tenants instead of being tied to only one.

I think I got it working, but it'd be cool to have it in the package directly as a trait. I'd be happy to open a PR.

@stancl
Copy link
Member

stancl commented Feb 18, 2025

You still haven't said what types of mappings you're talking about. Is this referencing resource syncing?

@Saad5400
Copy link
Author

Saad5400 commented Feb 18, 2025

I'm referring to a many-to-many relationship with the tenant model in a single database.

Any model (User, Post, Invoice, etc.) can be attached to one or more tenants via a single pivot table.

The trait would automatically apply a global scope to filter models by the currently active tenant.

Automatic Attachment: When creating a new model instance, if no tenant is specified, the trait could automatically associate it with the current tenant.

This what I did:

Trait:

<?php

declare(strict_types=1);

namespace App\Tenancy\Traits;

use App\Tenancy\Database\TenantMorphScope;

trait HasTenants
{
    /**
     * The name of the pivot table used for the polymorphic relationship.
     */
    public static $tenantPivotTable = 'tenantables';

    /**
     * The foreign key on the pivot table that references the tenant model.
     */
    public static $tenantForeignKey = 'tenant_id';

    /**
     * The morph name used for this polymorphic relation.
     */
    public static $morphName = 'tenantable';

    /**
     * The name of the column on the tenantables table that references the tenantable model.
     */
    public static $tenantableIdColumn = 'tenantable_id';

    /**
     * Define a polymorphic many-to-many relationship with the Tenant model.
     *
     * This assumes your tenant model is set in your config at:
     * config('tenancy.tenant_model')
     */
    public function tenants()
    {
        return $this->morphToMany(
            config('tenancy.tenant_model'),
            static::$morphName,             // The morph name used for this polymorphic relation
            static::$tenantPivotTable,      // The pivot table name (tenantables)
            static::$tenantableIdColumn,    // The local key on the pivot table (User's integer ID)
            static::$tenantForeignKey,      // The foreign key on the pivot table (Tenant's UUID)
        );
    }


    /**
     * Boot the trait to add a global scope and automatically attach the current tenant.
     */
    public static function bootHasTenants()
    {
        // Add a global scope so that queries on models using this trait are
        // automatically filtered to include only those associated with the current tenant.
        static::addGlobalScope(new TenantMorphScope);

        // When a new model is created, automatically attach the current tenant
        // if tenancy is initialized and no tenant has been attached yet.
        static::created(function ($model) {
            if (function_exists('tenancy') && tenancy()->initialized) {
                if (!$model->relationLoaded('tenants') || $model->tenants->isEmpty()) {
                    $model->tenants()->attach(tenant()->getTenantKey());
                }
            }
        });
    }
}

Scope:

<?php

declare(strict_types=1);

namespace App\Tenancy\Database;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantMorphScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        // If tenancy isn't initialized, don't apply the scope
        if (! function_exists('tenancy') || ! tenancy()->initialized) {
            return;
        }

        // Filter by models that have a tenant matching the current tenant's ID
        $builder->whereHas('tenants', function (Builder $query) {
            $query->where('id', tenant()->getTenantKey());
        });
    }

    public function extend(Builder $builder)
    {
        // Provide a convenient macro to remove this global scope
        $builder->macro('withoutTenancy', function (Builder $builder) {
            return $builder->withoutGlobalScope($this);
        });
    }
}

It'd be cool if it's already built in the package. And I'd be happy to open a PR for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants