diff --git a/src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php b/src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php index 6876f476..e48fd8a3 100644 --- a/src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php +++ b/src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php @@ -19,13 +19,15 @@ class DeleteResourcesInTenants extends QueueableListener $centralResource = $event->centralResource; $forceDelete = $event->forceDelete; - tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) { + $relationshipName = $centralResource->getTenantsRelationshipName(); + + tenancy()->runForMultiple($centralResource->{$relationshipName}()->cursor(), function () use ($centralResource, $forceDelete, $relationshipName) { $this->deleteSyncedResource($centralResource, $forceDelete); // Delete pivot records if the central resource doesn't use soft deletes // or the central resource was deleted using forceDelete() if ($forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) { - $centralResource->tenants()->detach(tenant()); + $centralResource->{$relationshipName}()->detach(tenant()); } }); } diff --git a/src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php b/src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php index 1ad862c3..090e7a5c 100644 --- a/src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php +++ b/src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php @@ -24,7 +24,9 @@ class RestoreResourcesInTenants extends QueueableListener return; } - tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource) { + $relationshipName = $centralResource->getTenantsRelationshipName(); + + tenancy()->runForMultiple($centralResource->{$relationshipName}()->cursor(), function () use ($centralResource) { $tenantResourceClass = $centralResource->getTenantModelName(); /** * @var Syncable $centralResource diff --git a/src/ResourceSyncing/Listeners/UpdateOrCreateSyncedResource.php b/src/ResourceSyncing/Listeners/UpdateOrCreateSyncedResource.php index 8677fc21..f36902aa 100644 --- a/src/ResourceSyncing/Listeners/UpdateOrCreateSyncedResource.php +++ b/src/ResourceSyncing/Listeners/UpdateOrCreateSyncedResource.php @@ -63,13 +63,14 @@ class UpdateOrCreateSyncedResource extends QueueableListener } /** @var Tenant&Model&SyncMaster $centralModel */ + $relationshipName = $centralModel->getTenantsRelationshipName(); // Since this model is "dirty" (taken by reference from the event), it might have the tenants // relationship already loaded and cached. For this reason, we refresh the relationship. - $centralModel->load('tenants'); + $centralModel->load($relationshipName); /** @var TenantCollection $tenants */ - $tenants = $centralModel->tenants; + $tenants = $centralModel->{$relationshipName}; return $tenants; } @@ -100,21 +101,22 @@ class UpdateOrCreateSyncedResource extends QueueableListener return ((string) $model->pivot->getAttribute(Tenancy::tenantKeyColumn())) === ((string) $tenant->getTenantKey()); }; - $mappingExists = $centralModel->tenants->contains($currentTenantMapping); + $relationshipName = $centralModel->getTenantsRelationshipName(); + $mappingExists = $centralModel->{$relationshipName}->contains($currentTenantMapping); if (! $mappingExists) { // Here we should call TenantPivot, but we call general Pivot, so that this works // even if people use their own pivot model that is not based on our TenantPivot - Pivot::withoutEvents(function () use ($centralModel, $event) { + Pivot::withoutEvents(function () use ($centralModel, $event, $relationshipName) { /** @var TenantWithDatabase */ $tenant = $event->tenant; - $centralModel->tenants()->attach($tenant->getTenantKey()); + $centralModel->{$relationshipName}()->attach($tenant->getTenantKey()); }); } /** @var TenantCollection $tenants */ - $tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) { + $tenants = $centralModel->{$relationshipName}->filter(function ($model) use ($currentTenantMapping) { // Remove the mapping for the current tenant. return ! $currentTenantMapping($model); }); diff --git a/src/ResourceSyncing/ResourceSyncing.php b/src/ResourceSyncing/ResourceSyncing.php index f0d8cc12..f427b838 100644 --- a/src/ResourceSyncing/ResourceSyncing.php +++ b/src/ResourceSyncing/ResourceSyncing.php @@ -105,12 +105,6 @@ trait ResourceSyncing return true; } - public function tenants(): BelongsToMany - { - return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', $this->getGlobalIdentifierKeyName()) - ->using(TenantMorphPivot::class); - } - public function getGlobalIdentifierKeyName(): string { return 'global_id'; diff --git a/src/ResourceSyncing/SyncMaster.php b/src/ResourceSyncing/SyncMaster.php index 882aeb54..cf108759 100644 --- a/src/ResourceSyncing/SyncMaster.php +++ b/src/ResourceSyncing/SyncMaster.php @@ -6,7 +6,6 @@ namespace Stancl\Tenancy\ResourceSyncing; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; /** @@ -14,13 +13,16 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; */ interface SyncMaster extends Syncable { - /** - * @return BelongsToMany - */ - public function tenants(): BelongsToMany; public function getTenantModelName(): string; + /** + * Should return the name of the relationship to the tenants table (e.g. 'tenants'). + * + * In the class where this interface is implemented, the relationship method also has to be defined. + */ + public function getTenantsRelationshipName(): string; + public function triggerDetachEvent(TenantWithDatabase&Model $tenant): void; public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void; diff --git a/tests/Etc/ResourceSyncing/CentralUser.php b/tests/Etc/ResourceSyncing/CentralUser.php index 1533bd21..c2bd3b47 100644 --- a/tests/Etc/ResourceSyncing/CentralUser.php +++ b/tests/Etc/ResourceSyncing/CentralUser.php @@ -8,10 +8,13 @@ use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Database\Concerns\CentralConnection; use Stancl\Tenancy\ResourceSyncing\ResourceSyncing; use Stancl\Tenancy\ResourceSyncing\SyncMaster; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Stancl\Tenancy\ResourceSyncing\TenantMorphPivot; class CentralUser extends Model implements SyncMaster { use ResourceSyncing, CentralConnection; + protected $guarded = []; public $timestamps = false; @@ -29,6 +32,19 @@ class CentralUser extends Model implements SyncMaster return TenantUser::class; } + public function getTenantsRelationshipName(): string + { + return 'tenants'; + } + + + public function tenants(): BelongsToMany + { + return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', $this->getGlobalIdentifierKeyName()) + ->using(TenantMorphPivot::class); + } + + public function shouldSync(): bool { return static::$shouldSync; diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 3250c37a..359210d4 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -1265,6 +1265,30 @@ test('global scopes on syncable models can break resource syncing', function () expect($tenant1->run(fn () => TenantUser::first()->name))->toBe('tenant2 user'); }); +test('tenants relationship name can be customized', function () { + $tenant = Tenant::create(); + migrateUsersTableForTenants(); + + $tenant->run(function () { + expect(TenantUser::count())->toBe(0); + }); + + // Model with a custom tenants relationship ('organizations') + $centralUserWithCustomTenants = CentralUserWithCustomTenantsRelationship::create([ + 'global_id' => 'tenant_user', + 'name' => 'Tenant user', + 'email' => 'tenant@user', + 'password' => 'secret', + 'role' => 'tester', + ]); + + + $centralUserWithCustomTenants->organizations()->attach($tenant); + + $tenant->run(function () { + expect(TenantUser::firstWhere('name', 'Tenant user'))->not()->toBeNull(); + }); +}); /** * Create two tenants and run migrations for those tenants. * @@ -1322,6 +1346,20 @@ class CentralUser extends BaseCentralUser } } +class CentralUserWithCustomTenantsRelationship extends BaseCentralUser +{ + public function getTenantsRelationshipName(): string + { + return 'organizations'; + } + + public function organizations(): BelongsToMany + { + return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id') + ->using(TenantPivot::class); + } +} + class TenantUser extends BaseTenantUser { public function getCentralModelName(): string @@ -1402,6 +1440,17 @@ class CentralCompany extends Model implements SyncMaster public $table = 'companies'; + public function getTenantsRelationshipName(): string + { + return 'tenants'; + } + + public function tenants(): BelongsToMany + { + return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', $this->getGlobalIdentifierKeyName()) + ->using(TenantMorphPivot::class); + } + public function getTenantModelName(): string { return TenantCompany::class;