1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-08-06 20:34:05 +00:00
tenancy/tests/GlobalCacheTest.php
lukinovec ecf031237d
Make globalCache always use central conn with DB cache stores (#1462)
`globalCache` should always use the central connection, but when using a
`database`-driver cache store with `DatabaseTenancyBootstrapper`, it
does not (with the exception of `DatabaseCacheBootstrapper`, explained
below).
  
`globalCache` creates a fresh `CacheManager` each time it's resolved
(it's a `bind`, not a `singleton`). A freshly-created manager builds its
database stores using the current default DB connection. When
`DatabaseTenancyBootstrapper` is active, that default is `tenant`. So
`globalCache` in tenant context points at the tenant DB. Specifically,
`CachedTenantResolver` stores cached tenant lookups via `globalCache`.
When a domain is deleted in tenant context, the invalidation logic calls
`globalCache->forget(...)`, but that hits the tenant DB, while the
resolver cache entry is in the central DB. `globalCache->forget(...)`
doesn't actually do anything in that case.
  
With `DatabaseCacheBootstrapper`, this is already handled. `globalCache`
is always central because it sets
`TenancyServiceProvider::$adjustCacheManagerUsing` to a callback that
explicitly restores the central connection on `globalCache`'s stores.
  
To fix this, after constructing the fresh CacheManager in the
globalCache binding, explicitly set the connection of every
database-driver store to its configured connection value, **falling back
to central_connection** when the config value is null (null value =
inherits whatever the current default DB connection is).
  
This is sufficient for `CacheTenancyBootstrapper` and any other
bootstrapper that doesn't explicitly set the store's DB connection. For
`DatabaseCacheBootstrapper` specifically, this alone is not enough since
it explicitly sets the store's config connection to 'tenant'. That's why
DatabaseCacheBootstrapper's `$adjustCacheManagerUsing` callback runs
after and overrides those stores back to the original (central)
connection.

> In short: `makeDatabaseCacheStoresCentral()` handles stores with a
`null` connection config (falls back to central).
`$adjustCacheManagerUsing` handles the `DatabaseCacheBootstrapper` case
where the config is explicitly set to 'tenant'.
  
Added datasets that use the database cache store +
CacheTenancyBootstrapper to the relevant tests (globalCache and
invalidation) to test regression
(0cf7043b73),
and the changes mentioned above
(https://github.com/archtechx/tenancy/pull/1462/commits/5e65c67ea0daf98f57f2a6a7b0e1937bbc397a56)
make these tests pass.

---------

Co-authored-by: Samuel Stancl <samuel@archte.ch>
2026-06-27 18:30:01 -07:00

211 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Facades\GlobalCache;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use function Stancl\Tenancy\Tests\withCacheTables;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function () {
config([
'cache.default' => 'redis',
'tenancy.cache.stores' => ['redis'],
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
withCacheTables();
});
test('global cache manager stores data in global cache', function (string $store, array $bootstrappers) {
config([
'cache.default' => $store,
'tenancy.bootstrappers' => $bootstrappers,
]);
if ($store === 'database') withTenantDatabases(true);
expect(cache('foo'))->toBe(null);
GlobalCache::put('foo', 'bar');
expect(GlobalCache::get('foo'))->toBe('bar');
$tenant1 = Tenant::create();
tenancy()->initialize($tenant1);
expect(GlobalCache::get('foo'))->toBe('bar');
GlobalCache::put('abc', 'xyz');
cache(['def' => 'ghi']);
expect(cache('def'))->toBe('ghi');
// different stores
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
if ($store === 'redis') {
// same underlying connection. the prefix is set ON THE STORE
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
} else {
// different connections
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
}
tenancy()->end();
expect(GlobalCache::get('abc'))->toBe('xyz');
expect(GlobalCache::get('foo'))->toBe('bar');
expect(cache('def'))->toBe(null);
$tenant2 = Tenant::create();
tenancy()->initialize($tenant2);
expect(GlobalCache::get('abc'))->toBe('xyz');
expect(GlobalCache::get('foo'))->toBe('bar');
expect(cache('def'))->toBe(null);
cache(['def' => 'xxx']);
expect(cache('def'))->toBe('xxx');
tenancy()->initialize($tenant1);
expect(cache('def'))->toBe('ghi');
})->with([
['redis', [CacheTagsBootstrapper::class]],
['redis', [CacheTenancyBootstrapper::class]],
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
]);
test('global cache facade is not persistent', function () {
$oldId = spl_object_id(GlobalCache::getFacadeRoot());
$_ = new class {};
expect(spl_object_id(GlobalCache::getFacadeRoot()))->not()->toBe($oldId);
});
test('global cache is always central', function (string $store, array $bootstrappers, string $initialCentralCall) {
config([
'cache.default' => $store,
'tenancy.bootstrappers' => $bootstrappers,
]);
if ($store === 'database') {
withTenantDatabases(true);
}
// This tells us which "accessor" for the global cache should be instantiated first, before we go
// into the tenant context. We make sure to not touch the other one here. This tests that whether
// a particular accessor is used "early" makes no difference in the later behavior.
if ($initialCentralCall === 'helper') {
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
global_cache()->put('central-helper', true);
} else if ($initialCentralCall === 'facade') {
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
GlobalCache::put('central-facade', true);
} else if ($initialCentralCall === 'both') {
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
global_cache()->put('central-helper', true);
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
GlobalCache::put('central-facade', true);
}
$tenant = Tenant::create();
$tenant->enter();
// Here we use both the helper and the facade to ensure the value is accessible via either one
if ($initialCentralCall === 'helper') {
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
expect(global_cache('central-helper'))->toBe(true);
expect(GlobalCache::get('central-helper'))->toBe(true);
} else if ($initialCentralCall === 'facade') {
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
expect(global_cache('central-facade'))->toBe(true);
expect(GlobalCache::get('central-facade'))->toBe(true);
} else if ($initialCentralCall === 'both') {
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
expect(global_cache('central-helper'))->toBe(true);
expect(GlobalCache::get('central-helper'))->toBe(true);
expect(global_cache('central-facade'))->toBe(true);
expect(GlobalCache::get('central-facade'))->toBe(true);
}
global_cache()->put('tenant-helper', true);
GlobalCache::put('tenant-facade', true);
tenancy()->end();
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
expect(global_cache('tenant-helper'))->toBe(true);
expect(GlobalCache::get('tenant-helper'))->toBe(true);
expect(global_cache('tenant-facade'))->toBe(true);
expect(GlobalCache::get('tenant-facade'))->toBe(true);
if ($initialCentralCall === 'helper') {
expect(GlobalCache::get('central-helper'))->toBe(true);
} else if ($initialCentralCall === 'facade') {
expect(global_cache('central-facade'))->toBe(true);
} else if ($initialCentralCall === 'both') {
expect(global_cache('central-helper'))->toBe(true);
expect(GlobalCache::get('central-helper'))->toBe(true);
expect(global_cache('central-facade'))->toBe(true);
expect(GlobalCache::get('central-facade'))->toBe(true);
}
})->with([
['redis', [CacheTagsBootstrapper::class]],
['redis', [CacheTenancyBootstrapper::class]],
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
['database', [DatabaseTenancyBootstrapper::class, CacheTenancyBootstrapper::class]],
])->with([
'helper',
'facade',
'both',
'none',
]);
test('the global_cache helper supports the same syntax as the cache helper', function (string $store, array $bootstrappers) {
config([
'cache.default' => $store,
'tenancy.bootstrappers' => $bootstrappers,
]);
if ($store === 'database') withTenantDatabases(true);
$tenant = Tenant::create();
$tenant->enter();
// different stores
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
if ($store === 'redis') {
// same underlying connection. the prefix is set ON THE STORE
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
} else {
// different connections
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
}
expect(cache('foo'))->toBe(null); // tenant cache is empty
global_cache(['foo' => 'bar']);
expect(global_cache('foo'))->toBe('bar');
global_cache()->set('foo', 'baz');
expect(global_cache()->get('foo'))->toBe('baz');
expect(cache('foo'))->toBe(null); // tenant cache is not affected
})->with([
['redis', [CacheTagsBootstrapper::class]],
['redis', [CacheTenancyBootstrapper::class]],
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
]);