From 735a2dd48beda557eec08dbc2d62ff78a1ad85fb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 16 Sep 2026 16:42:06 +0200 Subject: [PATCH 1/3] Make force deleting central resources force delete already-trashed tenant resources --- .../Listeners/DeletesSyncedResources.php | 14 +++--- tests/ResourceSyncingTest.php | 43 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/ResourceSyncing/Listeners/DeletesSyncedResources.php b/src/ResourceSyncing/Listeners/DeletesSyncedResources.php index fc136b68..7e7dfe3d 100644 --- a/src/ResourceSyncing/Listeners/DeletesSyncedResources.php +++ b/src/ResourceSyncing/Listeners/DeletesSyncedResources.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\ResourceSyncing\Listeners; use Illuminate\Database\Eloquent\Model; -use Stancl\Tenancy\ResourceSyncing\Syncable; use Stancl\Tenancy\ResourceSyncing\SyncMaster; +use Illuminate\Contracts\Database\Eloquent\Builder; trait DeletesSyncedResources { @@ -14,16 +14,20 @@ trait DeletesSyncedResources { $tenantResourceClass = $centralResource->getTenantModelName(); - /** @var (Syncable&Model)|null $tenantResource */ - $tenantResource = $tenantResourceClass::firstWhere( + /** @var Builder $query */ + $query = $tenantResourceClass::where( $centralResource->getGlobalIdentifierKeyName(), $centralResource->getGlobalIdentifierKey() ); if ($force) { - $tenantResource?->forceDelete(); + if ($query->hasMacro('withTrashed')) { + $query->withTrashed(); // @phpstan-ignore method.notFound + } + + $query->first()?->forceDelete(); } else { - $tenantResource?->delete(); + $query->first()?->delete(); } } } diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index e971b6b5..11bd9152 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -1193,6 +1193,49 @@ test('using forceDelete on a central resource with soft deletes force deletes th expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull(); }); +test('force deleting a central resource force deletes tenant resources that are already trashed', function () { + [$tenant1] = createTenantsAndRunMigrations(); + migrateUsersTableForTenants(); + addExtraColumns(true); + + $centralUser = CentralUserWithSoftDeletes::create([ + 'global_id' => 'user', + 'name' => 'Central user', + 'email' => 'central@localhost', + 'password' => 'password', + 'role' => 'commenter', + 'foo' => 'foo', + ]); + + $centralUser->tenants()->attach($tenant1); + + tenancy()->initialize($tenant1); + + // Trash the tenant resource a day in the past so that deleting it again would update deleted_at + $this->travelTo(now()->subDay()); + + TenantUserWithSoftDeletes::firstWhere('global_id', 'user')->delete(); + + $this->travelBack(); + + $tenantUserDeletedAt = TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user')->deleted_at; + + expect($tenantUserDeletedAt)->not()->toBeNull(); + + // Trashing the central resource doesn't touch already trashed tenant resources + $centralUser->delete(); + + expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user')->deleted_at)->toEqual($tenantUserDeletedAt); + + tenancy()->end(); + + $centralUser->forceDelete(); + + tenancy()->initialize($tenant1); + + expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull(); +}); + test('resource creation works correctly when tenant resource provides defaults in the creation attributes', function () { [$tenant1, $tenant2] = createTenantsAndRunMigrations(); From adad6acebebdd233016a6e2b6362e730a9934a25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 16 Sep 2026 14:42:36 +0000 Subject: [PATCH 2/3] Fix code style (php-cs-fixer) --- src/ResourceSyncing/Listeners/DeletesSyncedResources.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ResourceSyncing/Listeners/DeletesSyncedResources.php b/src/ResourceSyncing/Listeners/DeletesSyncedResources.php index 7e7dfe3d..7be3fc6c 100644 --- a/src/ResourceSyncing/Listeners/DeletesSyncedResources.php +++ b/src/ResourceSyncing/Listeners/DeletesSyncedResources.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\ResourceSyncing\Listeners; +use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\ResourceSyncing\SyncMaster; -use Illuminate\Contracts\Database\Eloquent\Builder; trait DeletesSyncedResources { From 86c265da0378326cdb96b94b79538b24e7577f03 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 17 Sep 2026 10:28:01 +0200 Subject: [PATCH 3/3] Test detaching, test trashing already trashed resources in a separate test --- tests/ResourceSyncingTest.php | 44 ++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 11bd9152..7e09101e 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -1198,6 +1198,42 @@ test('force deleting a central resource force deletes tenant resources that are migrateUsersTableForTenants(); addExtraColumns(true); + foreach ([ + fn ($user, $tenant) => $user->forceDelete(), + fn ($user, $tenant) => $user->tenants()->detach($tenant), + ] as $triggerForceDelete) { + $centralUser = CentralUserWithSoftDeletes::create([ + 'global_id' => 'user', + 'name' => 'Central user', + 'email' => 'central@localhost', + 'password' => 'password', + 'role' => 'commenter', + 'foo' => 'foo', + ]); + + $centralUser->tenants()->attach($tenant1); + + tenancy()->initialize($tenant1); + + TenantUserWithSoftDeletes::firstWhere('global_id', 'user')->delete(); + + tenancy()->end(); + + $triggerForceDelete($centralUser, $tenant1); + + tenancy()->initialize($tenant1); + + expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull(); + + tenancy()->end(); + } +}); + +test('trashing a central resource does not affect tenant resources that are already trashed', function () { + [$tenant1] = createTenantsAndRunMigrations(); + migrateUsersTableForTenants(); + addExtraColumns(true); + $centralUser = CentralUserWithSoftDeletes::create([ 'global_id' => 'user', 'name' => 'Central user', @@ -1226,14 +1262,6 @@ test('force deleting a central resource force deletes tenant resources that are $centralUser->delete(); expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user')->deleted_at)->toEqual($tenantUserDeletedAt); - - tenancy()->end(); - - $centralUser->forceDelete(); - - tenancy()->initialize($tenant1); - - expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull(); }); test('resource creation works correctly when tenant resource provides defaults in the creation attributes', function () {