Add Many‑to‑Many Relationship Support (e.g., HasTenants trait)
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:
- Define a polymorphic many‑to‑many relationship to the Tenant model.
- Include a global scope that filters models by the current tenant.
- 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.
Did ChatGPT write this text? Despite all of those words I have no idea what you're actually trying to say.
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.
You still haven't said what types of mappings you're talking about. Is this referencing resource syncing?
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!
Appreciate the writeup. Since single-db tenancy (based on model traits) is essentially a userland feature, with necessary code changes, this doesn't require a feature in Tenancy (people can just implement something like the above in their own app). I'm going to close this for now and if there's a lot of demand for this being included by default I can consider adding it at any point since it doesn't require breaking changes 👍