From 5e3eb4322cc487d540796317e813ac2ffe076646 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 7 Jul 2026 12:35:03 +0200 Subject: [PATCH 1/3] Test that updating pivot columns does not re-create the synced tenant resource Regression test for https://github.com/archtechx/tenancy/issues/1467 --- tests/ResourceSyncingTest.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 11a172c50..e971b6b54 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -274,6 +274,32 @@ }); }); +test('updating pivot column does not re-create the synced tenant resource', function () { + // Add an extra pivot column so we can update it + Schema::table('tenant_users', fn (Blueprint $table) => $table->string('note')->nullable()); + + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', + ]); + + $tenant = Tenant::create(); + migrateUsersTableForTenants(); + + // Attaching creates the tenant resource + $centralUser->tenants()->attach($tenant); + $tenant->run(fn () => expect(TenantUser::count())->toBe(1)); + + // Updating a pivot column does not re-attach and create the resource again + // (which would throw a duplicate entry error -- regression test for #1467) + $centralUser->tenants()->updateExistingPivot($tenant->getTenantKey(), ['note' => 'foo']); + + $tenant->run(fn () => expect(TenantUser::count())->toBe(1)); +}); + test('detaching central users from tenants or vice versa force deletes the synced tenant resource', function (bool $attachUserToTenant) { $centralUser = CentralUser::create([ 'global_id' => 'acme', From d2fb4bc0d524cd98ec680e973b791f475afc3548 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 7 Jul 2026 12:42:16 +0200 Subject: [PATCH 2/3] Only trigger pivot attach on creation (not on updates) Use created() instead of saved() in TriggerSyncingEvents so that updating pivot columns no longer attempts to attach. Attaching re-created the tenant resource and caused a duplicate entry error (detaching already uses deleting(), so using created() is more consistent with that anyway) --- src/ResourceSyncing/TriggerSyncingEvents.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ResourceSyncing/TriggerSyncingEvents.php b/src/ResourceSyncing/TriggerSyncingEvents.php index 059eb5793..dada31db9 100644 --- a/src/ResourceSyncing/TriggerSyncingEvents.php +++ b/src/ResourceSyncing/TriggerSyncingEvents.php @@ -28,7 +28,8 @@ public static function bootTriggerSyncingEvents(): void $pivot->getCentralResourceAndTenant(); }); - static::saved(static function (self $pivot) { + // Only attach when the pivot is created + static::created(static function (self $pivot) { /** * @var static&Pivot $pivot * @var SyncMaster|null $centralResource From 7900457254364fa742317ca3b218d2711bc8a331 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 7 Jul 2026 13:37:42 +0200 Subject: [PATCH 3/3] Clarify how the pivot saving listener throws The comment said an exception is thrown but not how -- it happens indirectly via findCentralResource() -> getResourceClass(). Also added a @throws annotation to getCentralResourceAndTenant(). --- src/ResourceSyncing/TriggerSyncingEvents.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ResourceSyncing/TriggerSyncingEvents.php b/src/ResourceSyncing/TriggerSyncingEvents.php index dada31db9..b59a95502 100644 --- a/src/ResourceSyncing/TriggerSyncingEvents.php +++ b/src/ResourceSyncing/TriggerSyncingEvents.php @@ -22,9 +22,9 @@ trait TriggerSyncingEvents public static function bootTriggerSyncingEvents(): void { static::saving(static function (self $pivot) { - // Try getting the central resource to see if it is available - // If it is not available, throw an exception to interrupt the saving process - // And prevent creating a pivot record without a central resource + // Try getting the central resource to see if it is available. + // If it is not, getCentralResourceAndTenant() throws (indirectly, via findCentralResource() -> getResourceClass()), + // interrupting the save, preventing the creation of a pivot record without a central resource. $pivot->getCentralResourceAndTenant(); }); @@ -56,6 +56,10 @@ public static function bootTriggerSyncingEvents(): void }); } + /** + * @throws CentralResourceNotAvailableInPivotException Throws when the tenant is the pivot parent + * but the central resource class cannot be resolved (thrown indirectly via findCentralResource() -> getResourceClass()) + */ public function getCentralResourceAndTenant(): array { /** @var $this&Pivot $this */