1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-04 08:44:04 +00:00
tenancy/src/Bootstrappers/CacheTenancyBootstrapper.php
Samuel Štancl aba7a50619
Minor fixes
The change in SQLiteDatabaseManager wasn't properly saving the
updated internal value.

The check in CacheTenancyBootstrapper wasn't handling that local tests
have a 'testing' environment, not local. However fixing only the
condition would've still added the store to $names which would throw
an exception down the line. We make sure to only throw the exception
in prod, but also make sure to only add the store to $names if it is
supported.
2025-10-22 12:58:45 +02:00

165 lines
6.3 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)) {
// These are the only cache driven session backends (see Laravel's config/session.php)
if (! in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)) {
if (app()->environment('production')) {
// We only throw this exception in prod to make configuration a little easier. Developers
// may have scope_sessions set to true while using different session drivers e.g. in tests.
// Previously we just silently ignored this, however since session scoping is of high importance
// in production, we make sure to notify the developer, by throwing an exception, that session
// scoping isn't happening as expected/configured due to an incompatible session driver.
throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_session');
}
} else {
// Scoping sessions using this bootstrapper implicitly adds the session store to $names
$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') {
// 'file' stores are ignored here and instead handled by FilesystemTenancyBootstrapper
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;
}
}