From cab8ecebeced03306d4121eb7a397d37310278dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 4 Nov 2025 21:16:39 +0100 Subject: [PATCH] Create tenant storage directories in FilesystemTenancyBootstrapper (#1410) This is because the CreateTenantStorage listener only runs when a tenant is created, but in multi-server setups the directory may need to be created each time a tenant is *used*, not just created. Also changed the listeners to use TenantEvent instead of specific events, to make it possible to use them with other events, such as TenancyBootstrapped. Also update permission bits in a few mkdir() calls to better scope data to the current OS user. Also fix a typo in CacheTenancyBootstrapper (exception message). --- .../CacheTenancyBootstrapper.php | 2 +- .../FilesystemTenancyBootstrapper.php | 11 +++++++++- src/Listeners/CreateTenantStorage.php | 13 +++++++++--- src/Listeners/DeleteTenantStorage.php | 4 ++-- .../FilesystemTenancyBootstrapperTest.php | 21 +++++++++++++++++++ tests/SessionSeparationTest.php | 1 + 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Bootstrappers/CacheTenancyBootstrapper.php b/src/Bootstrappers/CacheTenancyBootstrapper.php index 20e09816..9d87e19a 100644 --- a/src/Bootstrappers/CacheTenancyBootstrapper.php +++ b/src/Bootstrappers/CacheTenancyBootstrapper.php @@ -108,7 +108,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper // Previously we just silently ignored this, however since session scoping is of high importance // in production, we make sure to notify the developer, by throwing an exception, that session // scoping isn't happening as expected/configured due to an incompatible session driver. - throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_session'); + throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_sessions'); } } else { // Scoping sessions using this bootstrapper implicitly adds the session store to $names diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index d5088c5c..2c2d9ec9 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -78,6 +78,15 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper return; } + $path = $suffix + ? $this->tenantStoragePath($suffix) . '/framework/cache' + : $this->originalStoragePath . '/framework/cache'; + + if (! is_dir($path)) { + // Create tenant framework/cache directory if it does not exist + mkdir($path, 0750, true); + } + if ($suffix === false) { $this->app->useStoragePath($this->originalStoragePath); } else { @@ -211,7 +220,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if (! is_dir($path)) { // Create tenant framework/sessions directory if it does not exist - mkdir($path, 0755, true); + mkdir($path, 0750, true); } $this->app['config']['session.files'] = $path; diff --git a/src/Listeners/CreateTenantStorage.php b/src/Listeners/CreateTenantStorage.php index 73da89fc..3bebb731 100644 --- a/src/Listeners/CreateTenantStorage.php +++ b/src/Listeners/CreateTenantStorage.php @@ -4,18 +4,25 @@ declare(strict_types=1); namespace Stancl\Tenancy\Listeners; -use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\Contracts\TenantEvent; +/** + * Can be used to manually create framework directories in the tenant storage when storage_path() is scoped. + * + * Useful when using real-time facades which use the framework/cache directory. + * + * Generally not needed anymore as the directory is also created by the FilesystemTenancyBootstrapper. + */ class CreateTenantStorage { - public function handle(TenantCreated $event): void + public function handle(TenantEvent $event): void { $storage_path = tenancy()->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, 0777, true); + mkdir($cache_path, 0750, true); } } } diff --git a/src/Listeners/DeleteTenantStorage.php b/src/Listeners/DeleteTenantStorage.php index 25adc4f4..ec360073 100644 --- a/src/Listeners/DeleteTenantStorage.php +++ b/src/Listeners/DeleteTenantStorage.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Listeners; use Illuminate\Support\Facades\File; -use Stancl\Tenancy\Events\DeletingTenant; +use Stancl\Tenancy\Events\Contracts\TenantEvent; class DeleteTenantStorage { - public function handle(DeletingTenant $event): void + public function handle(TenantEvent $event): void { $path = tenancy()->run($event->tenant, fn () => storage_path()); diff --git a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php index 857e0eac..706a7882 100644 --- a/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php @@ -200,3 +200,24 @@ test('tenant storage can get deleted after the tenant when DeletingTenant listen expect(File::isDirectory($tenantStoragePath))->toBeFalse(); }); + +test('the framework/cache directory is created when storage_path is scoped', function (bool $suffixStoragePath) { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + ], + 'tenancy.filesystem.suffix_storage_path' => $suffixStoragePath + ]); + + $centralStoragePath = storage_path(); + + tenancy()->initialize($tenant = Tenant::create()); + + if ($suffixStoragePath) { + expect(storage_path('framework/cache'))->toBe($centralStoragePath . "/tenant{$tenant->id}/framework/cache"); + expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeTrue(); + } else { + expect(storage_path('framework/cache'))->toBe($centralStoragePath . '/framework/cache'); + expect(is_dir($centralStoragePath . "/tenant{$tenant->id}/framework/cache"))->toBeFalse(); + } +})->with([true, false]); diff --git a/tests/SessionSeparationTest.php b/tests/SessionSeparationTest.php index 02b018d1..d699bc61 100644 --- a/tests/SessionSeparationTest.php +++ b/tests/SessionSeparationTest.php @@ -56,6 +56,7 @@ test('file sessions are separated', function (bool $scopeSessions) { if ($scopeSessions) { expect($sessionPath())->toBe(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions')); + expect(is_dir(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions')))->toBeTrue(); } else { expect($sessionPath())->toBe(storage_path('framework/sessions')); }