1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-06-21 15:54:04 +00:00
tenancy/src/Bootstrappers/DatabaseCacheBootstrapper.php
Samuel Štancl 0ef4dfd230
DB cache bootstrapper: setConnection() instead of purge() (#1408)
By purging stores, we "detach" existing cache stores from the
CacheManager, making them impossible to adjust in the future.

We also unnecessarily recreate them on every tenancy bootstrap/revert.

A simpler case where this causes problems is defining a RateLimiter in
a service provider. That injects a single cache store into the
rate limiter singleton, which then becomes a completely independent
object after tenancy is initialized due to the purge. This in turn
means the central and tenant contexts share the rate limiter cache
instead of using separate caches as one would expect.
2025-11-04 15:47:15 +01:00

131 lines
5.8 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. These 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
* (typicaly the GlobalCache facade or global_cache() helper).
*/
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';
}
);
}
}