diff --git a/src/Bootstrappers/DatabaseCacheBootstrapper.php b/src/Bootstrappers/DatabaseCacheBootstrapper.php index 55a6b6e4..13e08bfa 100644 --- a/src/Bootstrappers/DatabaseCacheBootstrapper.php +++ b/src/Bootstrappers/DatabaseCacheBootstrapper.php @@ -13,40 +13,73 @@ use Stancl\Tenancy\Contracts\Tenant; * This bootstrapper allows cache to be stored in the tenant databases by switching * the database cache store's (and cache locks) connection. * - * Intended to be used with the 'database' cache store, instead of CacheTenancyBootstrapper. + * Intended to be used with database driver-based cache stores, instead of CacheTenancyBootstrapper. * - * On bootstrap(), the database cache store's connection is set to 'tenant' - * and the database cache store is purged from the CacheManager's resolved stores. - * This forces the manager to resolve a new instance of the database store created with the 'tenant' DB connection on the next cache operation. + * On bootstrap(), all database cache stores' connections are set to 'tenant' + * and the database cache stores are purged from the CacheManager's resolved stores. + * This forces the manager to resolve new instances of the database stores created with the 'tenant' DB connection on the next cache operation. * - * On revert(), the cache store's connection is reverted to the originally used one (usually 'central'), and again, - * the database cache store is purged from the CacheManager's resolved stores so that the originally used one is resolved on the next cache operation. + * On revert(), the cache stores' connections are reverted to the originally used ones (usually 'central'), and again, + * the database cache stores are purged from the CacheManager's resolved stores so that the originally used ones are resolved on the next cache operation. */ class DatabaseCacheBootstrapper implements TenancyBootstrapper { + /** + * Cache stores to process. If null, all stores with 'database' driver will be processed. + * If array, only the specified stores will be processed (with driver validation). + */ + public static array|null $stores = null; + public function __construct( protected Repository $config, protected CacheManager $cache, - protected string|null $originalConnection = null, - protected string|null $originalLockConnection = null + protected array $originalConnections = [], + protected array $originalLockConnections = [] ) {} public function bootstrap(Tenant $tenant): void { - $this->originalConnection = $this->config->get('cache.stores.database.connection'); - $this->originalLockConnection = $this->config->get('cache.stores.database.lock_connection'); + $stores = $this->getDatabaseCacheStores(); - $this->config->set('cache.stores.database.connection', 'tenant'); - $this->config->set('cache.stores.database.lock_connection', 'tenant'); + 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->cache->purge('database'); + $this->config->set("cache.stores.{$storeName}.connection", 'tenant'); + $this->config->set("cache.stores.{$storeName}.lock_connection", 'tenant'); + + $this->cache->purge($storeName); + } } public function revert(): void { - $this->config->set('cache.stores.database.connection', $this->originalConnection); - $this->config->set('cache.stores.database.lock_connection', $this->originalLockConnection); + foreach ($this->originalConnections as $storeName => $originalConnection) { + $this->config->set("cache.stores.{$storeName}.connection", $originalConnection); + $this->config->set("cache.stores.{$storeName}.lock_connection", $this->originalLockConnections[$storeName]); - $this->cache->purge('database'); + $this->cache->purge($storeName); + } + } + + /** + * Get the names of cache stores that use the database driver. + */ + protected function getDatabaseCacheStores(): array + { + // Get all stores specified in the static $stores property. + // If they don't have the database driver, ignore them. + if (static::$stores !== null) { + return array_filter(static::$stores, function ($storeName) { + $store = $this->config->get("cache.stores.{$storeName}"); + + return $store && ($store['driver'] ?? null) === 'database'; + }); + } + + // Get all stores with database driver if $stores is null + return array_keys(array_filter($this->config->get('cache.stores', []), function ($store) { + return ($store['driver'] ?? null) === 'database'; + })); } } diff --git a/tests/DbCacheBootstrapperTest.php b/tests/DbCacheBootstrapperTest.php index 5a0995cb..7463af93 100644 --- a/tests/DbCacheBootstrapperTest.php +++ b/tests/DbCacheBootstrapperTest.php @@ -143,3 +143,89 @@ test('cache is separated correctly when using DatabaseCacheBootstrapper', functi expect(Cache::get('foo'))->toBe('central'); expect($getCacheUsingDbQuery('foo'))->toContain('central'); }); + +test('DatabaseCacheBootstrapper auto-detects all database driver stores by default', function() { + // Configure multiple stores with different drivers + config([ + 'cache.stores.database' => [ + 'driver' => 'database', + 'connection' => 'central', + 'table' => 'cache', + ], + 'cache.stores.sessions' => [ + 'driver' => 'database', + 'connection' => 'central', + 'table' => 'sessions_cache', + ], + 'cache.stores.redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + 'cache.stores.file' => [ + 'driver' => 'file', + 'path' => '/foo/bar', + ], + ]); + + // Here, we're using auto-detection (default behavior) + expect(config('cache.stores.database.connection'))->toBe('central'); + expect(config('cache.stores.sessions.connection'))->toBe('central'); + expect(config('cache.stores.redis.connection'))->toBe('default'); + expect(config('cache.stores.file.path'))->toBe('/foo/bar'); + + tenancy()->initialize(Tenant::create()); + + // Using auto-detection (default behavior), + // all database driver stores should be configured, + // and stores with non-database drivers are ignored. + expect(config('cache.stores.database.connection'))->toBe('tenant'); + expect(config('cache.stores.sessions.connection'))->toBe('tenant'); + expect(config('cache.stores.redis.connection'))->toBe('default'); // unchanged + expect(config('cache.stores.file.path'))->toBe('/foo/bar'); // unchanged + + tenancy()->end(); + + // All database stores should be reverted, others unchanged + expect(config('cache.stores.database.connection'))->toBe('central'); + expect(config('cache.stores.sessions.connection'))->toBe('central'); + expect(config('cache.stores.redis.connection'))->toBe('default'); + expect(config('cache.stores.file.path'))->toBe('/foo/bar'); +}); + +test('manual $stores configuration takes precedence over auto-detection', function() { + // Configure multiple database stores + config([ + 'cache.stores.sessions' => [ + 'driver' => 'database', + 'connection' => 'central', + 'table' => 'sessions_cache', + ], + 'cache.stores.redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + ]); + + // Specific store overrides (including non-database stores) + DatabaseCacheBootstrapper::$stores = ['sessions', 'redis']; // Note: excludes 'database' + + expect(config('cache.stores.database.connection'))->toBe('central'); + expect(config('cache.stores.sessions.connection'))->toBe('central'); + expect(config('cache.stores.redis.connection'))->toBe('default'); + + tenancy()->initialize(Tenant::create()); + + // Manual config takes precedence: only 'sessions' is configured + // - redis filtered out by driver check + // - database store not included in $stores + expect(config('cache.stores.database.connection'))->toBe('central'); // Excluded in manual config + expect(config('cache.stores.sessions.connection'))->toBe('tenant'); // Included and is database driver + expect(config('cache.stores.redis.connection'))->toBe('default'); // Included but filtered out (not database driver) + + tenancy()->end(); + + // Only the manually configured stores' config will be reverted + expect(config('cache.stores.database.connection'))->toBe('central'); + expect(config('cache.stores.sessions.connection'))->toBe('central'); + expect(config('cache.stores.redis.connection'))->toBe('default'); +});