1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-13 19:24:03 +00:00
tenancy/src/Bootstrappers/CacheTenancyBootstrapper.php
Samuel Štancl eecf6f21c8
Cache prefixing logic rewrite, session scoping improvements, tests refactor (#43)
* Run cache tests on all supported drivers

* update ci healthcheck for memcached

* remove memcached healthcheck

* fix typos in test comments, expand internal.md [ci skip]

* add empty line [ci skip]

* switch to using $store->setPrefix()

* add dynamodb

* refactor try-finally to try-catch

* remove unnecessary clearResolvedInstances() call

* add dual Cache:: and cache() assertions

* add apc

* Flush APCu cache in test setup

* Revert "add dual Cache:: and cache() assertions"

This reverts commit a0bab162fbe2dd0d25e7056ceca4fb7ce54efc77.

* phpstan fix

* Add logic for scoping 'file' disks to FilesystemTenancyBootstrapper

* minor changes, add todos

* refactor how the session.connection is used in the DB session bootstrapper

* add session forgery prevention logic to the db session bootstrapper

* only use the fs bootstrapper for file disk in 'cache data is separated' dataset

* minor session scoping test changes

* Add session scoping logic to FilesystemTenancyBootstrapper, correctly update disk roots even with storage_path_tenancy disabled

* Fix code style (php-cs-fixer)

* update docblock

* make not-null check more explicit

* separate bootstrapper tests, fix swapped test names for two tests

* refactor cache bootstrapper tests

* resolve global cache todo

* expand tests: session separation tests, more filesystem separation assertions; change prefix_base-type config keys to templates/formats

* add apc session scoping test, various session separation bugfixes

* phpstan + minor logic fixes

* prefix_format -> prefix

* fix database session separation test

* revert composer.json changes, update laravel dependencies to expected next release

* only run session scoping logic in cache bootstrapper for redis, memcached, dynamodb, apc; update gitattributes

* tenancy.central_domains -> tenancy.identification.central_domains

* db session separation test: add datasets

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-04-09 20:40:27 +02:00

154 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Closure;
use Exception;
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Session\SessionManager;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
/**
* Makes cache tenant-aware by applying a prefix.
*/
class CacheTenancyBootstrapper implements TenancyBootstrapper
{
/** @var Closure(Tenant, string): string */
public static Closure|null $prefixGenerator = null;
/** @var array<string, string> */
protected array $originalPrefixes = [];
public function __construct(
protected ConfigRepository $config,
protected CacheManager $cache,
protected SessionManager $session,
) {}
public function bootstrap(Tenant $tenant): void
{
foreach ($this->getCacheStores() as $name) {
$store = $this->cache->driver($name)->getStore();
$this->originalPrefixes[$name] = $store->getPrefix();
$this->setCachePrefix($store, $this->generatePrefix($tenant, $name));
}
if ($this->shouldScopeSessions()) {
$name = $this->getSessionCacheStoreName();
$handler = $this->session->driver()->getHandler();
if ($handler instanceof CacheBasedSessionHandler) {
// The CacheBasedSessionHandler is constructed with a *clone* of
// an existing cache store, so we need to set the prefix separately.
$store = $handler->getCache()->getStore();
// We also don't need to set the original prefix, since the cache store
// is implicitly added to the configured cache stores when session scoping
// is enabled.
$this->setCachePrefix($store, $this->generatePrefix($tenant, $name));
}
}
}
public function revert(): void
{
foreach ($this->getCacheStores() as $name) {
$store = $this->cache->driver($name)->getStore();
$this->setCachePrefix($store, $this->originalPrefixes[$name]);
}
if ($this->shouldScopeSessions()) {
$name = $this->getSessionCacheStoreName();
$handler = $this->session->driver()->getHandler();
if ($handler instanceof CacheBasedSessionHandler) {
$store = $handler->getCache()->getStore();
$this->setCachePrefix($store, $this->originalPrefixes[$name]);
}
}
}
protected function getSessionCacheStoreName(): string
{
return $this->config->get('session.store') ?? $this->config->get('session.driver');
}
protected function shouldScopeSessions(): bool
{
// We don't want to scope sessions if:
// 1. The user has disabled session scoping via this bootstrapper, AND
// 2. The session driver hasn't been instantiated yet (if this is the case,
// it will be instantiated later by cloning an existing cache store
// that will have already been prefixed in this bootstrapper).
return $this->config->get('tenancy.cache.scope_sessions', true)
&& count($this->session->getDrivers()) !== 0;
}
/** @return string[] */
protected function getCacheStores(): array
{
$names = $this->config->get('tenancy.cache.stores');
if (
$this->config->get('tenancy.cache.scope_sessions', true) &&
in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)
) {
$names[] = $this->getSessionCacheStoreName();
}
$names = array_unique($names);
return array_filter($names, function ($name) {
$store = $this->config->get("cache.stores.{$name}");
if ($store === null || $store['driver'] === 'file') {
return false;
}
if ($store['driver'] === 'array') {
throw new Exception('Cache store [' . $name . '] is not supported by this bootstrapper.');
}
return true;
});
}
protected function setCachePrefix(Store $store, string|null $prefix): void
{
if (! method_exists($store, 'setPrefix')) {
throw new Exception('Cache store [' . get_class($store) . '] does not support setting a prefix.');
}
$store->setPrefix($prefix);
}
public function generatePrefix(Tenant $tenant, string $store): string
{
return static::$prefixGenerator
? (static::$prefixGenerator)($tenant, $store)
: $this->originalPrefixes[$store] . str($this->config->get('tenancy.cache.prefix'))
->replace('%tenant%', (string) $tenant->getTenantKey())->toString();
}
/**
* Set a custom prefix generator.
*
* The first argument is the tenant, the second argument is the cache store name.
*
* @param Closure(Tenant, string): string $prefixGenerator
*/
public static function generatePrefixUsing(Closure $prefixGenerator): void
{
static::$prefixGenerator = $prefixGenerator;
}
}