From bf42a1289451b87c774cb293ead97d3f850f9451 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 26 Mar 2026 15:34:04 +0100 Subject: [PATCH 1/9] Change tenant storage listeners into jobs Also move the commented jobs to the JobPipelines and update FilesystemTenancyBootstrapperTest accordingly. --- assets/TenancyServiceProvider.stub.php | 6 ++-- .../CreateTenantStorage.php | 21 ++++++++++--- src/Jobs/DeleteTenantStorage.php | 31 +++++++++++++++++++ src/Listeners/DeleteTenantStorage.php | 20 ------------ .../FilesystemTenancyBootstrapperTest.php | 8 +++-- 5 files changed, 55 insertions(+), 31 deletions(-) rename src/{Listeners => Jobs}/CreateTenantStorage.php (51%) create mode 100644 src/Jobs/DeleteTenantStorage.php delete mode 100644 src/Listeners/DeleteTenantStorage.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 1cb358de0..050a2ffcd 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -46,6 +46,7 @@ public function events() Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, // Jobs\SeedDatabase::class, + // Jobs\CreateTenantStorage::class, // Jobs\CreateStorageSymlinks::class, // Your own jobs to prepare the tenant. @@ -53,8 +54,6 @@ public function events() ])->send(function (Events\TenantCreated $event) { return $event->tenant; })->shouldBeQueued(false), - - // Listeners\CreateTenantStorage::class, ], Events\SavingTenant::class => [], Events\TenantSaved::class => [], @@ -63,12 +62,11 @@ public function events() Events\DeletingTenant::class => [ JobPipeline::make([ Jobs\DeleteDomains::class, + // Jobs\DeleteTenantStorage::class, // Jobs\RemoveStorageSymlinks::class, ])->send(function (Events\DeletingTenant $event) { return $event->tenant; })->shouldBeQueued(false), - - // Listeners\DeleteTenantStorage::class, ], Events\TenantDeleted::class => [ JobPipeline::make([ diff --git a/src/Listeners/CreateTenantStorage.php b/src/Jobs/CreateTenantStorage.php similarity index 51% rename from src/Listeners/CreateTenantStorage.php rename to src/Jobs/CreateTenantStorage.php index 3bebb7310..8e4e15a91 100644 --- a/src/Listeners/CreateTenantStorage.php +++ b/src/Jobs/CreateTenantStorage.php @@ -2,9 +2,14 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Listeners; +namespace Stancl\Tenancy\Jobs; -use Stancl\Tenancy\Events\Contracts\TenantEvent; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Stancl\Tenancy\Contracts\Tenant; /** * Can be used to manually create framework directories in the tenant storage when storage_path() is scoped. @@ -13,11 +18,17 @@ * * Generally not needed anymore as the directory is also created by the FilesystemTenancyBootstrapper. */ -class CreateTenantStorage +class CreateTenantStorage implements ShouldQueue { - public function handle(TenantEvent $event): void + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct( + public Tenant $tenant, + ) {} + + public function handle(): void { - $storage_path = tenancy()->run($event->tenant, fn () => storage_path()); + $storage_path = tenancy()->run($this->tenant, fn () => storage_path()); $cache_path = "$storage_path/framework/cache"; if (! is_dir($cache_path)) { diff --git a/src/Jobs/DeleteTenantStorage.php b/src/Jobs/DeleteTenantStorage.php new file mode 100644 index 000000000..bbe8b08e5 --- /dev/null +++ b/src/Jobs/DeleteTenantStorage.php @@ -0,0 +1,31 @@ +run($this->tenant, fn () => storage_path()); + + if (is_dir($path)) { + File::deleteDirectory($path); + } + } +} diff --git a/src/Listeners/DeleteTenantStorage.php b/src/Listeners/DeleteTenantStorage.php deleted file mode 100644 index ec360073e..000000000 --- a/src/Listeners/DeleteTenantStorage.php +++ /dev/null @@ -1,20 +0,0 @@ -run($event->tenant, fn () => storage_path()); - - if (is_dir($path)) { - File::deleteDirectory($path); - } - } -} diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 628b974e3..04b4e1ed6 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -13,7 +13,7 @@ use Stancl\Tenancy\Jobs\CreateStorageSymlinks; use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; -use Stancl\Tenancy\Listeners\DeleteTenantStorage; +use Stancl\Tenancy\Jobs\DeleteTenantStorage; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use function Stancl\Tenancy\Tests\pest; @@ -185,7 +185,11 @@ }); test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { - Event::listen(DeletingTenant::class, DeleteTenantStorage::class); + Event::listen(DeletingTenant::class, + JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); tenancy()->initialize(Tenant::create()); $tenantStoragePath = storage_path(); From 873488be55107bc0b20c242c941ecdeb16c3cb39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 14:34:27 +0000 Subject: [PATCH 2/9] Fix code style (php-cs-fixer) --- src/Jobs/DeleteTenantStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jobs/DeleteTenantStorage.php b/src/Jobs/DeleteTenantStorage.php index bbe8b08e5..c0329c502 100644 --- a/src/Jobs/DeleteTenantStorage.php +++ b/src/Jobs/DeleteTenantStorage.php @@ -9,8 +9,8 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Stancl\Tenancy\Contracts\Tenant; use Illuminate\Support\Facades\File; +use Stancl\Tenancy\Contracts\Tenant; class DeleteTenantStorage implements ShouldQueue { From 5b2cce347a41251be6fb788b2406633b17a461c8 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 27 Mar 2026 11:58:00 +0100 Subject: [PATCH 3/9] Add test for CreateTenantStorage --- .../FilesystemTenancyBootstrapperTest.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 04b4e1ed6..4594d7aa9 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -13,6 +13,7 @@ use Stancl\Tenancy\Jobs\CreateStorageSymlinks; use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Jobs\CreateTenantStorage; use Stancl\Tenancy\Jobs\DeleteTenantStorage; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; @@ -184,6 +185,26 @@ $this->assertDirectoryDoesNotExist(public_path("public-$tenantKey")); }); +test('tenant storage gets created when TenantCreated listens to CreateTenantStorage', function() { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + ], + ]); + + Event::listen(TenantCreated::class, + JobPipeline::make([CreateTenantStorage::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); + + $centralStoragePath = storage_path(); + $tenant = Tenant::create(); + $tenantStoragePath = $centralStoragePath . '/tenant' . $tenant->getTenantKey(); + + $this->assertDirectoryExists($tenantStoragePath . '/framework/cache'); +}); + test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { @@ -260,4 +281,3 @@ expect(file_get_contents(storage_path() . "/app/public/scoped_disk_prefix/foo.txt"))->toBe('central2'); expect(file_get_contents(storage_path() . "/tenant{$tenant->id}/app/public/scoped_disk_prefix/foo.txt"))->toBe('tenant'); }); - From a28593af17167018c3c212d75fcfcdf16da1452e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 16:24:12 +0200 Subject: [PATCH 4/9] Correct storage deletion test --- tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 4594d7aa9..136b2100d 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -206,6 +206,10 @@ }); test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { + config([ + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], + ]); + Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { return $event->tenant; @@ -215,12 +219,8 @@ tenancy()->initialize(Tenant::create()); $tenantStoragePath = storage_path(); - Storage::fake('test'); - expect(File::isDirectory($tenantStoragePath))->toBeTrue(); - Storage::put('test.txt', 'testing file'); - tenant()->delete(); expect(File::isDirectory($tenantStoragePath))->toBeFalse(); From 33c28405ad8c945374b7bcfdb2e37a91ff7e5c23 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 16:36:52 +0200 Subject: [PATCH 5/9] Guard against accidental central storage deletion Also fix and improve related test. --- src/Jobs/DeleteTenantStorage.php | 18 +++++++- .../FilesystemTenancyBootstrapperTest.php | 42 +++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Jobs/DeleteTenantStorage.php b/src/Jobs/DeleteTenantStorage.php index c0329c502..f31ddbd22 100644 --- a/src/Jobs/DeleteTenantStorage.php +++ b/src/Jobs/DeleteTenantStorage.php @@ -12,6 +12,13 @@ use Illuminate\Support\Facades\File; use Stancl\Tenancy\Contracts\Tenant; +/** + * Only delete the tenant storage if storage path suffixing is enabled + * and the tenant's storage path is different from the central storage path. + * + * This is to prevent accidental deletion of the central storage when + * a tenant's storage path is not properly suffixed. + */ class DeleteTenantStorage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -22,9 +29,18 @@ public function __construct( public function handle(): void { + // Skip storage deletion if path suffixing is disabled + if (config('tenancy.filesystem.suffix_storage_path') === false) { + return; + } + + $centralPath = tenancy()->central(fn () => storage_path()); $path = tenancy()->run($this->tenant, fn () => storage_path()); - if (is_dir($path)) { + // Skip storage deletion if tenant's storage path is the same as central storage path + $tenantPathIsCentral = realpath($path) === realpath($centralPath); + + if (is_dir($path) && ! $tenantPathIsCentral) { File::deleteDirectory($path); } } diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 136b2100d..e77493243 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -206,19 +206,53 @@ }); test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { - config([ - 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], - ]); - Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { return $event->tenant; })->shouldBeQueued(false)->toListener() ); + $centralStoragePath = storage_path(); + tenancy()->initialize(Tenant::create()); + + // FilesystemTenancyBootstrapper not enabled, + // tenant and central storage path is the same, + // the storage deletion will be skipped. + $tenantStoragePath = storage_path(); + expect($tenantStoragePath)->toBe($centralStoragePath); + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + tenant()->delete(); + + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + + config([ + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], + 'tenancy.filesystem.suffix_storage_path' => false, + ]); + + tenancy()->initialize(Tenant::create()); + + $tenantStoragePath = storage_path(); + + // FilesystemTenancyBootstrapper enabled, + // but tenant and central storage path is still the same + // because suffix_storage_path is false. + // The storage deletion will be skipped. + expect($tenantStoragePath)->toBe($centralStoragePath); + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + tenant()->delete(); + + expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + + config([ + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], + 'tenancy.filesystem.suffix_storage_path' => true, + ]); + tenancy()->initialize(Tenant::create()); $tenantStoragePath = storage_path(); + expect($centralStoragePath)->not()->toBe($tenantStoragePath); expect(File::isDirectory($tenantStoragePath))->toBeTrue(); tenant()->delete(); From 5e0153c507d9434c5c6f6f12e379cbe8948b463e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 16:47:08 +0200 Subject: [PATCH 6/9] Obtain suffix base from config instead of hardcoding Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index e77493243..d0c8af592 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -200,7 +200,8 @@ $centralStoragePath = storage_path(); $tenant = Tenant::create(); - $tenantStoragePath = $centralStoragePath . '/tenant' . $tenant->getTenantKey(); + $suffixBase = config('tenancy.filesystem.suffix_base', 'tenant'); + $tenantStoragePath = $centralStoragePath . '/' . $suffixBase . $tenant->getTenantKey(); $this->assertDirectoryExists($tenantStoragePath . '/framework/cache'); }); From 97e856616cde8e4fb447047cac8384064e1fe10a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 20 Apr 2026 09:05:26 +0200 Subject: [PATCH 7/9] Add deprecated listener versions of the storage jobs Without this, updating Tenancy in existing projects would break TenancyServiceProvider. The logic of the deprecated versions is up-to-date with the changes made in the jobs up until now. --- src/Listeners/CreateTenantStorage.php | 24 ++++++++++++++++++++ src/Listeners/DeleteTenantStorage.php | 32 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/Listeners/CreateTenantStorage.php create mode 100644 src/Listeners/DeleteTenantStorage.php diff --git a/src/Listeners/CreateTenantStorage.php b/src/Listeners/CreateTenantStorage.php new file mode 100644 index 000000000..3210a050a --- /dev/null +++ b/src/Listeners/CreateTenantStorage.php @@ -0,0 +1,24 @@ +run($event->tenant, fn () => storage_path()); + $cache_path = "$storage_path/framework/cache"; + + if (! is_dir($cache_path)) { + // Create the tenant's storage directory and /framework/cache within (used for e.g. real-time facades) + mkdir($cache_path, 0750, true); + } + } +} diff --git a/src/Listeners/DeleteTenantStorage.php b/src/Listeners/DeleteTenantStorage.php new file mode 100644 index 000000000..c32e14ba4 --- /dev/null +++ b/src/Listeners/DeleteTenantStorage.php @@ -0,0 +1,32 @@ +central(fn () => storage_path()); + $path = tenancy()->run($event->tenant, fn () => storage_path()); + + // Skip storage deletion if tenant's storage path is the same as central storage path + $tenantPathIsCentral = realpath($path) === realpath($centralPath); + + if (is_dir($path) && ! $tenantPathIsCentral) { + File::deleteDirectory($path); + } + } +} From 5bb76e1421673d63dfabcab8da7c2a347b77db80 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Mon, 20 Apr 2026 18:22:04 +0200 Subject: [PATCH 8/9] refactor 1. Remove the CreateTenantStorage job altogether since as the docblock says the class should be redundant now that FilesystemTenancyBootstrapper creates this path automatically when storage_path suffixing is enabled 2. Remove docblock on the DeleteTenantStorage job - a class's docblock should describe what it does, not how it handles edge cases. Here the former isn't even necessary and the latter is well explained by comments in the implementation. 3. Remove the CreateTenantStorage test following the class's removal, the test would pass on its own even without the job with just tenancy initialization as mentioned above. 4. Slightly improve the structure of the DeleteTenantStorage job and deprecated listener 5. Improve deprecation notices so they include full steps for upgrading to the new approach. --- assets/TenancyServiceProvider.stub.php | 1 - src/Jobs/CreateTenantStorage.php | 39 ------------------- src/Jobs/DeleteTenantStorage.php | 24 +++++------- src/Listeners/CreateTenantStorage.php | 2 +- src/Listeners/DeleteTenantStorage.php | 19 +++++---- .../FilesystemTenancyBootstrapperTest.php | 22 ----------- 6 files changed, 22 insertions(+), 85 deletions(-) delete mode 100644 src/Jobs/CreateTenantStorage.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 050a2ffcd..915e80c2b 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -46,7 +46,6 @@ public function events() Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, // Jobs\SeedDatabase::class, - // Jobs\CreateTenantStorage::class, // Jobs\CreateStorageSymlinks::class, // Your own jobs to prepare the tenant. diff --git a/src/Jobs/CreateTenantStorage.php b/src/Jobs/CreateTenantStorage.php deleted file mode 100644 index 8e4e15a91..000000000 --- a/src/Jobs/CreateTenantStorage.php +++ /dev/null @@ -1,39 +0,0 @@ -run($this->tenant, fn () => storage_path()); - $cache_path = "$storage_path/framework/cache"; - - if (! is_dir($cache_path)) { - // Create the tenant's storage directory and /framework/cache within (used for e.g. real-time facades) - mkdir($cache_path, 0750, true); - } - } -} diff --git a/src/Jobs/DeleteTenantStorage.php b/src/Jobs/DeleteTenantStorage.php index f31ddbd22..36a0d326f 100644 --- a/src/Jobs/DeleteTenantStorage.php +++ b/src/Jobs/DeleteTenantStorage.php @@ -12,13 +12,6 @@ use Illuminate\Support\Facades\File; use Stancl\Tenancy\Contracts\Tenant; -/** - * Only delete the tenant storage if storage path suffixing is enabled - * and the tenant's storage path is different from the central storage path. - * - * This is to prevent accidental deletion of the central storage when - * a tenant's storage path is not properly suffixed. - */ class DeleteTenantStorage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -29,19 +22,22 @@ public function __construct( public function handle(): void { - // Skip storage deletion if path suffixing is disabled if (config('tenancy.filesystem.suffix_storage_path') === false) { + // Skip storage deletion if path suffixing is disabled return; } - $centralPath = tenancy()->central(fn () => storage_path()); - $path = tenancy()->run($this->tenant, fn () => storage_path()); + $centralStoragePath = tenancy()->central(fn () => storage_path()); + $tenantStoragePath = tenancy()->run($this->tenant, fn () => storage_path()); - // Skip storage deletion if tenant's storage path is the same as central storage path - $tenantPathIsCentral = realpath($path) === realpath($centralPath); + if ($tenantStoragePath === $centralStoragePath) { + // Check again to ensure the tenant storage path is distinct from the central storage path + // to avoid any accidental central storage path deletion + return; + } - if (is_dir($path) && ! $tenantPathIsCentral) { - File::deleteDirectory($path); + if (is_dir($tenantStoragePath)) { + File::deleteDirectory($tenantStoragePath); } } } diff --git a/src/Listeners/CreateTenantStorage.php b/src/Listeners/CreateTenantStorage.php index 3210a050a..0ffdef602 100644 --- a/src/Listeners/CreateTenantStorage.php +++ b/src/Listeners/CreateTenantStorage.php @@ -7,7 +7,7 @@ use Stancl\Tenancy\Events\Contracts\TenantEvent; /** - * @deprecated use Stancl\Tenancy\Jobs\CreateTenantStorage instead. + * @deprecated FilesystemTenancyBootstrapper creates the path automatically when suffix_storage_path is enabled. */ class CreateTenantStorage { diff --git a/src/Listeners/DeleteTenantStorage.php b/src/Listeners/DeleteTenantStorage.php index c32e14ba4..06f204545 100644 --- a/src/Listeners/DeleteTenantStorage.php +++ b/src/Listeners/DeleteTenantStorage.php @@ -8,25 +8,28 @@ use Stancl\Tenancy\Events\Contracts\TenantEvent; /** - * @deprecated use Stancl\Tenancy\Jobs\DeleteTenantStorage instead. + * @deprecated Use Stancl\Tenancy\Jobs\DeleteTenantStorage in a job pipeline instead. */ class DeleteTenantStorage { public function handle(TenantEvent $event): void { - // Skip storage deletion if path suffixing is disabled if (config('tenancy.filesystem.suffix_storage_path') === false) { + // Skip storage deletion if path suffixing is disabled return; } - $centralPath = tenancy()->central(fn () => storage_path()); - $path = tenancy()->run($event->tenant, fn () => storage_path()); + $centralStoragePath = tenancy()->central(fn () => storage_path()); + $tenantStoragePath = tenancy()->run($event->tenant, fn () => storage_path()); - // Skip storage deletion if tenant's storage path is the same as central storage path - $tenantPathIsCentral = realpath($path) === realpath($centralPath); + if ($tenantStoragePath === $centralStoragePath) { + // Check again to ensure the tenant storage path is distinct from the central storage path + // to avoid any accidental central storage path deletion + return; + } - if (is_dir($path) && ! $tenantPathIsCentral) { - File::deleteDirectory($path); + if (is_dir($tenantStoragePath)) { + File::deleteDirectory($tenantStoragePath); } } } diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index d0c8af592..449ee1f41 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -13,7 +13,6 @@ use Stancl\Tenancy\Jobs\CreateStorageSymlinks; use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; -use Stancl\Tenancy\Jobs\CreateTenantStorage; use Stancl\Tenancy\Jobs\DeleteTenantStorage; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; @@ -185,27 +184,6 @@ $this->assertDirectoryDoesNotExist(public_path("public-$tenantKey")); }); -test('tenant storage gets created when TenantCreated listens to CreateTenantStorage', function() { - config([ - 'tenancy.bootstrappers' => [ - FilesystemTenancyBootstrapper::class, - ], - ]); - - Event::listen(TenantCreated::class, - JobPipeline::make([CreateTenantStorage::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->shouldBeQueued(false)->toListener() - ); - - $centralStoragePath = storage_path(); - $tenant = Tenant::create(); - $suffixBase = config('tenancy.filesystem.suffix_base', 'tenant'); - $tenantStoragePath = $centralStoragePath . '/' . $suffixBase . $tenant->getTenantKey(); - - $this->assertDirectoryExists($tenantStoragePath . '/framework/cache'); -}); - test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { From 3f9f7cda0b74940d43192b7b0420ff9d198eed29 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Mon, 20 Apr 2026 19:35:27 +0200 Subject: [PATCH 9/9] make test more clear --- .../FilesystemTenancyBootstrapperTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 449ee1f41..4e8349172 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -184,7 +184,7 @@ $this->assertDirectoryDoesNotExist(public_path("public-$tenantKey")); }); -test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() { +test('tenant storage gets deleted during tenant deletion when the DeletingTenant pipeline contains DeleteTenantStorage', function() { Event::listen(DeletingTenant::class, JobPipeline::make([DeleteTenantStorage::class])->send(function (DeletingTenant $event) { return $event->tenant; @@ -199,10 +199,10 @@ // the storage deletion will be skipped. $tenantStoragePath = storage_path(); expect($tenantStoragePath)->toBe($centralStoragePath); - expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + expect(File::isDirectory($centralStoragePath))->toBeTrue(); tenant()->delete(); - expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + expect(File::isDirectory($centralStoragePath))->toBeTrue(); config([ 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], @@ -218,10 +218,10 @@ // because suffix_storage_path is false. // The storage deletion will be skipped. expect($tenantStoragePath)->toBe($centralStoragePath); - expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + expect(File::isDirectory($centralStoragePath))->toBeTrue(); tenant()->delete(); - expect(File::isDirectory($tenantStoragePath))->toBeTrue(); + expect(File::isDirectory($centralStoragePath))->toBeTrue(); config([ 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], @@ -231,12 +231,16 @@ tenancy()->initialize(Tenant::create()); $tenantStoragePath = storage_path(); - expect($centralStoragePath)->not()->toBe($tenantStoragePath); + // FilesystemTenancyBootstrapper enabled, + // suffix_storage_path enabled, so the two paths are distinct. + // Tenant storage will be deleted. + expect($tenantStoragePath)->not()->toBe($centralStoragePath); expect(File::isDirectory($tenantStoragePath))->toBeTrue(); tenant()->delete(); expect(File::isDirectory($tenantStoragePath))->toBeFalse(); + expect(File::isDirectory($centralStoragePath))->toBeTrue(); }); test('the framework/cache directory is created when storage_path is scoped', function (bool $suffixStoragePath) {