diff --git a/assets/config.php b/assets/config.php index ce74d3bf..f15a843a 100644 --- a/assets/config.php +++ b/assets/config.php @@ -48,6 +48,8 @@ return [ * SECURITY NOTE: Keep in mind that autoincrement IDs come with potential enumeration issues (such as tenant storage URLs). * * @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator + * @see \Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator + * @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDv7Generator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator @@ -311,7 +313,7 @@ return [ * * Note: This will implicitly add your configured session store to the list of prefixed stores above. */ - 'scope_sessions' => true, + 'scope_sessions' => in_array(env('SESSION_DRIVER'), ['redis', 'memcached', 'dynamodb', 'apc'], true), 'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call. ], diff --git a/src/Bootstrappers/CacheTenancyBootstrapper.php b/src/Bootstrappers/CacheTenancyBootstrapper.php index 20e09816..97bd7d24 100644 --- a/src/Bootstrappers/CacheTenancyBootstrapper.php +++ b/src/Bootstrappers/CacheTenancyBootstrapper.php @@ -102,14 +102,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper if ($this->config->get('tenancy.cache.scope_sessions', true)) { // These are the only cache driven session backends (see Laravel's config/session.php) if (! in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)) { - if (app()->environment('production')) { - // We only throw this exception in prod to make configuration a little easier. Developers - // may have scope_sessions set to true while using different session drivers e.g. in tests. - // 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 $names[] = $this->getSessionCacheStoreName(); diff --git a/src/Bootstrappers/DatabaseCacheBootstrapper.php b/src/Bootstrappers/DatabaseCacheBootstrapper.php index ae547471..0e41849f 100644 --- a/src/Bootstrappers/DatabaseCacheBootstrapper.php +++ b/src/Bootstrappers/DatabaseCacheBootstrapper.php @@ -63,13 +63,17 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper $stores = $this->scopedStoreNames(); foreach ($stores as $storeName) { - $this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection"); - $this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection"); + $this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection") ?? config('tenancy.database.central_connection'); + $this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection") ?? config('tenancy.database.central_connection'); $this->config->set("cache.stores.{$storeName}.connection", 'tenant'); $this->config->set("cache.stores.{$storeName}.lock_connection", 'tenant'); - $this->cache->purge($storeName); + /** @var DatabaseStore $store */ + $store = $this->cache->store($storeName)->getStore(); + + $store->setConnection(DB::connection('tenant')); + $store->setLockConnection(DB::connection('tenant')); } if (static::$adjustGlobalCacheManager) { @@ -78,8 +82,8 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper // *from here* being executed repeatedly in a loop on reinitialization. For that reason we do not do that // (this is our only use of $adjustCacheManagerUsing anyway) but ideally at some point we'd have a better solution. $originalConnections = array_combine($stores, array_map(fn (string $storeName) => [ - 'connection' => $this->originalConnections[$storeName] ?? config('tenancy.database.central_connection'), - 'lockConnection' => $this->originalLockConnections[$storeName] ?? config('tenancy.database.central_connection'), + 'connection' => $this->originalConnections[$storeName], + 'lockConnection' => $this->originalLockConnections[$storeName], ], $stores)); TenancyServiceProvider::$adjustCacheManagerUsing = static function (CacheManager $manager) use ($originalConnections) { @@ -100,7 +104,11 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper $this->config->set("cache.stores.{$storeName}.connection", $originalConnection); $this->config->set("cache.stores.{$storeName}.lock_connection", $this->originalLockConnections[$storeName]); - $this->cache->purge($storeName); + /** @var DatabaseStore $store */ + $store = $this->cache->store($storeName)->getStore(); + + $store->setConnection(DB::connection($this->originalConnections[$storeName])); + $store->setLockConnection(DB::connection($this->originalLockConnections[$storeName])); } TenancyServiceProvider::$adjustCacheManagerUsing = null; 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/src/UniqueIdentifierGenerators/ULIDGenerator.php b/src/UniqueIdentifierGenerators/ULIDGenerator.php index 17b62898..d099c824 100644 --- a/src/UniqueIdentifierGenerators/ULIDGenerator.php +++ b/src/UniqueIdentifierGenerators/ULIDGenerator.php @@ -9,7 +9,7 @@ use Illuminate\Support\Str; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; /** - * Generates a UUID for the tenant key. + * Generates a ULID for the tenant key. */ class ULIDGenerator implements UniqueIdentifierGenerator { diff --git a/src/UniqueIdentifierGenerators/UUIDGenerator.php b/src/UniqueIdentifierGenerators/UUIDGenerator.php index f8bf4b9c..a537b666 100644 --- a/src/UniqueIdentifierGenerators/UUIDGenerator.php +++ b/src/UniqueIdentifierGenerators/UUIDGenerator.php @@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; /** - * Generates a UUID for the tenant key. + * Generates a UUIDv4 for the tenant key. */ class UUIDGenerator implements UniqueIdentifierGenerator { diff --git a/src/UniqueIdentifierGenerators/UUIDv7Generator.php b/src/UniqueIdentifierGenerators/UUIDv7Generator.php new file mode 100644 index 00000000..274b17b8 --- /dev/null +++ b/src/UniqueIdentifierGenerators/UUIDv7Generator.php @@ -0,0 +1,20 @@ +toString(); + } +} 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')); } diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index 4c6e77e1..8ee2ae78 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -23,6 +23,7 @@ use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator; +use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDv7Generator; use function Stancl\Tenancy\Tests\pest; @@ -94,6 +95,20 @@ test('ulid ids are supported', function () { expect($tenant2->id > $tenant1->id)->toBeTrue(); }); +test('uuidv7 ids are supported', function () { + app()->bind(UniqueIdentifierGenerator::class, UUIDv7Generator::class); + + $tenant1 = Tenant::create(); + expect($tenant1->id)->toBeString(); + expect(strlen($tenant1->id))->toBe(36); + + $tenant2 = Tenant::create(); + expect($tenant2->id)->toBeString(); + expect(strlen($tenant2->id))->toBe(36); + + expect($tenant2->id > $tenant1->id)->toBeTrue(); +}); + test('hex ids are supported', function () { app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class);