mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 11:14:04 +00:00
* Initial implementation (lukinovec) * Make sure DatabaseCacheBootstrapper runs after DatabaseTenancyBootstrapper, misc wip changes * Fix withTenantDatabases() * Add failing test (GlobalCacheTest) * Configure globalCache's DB stores to use central connection instead of default connection every time it's reinstantiated * Make GlobalCache facade not cached. Even though it wasn't causing issues in our existing tests, it likely was flaky, and making it not $cached makes it now consistent with global_cache() - always getting a new CacheManager from the globalCache container binding * Add database connection assertions in GlobalCacheTest * Run all cached resolver/global cache tests with DatabaseCacheBootstrapper * Reset adjustCacheManagerUsing in revert() and TestCase * Reset static $stores property * Finalize GlobalCache-related changes * tests: remove pointless cache TTLs * Refactor DatabaseCacheBootstrapper * Refactor tests Co-authored-by: lukinovec <lukinovec@gmail.com>
This commit is contained in:
parent
3984d64cfa
commit
ecc3374293
14 changed files with 600 additions and 38 deletions
123
src/Bootstrappers/DatabaseCacheBootstrapper.php
Normal file
123
src/Bootstrappers/DatabaseCacheBootstrapper.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?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");
|
||||
$this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection");
|
||||
|
||||
$this->config->set("cache.stores.{$storeName}.connection", 'tenant');
|
||||
$this->config->set("cache.stores.{$storeName}.lock_connection", 'tenant');
|
||||
|
||||
$this->cache->purge($storeName);
|
||||
}
|
||||
|
||||
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] ?? config('tenancy.database.central_connection'),
|
||||
'lockConnection' => $this->originalLockConnections[$storeName] ?? config('tenancy.database.central_connection'),
|
||||
], $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]);
|
||||
|
||||
$this->cache->purge($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';
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Cache;
|
|||
|
||||
class GlobalCache extends Cache
|
||||
{
|
||||
/** Make sure this works identically to global_cache() */
|
||||
protected static $cached = false;
|
||||
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'globalCache';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
// globalCache should generally not be injected, however in this case
|
||||
// the class is always created from scratch when calling invalidateCache()
|
||||
// meaning the global cache stores are also resolved from scratch.
|
||||
$this->cache = $app->make('globalCache')->store(static::cacheStore());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
public static bool $registerForgetTenantParameterListener = true;
|
||||
public static bool $migrateFreshOverride = true;
|
||||
|
||||
/** @internal */
|
||||
public static Closure|null $adjustCacheManagerUsing = null;
|
||||
|
||||
/* Register services. */
|
||||
public function register(): void
|
||||
{
|
||||
|
|
@ -81,7 +84,29 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
});
|
||||
|
||||
$this->app->bind('globalCache', function ($app) {
|
||||
return new CacheManager($app);
|
||||
// We create a separate CacheManager to be used for "global" cache -- cache that
|
||||
// is always central, regardless of the current context.
|
||||
//
|
||||
// Importantly, we use a regular binding here, not a singleton. Thanks to that,
|
||||
// any time we resolve this cache manager, we get a *fresh* instance -- an instance
|
||||
// that was not affected by any scoping logic.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// For that reason, we also adjust the relevant stores on this new CacheManager
|
||||
// using the callback below. It is set by DatabaseCacheBootstrapper.
|
||||
$manager = new CacheManager($app);
|
||||
|
||||
if (static::$adjustCacheManagerUsing !== null) {
|
||||
(static::$adjustCacheManagerUsing)($manager);
|
||||
}
|
||||
|
||||
return $manager;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue