From 0cf7043b733848b6d139de967df676e30247323e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 1 Jun 2026 13:36:18 +0200 Subject: [PATCH 1/4] Add datasets to globalCache/invalidation tests as regression --- tests/CachedTenantResolverTest.php | 1 + tests/GlobalCacheTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index 920c95a1b..fc6cfb798 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -165,6 +165,7 @@ ['redis', [CacheTenancyBootstrapper::class]], ['redis', [CacheTagsBootstrapper::class]], ['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]], + ['database', [DatabaseTenancyBootstrapper::class, CacheTenancyBootstrapper::class]], ]); test('cache is invalidated when the tenant is deleted', function (string $resolver, bool $configureTenantModelColumn) { diff --git a/tests/GlobalCacheTest.php b/tests/GlobalCacheTest.php index 016ad2a48..4cda8b742 100644 --- a/tests/GlobalCacheTest.php +++ b/tests/GlobalCacheTest.php @@ -165,6 +165,7 @@ ['redis', [CacheTagsBootstrapper::class]], ['redis', [CacheTenancyBootstrapper::class]], ['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]], + ['database', [DatabaseTenancyBootstrapper::class, CacheTenancyBootstrapper::class]], ])->with([ 'helper', 'facade', From 5e65c67ea0daf98f57f2a6a7b0e1937bbc397a56 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 1 Jun 2026 13:47:52 +0200 Subject: [PATCH 2/4] Make globalCache always use the central connection --- src/TenancyServiceProvider.php | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index afd20fb61..aeaa08559 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -6,9 +6,11 @@ use Closure; use Illuminate\Cache\CacheManager; +use Illuminate\Cache\DatabaseStore; use Illuminate\Contracts\Container\Container; use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Routing\Events\RouteMatched; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; @@ -94,6 +96,15 @@ public function register(): void // using the callback below. It is set by DatabaseCacheBootstrapper. $manager = new CacheManager($app); + // Make globalCache use either the configured non-null connection, + // or fall back to the central connection. + $this->makeDatabaseCacheStoresCentral($manager); + + // If a bootstrapper (like DatabaseCacheBootstrapper) makes the + // cache connection tenant explicitly, the makeDatabaseCacheStoresCentral() + // call ends up setting the tenant connection rather than the central one, + // and the $adjustCacheManagerUsing callback is needed to + // make globalCache use the central connection. if (static::$adjustCacheManagerUsing !== null) { (static::$adjustCacheManagerUsing)($manager); } @@ -102,6 +113,34 @@ public function register(): void }); } + /** + * Ensure globalCache uses the central connection for database cache stores. + * + * A freshly built CacheManager creates database stores using the current default connection, which + * DatabaseTenancyBootstrapper switches to the tenant connection. Since global cache should always be + * central, reset those stores back to their configured connection, falling back to the central one. + */ + protected function makeDatabaseCacheStoresCentral(CacheManager $manager): void + { + $centralConnection = $this->app['config']['tenancy.database.central_connection']; + + foreach ($this->app['config']['cache.stores'] ?? [] as $name => $store) { + $notAValidDatabaseStore = ! is_array($store) || ($store['driver'] ?? null) !== 'database'; + + if ($notAValidDatabaseStore) { + continue; + } + + /** @var DatabaseStore $databaseStore */ + $databaseStore = $manager->store($name)->getStore(); + + // If $store['connection'] is null, it defaults to the default DB connection (which may be tenant). + // Fall back to the central connection to keep the global cache central. + $databaseStore->setConnection(DB::connection($store['connection'] ?? $centralConnection)); + $databaseStore->setLockConnection(DB::connection($store['lock_connection'] ?? $store['connection'] ?? $centralConnection)); + } + } + /* Bootstrap services. */ public function boot(): void { From 20036ef1ff051d938de84f206e1c1ef03a08c8eb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 1 Jun 2026 15:38:27 +0200 Subject: [PATCH 3/4] Clean up $adjustCacheManagerUsing in before and afterEach --- tests/CachedTenantResolverTest.php | 2 ++ tests/GlobalCacheTest.php | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index fc6cfb798..26c4f8753 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -24,12 +24,14 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Resolvers\RequestDataTenantResolver; +use Stancl\Tenancy\TenancyServiceProvider; use function Stancl\Tenancy\Tests\pest; use function Stancl\Tenancy\Tests\withCacheTables; use function Stancl\Tenancy\Tests\withTenantDatabases; beforeEach($cleanup = function () { Tenant::$extraCustomColumns = []; + TenancyServiceProvider::$adjustCacheManagerUsing = null; }); afterEach($cleanup); diff --git a/tests/GlobalCacheTest.php b/tests/GlobalCacheTest.php index 4cda8b742..2bf0e3c54 100644 --- a/tests/GlobalCacheTest.php +++ b/tests/GlobalCacheTest.php @@ -13,11 +13,14 @@ use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\TenancyServiceProvider; use function Stancl\Tenancy\Tests\withCacheTables; use function Stancl\Tenancy\Tests\withTenantDatabases; beforeEach(function () { + TenancyServiceProvider::$adjustCacheManagerUsing = null; + config([ 'cache.default' => 'redis', 'tenancy.cache.stores' => ['redis'], @@ -29,6 +32,10 @@ withCacheTables(); }); +afterEach(function () { + TenancyServiceProvider::$adjustCacheManagerUsing = null; +}); + test('global cache manager stores data in global cache', function (string $store, array $bootstrappers) { config([ 'cache.default' => $store, From 566f50440ae385b7cc75dde0bbc40361f315e92a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 5 Jun 2026 07:18:42 +0200 Subject: [PATCH 4/4] Improve comments --- .../DatabaseCacheBootstrapper.php | 5 +++- src/TenancyServiceProvider.php | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Bootstrappers/DatabaseCacheBootstrapper.php b/src/Bootstrappers/DatabaseCacheBootstrapper.php index 0e41849f7..0ec292663 100644 --- a/src/Bootstrappers/DatabaseCacheBootstrapper.php +++ b/src/Bootstrappers/DatabaseCacheBootstrapper.php @@ -25,7 +25,10 @@ * * Notably, this bootstrapper sets TenancyServiceProvider::$adjustCacheManagerUsing to a callback * that ensures all affected stores still use the central connection when accessed via global cache - * (typicaly the GlobalCache facade or global_cache() helper). + * (typically the GlobalCache facade or global_cache() helper), even though this bootstrapper explicitly + * sets the connection to tenant for all scoped cache stores. TenancyServiceProvider::makeDatabaseCacheStoresCentral() + * cannot fix globalCache on its own because it reads 'tenant' from config (set by this bootstrapper), not null, + * so the callback is still needed to correct the connection to central for globalCache. */ class DatabaseCacheBootstrapper implements TenancyBootstrapper { diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index aeaa08559..d0316ed93 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -88,23 +88,26 @@ public function register(): void // This works great for cache stores that are *directly* scoped, like Redis or // any other tagged or prefixed stores, but it doesn't work for the database driver. // - // When we use the DatabaseTenancyBootstrapper, it changes the default connection, - // and therefore the connection of the database store that will be created when - // this new CacheManager is instantiated again. + // When DatabaseTenancyBootstrapper is used, it changes the default DB connection + // to 'tenant'. A freshly created CacheManager would therefore instantiate database + // stores with the tenant connection. // - // For that reason, we also adjust the relevant stores on this new CacheManager - // using the callback below. It is set by DatabaseCacheBootstrapper. + // For that reason, we adjust the relevant stores on this new CacheManager + // using the makeDatabaseCacheStoresCentral() method and the $adjustCacheManagerUsing callback below + // (set by DatabaseCacheBootstrapper). $manager = new CacheManager($app); - // Make globalCache use either the configured non-null connection, - // or fall back to the central connection. + // When DatabaseTenancyBootstrapper is used, database stores whose 'connection' + // config is null fall back to the default DB connection ('tenant'). Reset each + // such store to its explicitly configured connection, or fall back to central. $this->makeDatabaseCacheStoresCentral($manager); - // If a bootstrapper (like DatabaseCacheBootstrapper) makes the - // cache connection tenant explicitly, the makeDatabaseCacheStoresCentral() - // call ends up setting the tenant connection rather than the central one, - // and the $adjustCacheManagerUsing callback is needed to - // make globalCache use the central connection. + // DatabaseCacheBootstrapper explicitly writes 'tenant' into each store's 'connection' + // config. makeDatabaseCacheStoresCentral() above would then read 'tenant' as the + // configured value (not null) and use it directly, so the central connection fallback + // wouldn't be used. + // + // This callback is used to correct those connections back to central for globalCache. if (static::$adjustCacheManagerUsing !== null) { (static::$adjustCacheManagerUsing)($manager); }