mirror of
https://github.com/archtechx/tenancy.git
synced 2026-09-20 13:54:03 +00:00
`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>
135 lines
6.2 KiB
PHP
135 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Stancl\Tenancy\Bootstrappers;
|
|
|
|
use Exception;
|
|
use Illuminate\Cache\CacheManager;
|
|
use Illuminate\Cache\DatabaseStore;
|
|
use Illuminate\Config\Repository;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
|
use Stancl\Tenancy\Contracts\Tenant;
|
|
use Stancl\Tenancy\TenancyServiceProvider;
|
|
|
|
/**
|
|
* This bootstrapper allows cache to be stored in tenant databases by switching the database
|
|
* connection used by cache stores that use the database driver.
|
|
*
|
|
* Can be used instead of CacheTenancyBootstrapper.
|
|
*
|
|
* By default, this bootstrapper scopes ALL cache stores that use the database driver. If you only
|
|
* want to scope SOME stores, set the static $stores property to an array of names of the stores
|
|
* you want to scope. Those stores must use 'database' as their driver.
|
|
*
|
|
* Notably, this bootstrapper sets TenancyServiceProvider::$adjustCacheManagerUsing to a callback
|
|
* that ensures all affected stores still use the central connection when accessed via global cache
|
|
* (typically the GlobalCache facade or global_cache() helper). The code in TenancyServiceProvider
|
|
* that uses `extend()` callbacks to make database stores on the global cache manager use the central
|
|
* connection only corrects stores scoped by the Database*Tenancy*Bootstrapper. This bootstrapper
|
|
* also changes the stores' connection in the *config* to 'tenant' which doesn't let that callback
|
|
* change the connection back to central on the global cache manager.
|
|
*/
|
|
class DatabaseCacheBootstrapper implements TenancyBootstrapper
|
|
{
|
|
/**
|
|
* Cache stores to scope.
|
|
*
|
|
* If null, all cache stores that use the database driver will be scoped.
|
|
* If an array, only the specified stores will be scoped. These all must use the database driver.
|
|
*/
|
|
public static array|null $stores = null;
|
|
|
|
/**
|
|
* Should scoped stores be adjusted on the global cache manager to use the central connection.
|
|
*
|
|
* You may want to set this to false if you don't use the built-in global cache and instead provide
|
|
* a list of stores to scope (static::$stores), with your own global store excluded that you then
|
|
* use manually. But in such a scenario you likely wouldn't be using global cache at all which means
|
|
* the callbacks for adjusting it wouldn't be executed in the first place.
|
|
*/
|
|
public static bool $adjustGlobalCacheManager = true;
|
|
|
|
public function __construct(
|
|
protected Repository $config,
|
|
protected CacheManager $cache,
|
|
protected array $originalConnections = [],
|
|
protected array $originalLockConnections = []
|
|
) {}
|
|
|
|
public function bootstrap(Tenant $tenant): void
|
|
{
|
|
if (! config('database.connections.tenant')) {
|
|
throw new Exception('DatabaseCacheBootstrapper must run after DatabaseTenancyBootstrapper.');
|
|
}
|
|
|
|
$stores = $this->scopedStoreNames();
|
|
|
|
foreach ($stores as $storeName) {
|
|
$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');
|
|
|
|
/** @var DatabaseStore $store */
|
|
$store = $this->cache->store($storeName)->getStore();
|
|
|
|
$store->setConnection(DB::connection('tenant'));
|
|
$store->setLockConnection(DB::connection('tenant'));
|
|
}
|
|
|
|
if (static::$adjustGlobalCacheManager) {
|
|
// Preferably we'd try to respect the original value of this static property -- store it in a variable,
|
|
// pull it into the closure, and execute it there. But such a naive approach would lead to existing callbacks
|
|
// *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],
|
|
'lockConnection' => $this->originalLockConnections[$storeName],
|
|
], $stores));
|
|
|
|
TenancyServiceProvider::$adjustCacheManagerUsing = static function (CacheManager $manager) use ($originalConnections) {
|
|
foreach ($originalConnections as $storeName => $connections) {
|
|
/** @var DatabaseStore $store */
|
|
$store = $manager->store($storeName)->getStore();
|
|
|
|
$store->setConnection(DB::connection($connections['connection']));
|
|
$store->setLockConnection(DB::connection($connections['lockConnection']));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
public function revert(): void
|
|
{
|
|
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]);
|
|
|
|
/** @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;
|
|
}
|
|
|
|
protected function scopedStoreNames(): array
|
|
{
|
|
return array_filter(
|
|
static::$stores ?? array_keys($this->config->get('cache.stores', [])),
|
|
function ($storeName) {
|
|
$store = $this->config->get("cache.stores.{$storeName}");
|
|
|
|
if (! $store) return false;
|
|
if (! isset($store['driver'])) return false;
|
|
|
|
return $store['driver'] === 'database';
|
|
}
|
|
);
|
|
}
|
|
}
|