mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 06:44: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
|
|
@ -43,3 +43,9 @@ If you need to rebuild the container for any reason (e.g. a change in `Dockerfil
|
||||||
## PHPStan
|
## PHPStan
|
||||||
|
|
||||||
Use `composer phpstan` to run our phpstan suite.
|
Use `composer phpstan` to run our phpstan suite.
|
||||||
|
|
||||||
|
## PhpStorm
|
||||||
|
|
||||||
|
Create `.env` with `PROJECT_PATH=/full/path/to/this/directory`. Configure a Docker-based interpreter for tests (with exec, not run).
|
||||||
|
|
||||||
|
If you want to use XDebug, use `composer docker-rebuild-with-xdebug`.
|
||||||
|
|
|
||||||
12
Dockerfile
12
Dockerfile
|
|
@ -30,4 +30,16 @@ RUN echo "apc.enable_cli=1" >> "$PHP_INI_DIR/php.ini"
|
||||||
# Only used on GHA
|
# Only used on GHA
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||||
|
|
||||||
|
# Conditionally install and configure Xdebug (last step for faster rebuilds)
|
||||||
|
ARG XDEBUG_ENABLED=false
|
||||||
|
RUN if [ "$XDEBUG_ENABLED" = "true" ]; then \
|
||||||
|
pecl install xdebug && docker-php-ext-enable xdebug && \
|
||||||
|
echo "xdebug.mode=debug" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini" && \
|
||||||
|
echo "xdebug.start_with_request=yes" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini" && \
|
||||||
|
echo "xdebug.client_host=host.docker.internal" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini" && \
|
||||||
|
echo "xdebug.client_port=9003" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini" && \
|
||||||
|
echo "xdebug.discover_client_host=true" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini" && \
|
||||||
|
echo "xdebug.log=/var/log/xdebug.log" >> "$PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini"; \
|
||||||
|
fi
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,14 @@
|
||||||
"docker-up": "docker compose up -d",
|
"docker-up": "docker compose up -d",
|
||||||
"docker-down": "docker compose down",
|
"docker-down": "docker compose down",
|
||||||
"docker-restart": "docker compose down && docker compose up -d",
|
"docker-restart": "docker compose down && docker compose up -d",
|
||||||
"docker-rebuild": "PHP_VERSION=8.4 docker compose up -d --no-deps --build",
|
"docker-rebuild": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"PHP_VERSION=8.4 docker compose up -d --no-deps --build"
|
||||||
|
],
|
||||||
|
"docker-rebuild-with-xdebug": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"PHP_VERSION=8.4 XDEBUG_ENABLED=true docker compose up -d --no-deps --build"
|
||||||
|
],
|
||||||
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
||||||
"testbench-unlink": "rm ./vendor/orchestra/testbench-core/laravel/vendor",
|
"testbench-unlink": "rm ./vendor/orchestra/testbench-core/laravel/vendor",
|
||||||
"testbench-link": "ln -s /var/www/html/vendor ./vendor/orchestra/testbench-core/laravel/vendor",
|
"testbench-link": "ln -s /var/www/html/vendor ./vendor/orchestra/testbench-core/laravel/vendor",
|
||||||
|
|
@ -72,10 +79,22 @@
|
||||||
"phpstan": "vendor/bin/phpstan --memory-limit=256M",
|
"phpstan": "vendor/bin/phpstan --memory-limit=256M",
|
||||||
"phpstan-pro": "vendor/bin/phpstan --memory-limit=256M --pro",
|
"phpstan-pro": "vendor/bin/phpstan --memory-limit=256M --pro",
|
||||||
"cs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config=.php-cs-fixer.php",
|
"cs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config=.php-cs-fixer.php",
|
||||||
"test": "./test --no-coverage",
|
"test": [
|
||||||
"test-full": "./test",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"act": "act -j tests --matrix 'laravel:^11.0'",
|
"./test --no-coverage"
|
||||||
"act-input": "act -j tests --matrix 'laravel:^11.0' --input"
|
],
|
||||||
|
"test-full": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"./test"
|
||||||
|
],
|
||||||
|
"act": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"act -j tests --matrix 'laravel:^11.0'"
|
||||||
|
],
|
||||||
|
"act-input": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"act -j tests --matrix 'laravel:^11.0' --input"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ services:
|
||||||
test:
|
test:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
args:
|
||||||
|
XDEBUG_ENABLED: ${XDEBUG_ENABLED:-false}
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -18,7 +20,8 @@ services:
|
||||||
dynamodb:
|
dynamodb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- .:/var/www/html:cached
|
- .:${PROJECT_PATH:-$PWD}:cached
|
||||||
|
working_dir: ${PROJECT_PATH:-$PWD}
|
||||||
environment:
|
environment:
|
||||||
DOCKER: 1
|
DOCKER: 1
|
||||||
DB_PASSWORD: password
|
DB_PASSWORD: password
|
||||||
|
|
@ -30,6 +33,8 @@ services:
|
||||||
TENANCY_TEST_SQLSRV_HOST: mssql
|
TENANCY_TEST_SQLSRV_HOST: mssql
|
||||||
TENANCY_TEST_SQLSRV_USERNAME: sa
|
TENANCY_TEST_SQLSRV_USERNAME: sa
|
||||||
TENANCY_TEST_SQLSRV_PASSWORD: P@ssword
|
TENANCY_TEST_SQLSRV_PASSWORD: P@ssword
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
mysql:
|
mysql:
|
||||||
|
|
|
||||||
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
|
class GlobalCache extends Cache
|
||||||
{
|
{
|
||||||
|
/** Make sure this works identically to global_cache() */
|
||||||
|
protected static $cached = false;
|
||||||
|
|
||||||
protected static function getFacadeAccessor()
|
protected static function getFacadeAccessor()
|
||||||
{
|
{
|
||||||
return 'globalCache';
|
return 'globalCache';
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ abstract class CachedTenantResolver implements TenantResolver
|
||||||
|
|
||||||
public function __construct(Application $app)
|
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());
|
$this->cache = $app->make('globalCache')->store(static::cacheStore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
public static bool $registerForgetTenantParameterListener = true;
|
public static bool $registerForgetTenantParameterListener = true;
|
||||||
public static bool $migrateFreshOverride = true;
|
public static bool $migrateFreshOverride = true;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
public static Closure|null $adjustCacheManagerUsing = null;
|
||||||
|
|
||||||
/* Register services. */
|
/* Register services. */
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
|
@ -81,7 +84,29 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('globalCache', function ($app) {
|
$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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ test('tags separate cache properly', function () {
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
cache()->put('foo', 'bar', 1);
|
cache()->put('foo', 'bar');
|
||||||
expect(cache()->get('foo'))->toBe('bar');
|
expect(cache()->get('foo'))->toBe('bar');
|
||||||
|
|
||||||
$tenant2 = Tenant::create();
|
$tenant2 = Tenant::create();
|
||||||
|
|
@ -64,7 +64,7 @@ test('tags separate cache properly', function () {
|
||||||
|
|
||||||
expect(cache('foo'))->not()->toBe('bar');
|
expect(cache('foo'))->not()->toBe('bar');
|
||||||
|
|
||||||
cache()->put('foo', 'xyz', 1);
|
cache()->put('foo', 'xyz');
|
||||||
expect(cache()->get('foo'))->toBe('xyz');
|
expect(cache()->get('foo'))->toBe('xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ test('invoking the cache helper works', function () {
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
cache(['foo' => 'bar'], 1);
|
cache(['foo' => 'bar']);
|
||||||
expect(cache('foo'))->toBe('bar');
|
expect(cache('foo'))->toBe('bar');
|
||||||
|
|
||||||
$tenant2 = Tenant::create();
|
$tenant2 = Tenant::create();
|
||||||
|
|
@ -80,7 +80,7 @@ test('invoking the cache helper works', function () {
|
||||||
|
|
||||||
expect(cache('foo'))->not()->toBe('bar');
|
expect(cache('foo'))->not()->toBe('bar');
|
||||||
|
|
||||||
cache(['foo' => 'xyz'], 1);
|
cache(['foo' => 'xyz']);
|
||||||
expect(cache('foo'))->toBe('xyz');
|
expect(cache('foo'))->toBe('xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ test('cache is persisted', function () {
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
cache(['foo' => 'bar'], 10);
|
cache(['foo' => 'bar']);
|
||||||
expect(cache('foo'))->toBe('bar');
|
expect(cache('foo'))->toBe('bar');
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
|
|
@ -102,7 +102,7 @@ test('cache is persisted when reidentification is used', function () {
|
||||||
$tenant2 = Tenant::create();
|
$tenant2 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
cache(['foo' => 'bar'], 10);
|
cache(['foo' => 'bar']);
|
||||||
expect(cache('foo'))->toBe('bar');
|
expect(cache('foo'))->toBe('bar');
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
tenancy()->initialize($tenant2);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
|
@ -23,6 +25,8 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach($cleanup = function () {
|
beforeEach($cleanup = function () {
|
||||||
Tenant::$extraCustomColumns = [];
|
Tenant::$extraCustomColumns = [];
|
||||||
|
|
@ -112,11 +116,19 @@ test('cache is invalidated when the tenant is updated', function (string $resolv
|
||||||
// Only testing update here - presumably if this works, deletes (and other things we test here)
|
// Only testing update here - presumably if this works, deletes (and other things we test here)
|
||||||
// will work as well. The main unique thing about this test is that it makes the change from
|
// will work as well. The main unique thing about this test is that it makes the change from
|
||||||
// *within* the tenant context.
|
// *within* the tenant context.
|
||||||
test('cache is invalidated when tenant is updated from within the tenant context', function (string $cacheBootstrapper) {
|
test('cache is invalidated when tenant is updated from within the tenant context', function (string $cacheStore, array $bootstrappers) {
|
||||||
config(['tenancy.bootstrappers' => [$cacheBootstrapper]]);
|
config([
|
||||||
|
'cache.default' => $cacheStore,
|
||||||
|
'tenancy.bootstrappers' => $bootstrappers,
|
||||||
|
]);
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
if ($cacheStore === 'database') {
|
||||||
|
withCacheTables();
|
||||||
|
withTenantDatabases();
|
||||||
|
}
|
||||||
|
|
||||||
$resolver = PathTenantResolver::class;
|
$resolver = PathTenantResolver::class;
|
||||||
|
|
||||||
$tenant = Tenant::create([$tenantModelColumn = tenantModelColumn(true) => 'acme']);
|
$tenant = Tenant::create([$tenantModelColumn = tenantModelColumn(true) => 'acme']);
|
||||||
|
|
@ -150,9 +162,9 @@ test('cache is invalidated when tenant is updated from within the tenant context
|
||||||
|
|
||||||
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the tenant was retrieved from the DB
|
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the tenant was retrieved from the DB
|
||||||
})->with([
|
})->with([
|
||||||
// todo@samuel test this with the database cache bootstrapper too?
|
['redis', [CacheTenancyBootstrapper::class]],
|
||||||
CacheTenancyBootstrapper::class,
|
['redis', [CacheTagsBootstrapper::class]],
|
||||||
CacheTagsBootstrapper::class,
|
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('cache is invalidated when the tenant is deleted', function (string $resolver, bool $configureTenantModelColumn) {
|
test('cache is invalidated when the tenant is deleted', function (string $resolver, bool $configureTenantModelColumn) {
|
||||||
|
|
|
||||||
193
tests/DatabaseCacheBootstrapperTest.php
Normal file
193
tests/DatabaseCacheBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||||
|
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
withBootstrapping();
|
||||||
|
withCacheTables();
|
||||||
|
withTenantDatabases(true);
|
||||||
|
|
||||||
|
DatabaseCacheBootstrapper::$stores = null;
|
||||||
|
|
||||||
|
config([
|
||||||
|
'cache.stores.database.connection' => 'central', // Explicitly set cache DB connection name in config
|
||||||
|
'cache.stores.database.lock_connection' => 'central', // Also set lock connection name
|
||||||
|
'cache.default' => 'database',
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
DatabaseTenancyBootstrapper::class,
|
||||||
|
DatabaseCacheBootstrapper::class, // Used instead of CacheTenancyBootstrapper
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
DatabaseCacheBootstrapper::$stores = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatabaseCacheBootstrapper switches the database cache store connections correctly', function () {
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.database.lock_connection'))->toBe('central');
|
||||||
|
expect(Cache::store()->getConnection()->getName())->toBe('central');
|
||||||
|
expect(Cache::lock('foo')->getConnectionName())->toBe('central');
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('tenant');
|
||||||
|
expect(config('cache.stores.database.lock_connection'))->toBe('tenant');
|
||||||
|
expect(Cache::store()->getConnection()->getName())->toBe('tenant');
|
||||||
|
expect(Cache::lock('foo')->getConnectionName())->toBe('tenant');
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.database.lock_connection'))->toBe('central');
|
||||||
|
expect(Cache::store()->getConnection()->getName())->toBe('central');
|
||||||
|
expect(Cache::lock('foo')->getConnectionName())->toBe('central');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cache is separated correctly when using DatabaseCacheBootstrapper', function() {
|
||||||
|
// We need the prefix later for lower-level assertions. Let's store it
|
||||||
|
// once now and reuse this variable rather than re-fetching it to make
|
||||||
|
// it clear that the scoping does NOT come from a prefix change.
|
||||||
|
|
||||||
|
$cachePrefix = config('cache.prefix');
|
||||||
|
$getCacheUsingDbQuery = fn (string $cacheKey) =>
|
||||||
|
DB::selectOne("SELECT * FROM `cache` WHERE `key` = '{$cachePrefix}{$cacheKey}'")?->value;
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
// Write to cache in central context
|
||||||
|
cache()->set('foo', 'central');
|
||||||
|
expect(Cache::get('foo'))->toBe('central');
|
||||||
|
// The value retrieved by the DB query is formatted like "s:7:"central";".
|
||||||
|
// We use toContain() because of this formatting instead of just toBe().
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toContain('central');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// Central cache doesn't leak to tenant context
|
||||||
|
expect(Cache::has('foo'))->toBeFalse();
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toBeNull();
|
||||||
|
|
||||||
|
cache()->set('foo', 'bar');
|
||||||
|
expect(Cache::get('foo'))->toBe('bar');
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toContain('bar');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
// Assert one tenant's cache doesn't leak to another tenant
|
||||||
|
expect(Cache::has('foo'))->toBeFalse();
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toBeNull();
|
||||||
|
|
||||||
|
cache()->set('foo', 'xyz');
|
||||||
|
expect(Cache::get('foo'))->toBe('xyz');
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toContain('xyz');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// Assert cache didn't leak to the original tenant
|
||||||
|
expect(Cache::get('foo'))->toBe('bar');
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toContain('bar');
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Assert central 'foo' cache is still the same ('central')
|
||||||
|
expect(Cache::get('foo'))->toBe('central');
|
||||||
|
expect($getCacheUsingDbQuery('foo'))->toContain('central');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatabaseCacheBootstrapper auto-detects all database driver stores by default', function() {
|
||||||
|
config([
|
||||||
|
'cache.stores.database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => 'central',
|
||||||
|
'table' => 'cache',
|
||||||
|
],
|
||||||
|
'cache.stores.sessions' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => 'central',
|
||||||
|
'table' => 'sessions_cache',
|
||||||
|
],
|
||||||
|
'cache.stores.redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => 'default',
|
||||||
|
],
|
||||||
|
'cache.stores.file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => '/foo/bar',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here, we're using auto-detection (default behavior)
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||||
|
expect(config('cache.stores.file.path'))->toBe('/foo/bar');
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
// Using auto-detection (default behavior),
|
||||||
|
// all database driver stores should be configured,
|
||||||
|
// and stores with non-database drivers are ignored.
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('tenant');
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('tenant');
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default'); // unchanged
|
||||||
|
expect(config('cache.stores.file.path'))->toBe('/foo/bar'); // unchanged
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// All database stores should be reverted, others unchanged
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||||
|
expect(config('cache.stores.file.path'))->toBe('/foo/bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manual $stores configuration takes precedence over auto-detection', function() {
|
||||||
|
// Configure multiple database stores
|
||||||
|
config([
|
||||||
|
'cache.stores.sessions' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => 'central',
|
||||||
|
'table' => 'sessions_cache',
|
||||||
|
],
|
||||||
|
'cache.stores.redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => 'default',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Specific store overrides (including non-database stores)
|
||||||
|
DatabaseCacheBootstrapper::$stores = ['sessions', 'redis']; // Note: excludes 'database'
|
||||||
|
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
// Manual config takes precedence: only 'sessions' is configured
|
||||||
|
// - redis filtered out by driver check
|
||||||
|
// - database store not included in $stores
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central'); // Excluded in manual config
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('tenant'); // Included and is database driver
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default'); // Included but filtered out (not database driver)
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Only the manually configured stores' config will be reverted
|
||||||
|
expect(config('cache.stores.database.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.sessions.connection'))->toBe('central');
|
||||||
|
expect(config('cache.stores.redis.connection'))->toBe('default');
|
||||||
|
});
|
||||||
|
|
@ -11,6 +11,11 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
|
||||||
|
use function Stancl\Tenancy\Tests\withCacheTables;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config([
|
config([
|
||||||
|
|
@ -20,26 +25,40 @@ beforeEach(function () {
|
||||||
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
withCacheTables();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('global cache manager stores data in global cache', function (string $bootstrapper) {
|
test('global cache manager stores data in global cache', function (string $store, array $bootstrappers) {
|
||||||
config(['tenancy.bootstrappers' => [$bootstrapper]]);
|
config([
|
||||||
|
'cache.default' => $store,
|
||||||
|
'tenancy.bootstrappers' => $bootstrappers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($store === 'database') withTenantDatabases(true);
|
||||||
|
|
||||||
expect(cache('foo'))->toBe(null);
|
expect(cache('foo'))->toBe(null);
|
||||||
GlobalCache::put(['foo' => 'bar'], 1);
|
GlobalCache::put('foo', 'bar');
|
||||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||||
|
|
||||||
GlobalCache::put(['abc' => 'xyz'], 1);
|
GlobalCache::put('abc', 'xyz');
|
||||||
cache(['def' => 'ghi'], 10);
|
cache(['def' => 'ghi']);
|
||||||
expect(cache('def'))->toBe('ghi');
|
expect(cache('def'))->toBe('ghi');
|
||||||
|
|
||||||
// different stores, same underlying connection. the prefix is set ON THE STORE
|
// different stores
|
||||||
expect(cache()->store()->getStore() === GlobalCache::store()->getStore())->toBeFalse();
|
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
|
||||||
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
|
if ($store === 'redis') {
|
||||||
|
// same underlying connection. the prefix is set ON THE STORE
|
||||||
|
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
|
||||||
|
} else {
|
||||||
|
// different connections
|
||||||
|
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
|
||||||
|
expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
}
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(GlobalCache::get('abc'))->toBe('xyz');
|
expect(GlobalCache::get('abc'))->toBe('xyz');
|
||||||
|
|
@ -51,25 +70,129 @@ test('global cache manager stores data in global cache', function (string $boots
|
||||||
expect(GlobalCache::get('abc'))->toBe('xyz');
|
expect(GlobalCache::get('abc'))->toBe('xyz');
|
||||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||||
expect(cache('def'))->toBe(null);
|
expect(cache('def'))->toBe(null);
|
||||||
cache(['def' => 'xxx'], 1);
|
cache(['def' => 'xxx']);
|
||||||
expect(cache('def'))->toBe('xxx');
|
expect(cache('def'))->toBe('xxx');
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
expect(cache('def'))->toBe('ghi');
|
expect(cache('def'))->toBe('ghi');
|
||||||
})->with([
|
})->with([
|
||||||
CacheTagsBootstrapper::class,
|
['redis', [CacheTagsBootstrapper::class]],
|
||||||
CacheTenancyBootstrapper::class,
|
['redis', [CacheTenancyBootstrapper::class]],
|
||||||
|
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('the global_cache helper supports the same syntax as the cache helper', function (string $bootstrapper) {
|
test('global cache facade is not persistent', function () {
|
||||||
config(['tenancy.bootstrappers' => [$bootstrapper]]);
|
$oldId = spl_object_id(GlobalCache::getFacadeRoot());
|
||||||
|
|
||||||
|
$_ = new class {};
|
||||||
|
|
||||||
|
expect(spl_object_id(GlobalCache::getFacadeRoot()))->not()->toBe($oldId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('global cache is always central', function (string $store, array $bootstrappers, string $initialCentralCall) {
|
||||||
|
config([
|
||||||
|
'cache.default' => $store,
|
||||||
|
'tenancy.bootstrappers' => $bootstrappers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($store === 'database') {
|
||||||
|
withTenantDatabases(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This tells us which "accessor" for the global cache should be instantiated first, before we go
|
||||||
|
// into the tenant context. We make sure to not touch the other one here. This tests that whether
|
||||||
|
// a particular accessor is used "early" makes no difference in the later behavior.
|
||||||
|
if ($initialCentralCall === 'helper') {
|
||||||
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
global_cache()->put('central-helper', true);
|
||||||
|
} else if ($initialCentralCall === 'facade') {
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
GlobalCache::put('central-facade', true);
|
||||||
|
} else if ($initialCentralCall === 'both') {
|
||||||
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
global_cache()->put('central-helper', true);
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
GlobalCache::put('central-facade', true);
|
||||||
|
}
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$tenant->enter();
|
$tenant->enter();
|
||||||
|
|
||||||
// different stores, same underlying connection. the prefix is set ON THE STORE
|
// Here we use both the helper and the facade to ensure the value is accessible via either one
|
||||||
expect(cache()->store()->getStore() === global_cache()->store()->getStore())->toBeFalse();
|
if ($initialCentralCall === 'helper') {
|
||||||
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
expect(global_cache('central-helper'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||||
|
} else if ($initialCentralCall === 'facade') {
|
||||||
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
expect(global_cache('central-facade'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||||
|
} else if ($initialCentralCall === 'both') {
|
||||||
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
expect(global_cache('central-helper'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||||
|
expect(global_cache('central-facade'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
global_cache()->put('tenant-helper', true);
|
||||||
|
GlobalCache::put('tenant-facade', true);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
if ($store === 'database') expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
if ($store === 'database') expect(GlobalCache::store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
|
||||||
|
expect(global_cache('tenant-helper'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('tenant-helper'))->toBe(true);
|
||||||
|
expect(global_cache('tenant-facade'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('tenant-facade'))->toBe(true);
|
||||||
|
|
||||||
|
if ($initialCentralCall === 'helper') {
|
||||||
|
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||||
|
} else if ($initialCentralCall === 'facade') {
|
||||||
|
expect(global_cache('central-facade'))->toBe(true);
|
||||||
|
} else if ($initialCentralCall === 'both') {
|
||||||
|
expect(global_cache('central-helper'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-helper'))->toBe(true);
|
||||||
|
expect(global_cache('central-facade'))->toBe(true);
|
||||||
|
expect(GlobalCache::get('central-facade'))->toBe(true);
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
['redis', [CacheTagsBootstrapper::class]],
|
||||||
|
['redis', [CacheTenancyBootstrapper::class]],
|
||||||
|
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||||
|
])->with([
|
||||||
|
'helper',
|
||||||
|
'facade',
|
||||||
|
'both',
|
||||||
|
'none',
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('the global_cache helper supports the same syntax as the cache helper', function (string $store, array $bootstrappers) {
|
||||||
|
config([
|
||||||
|
'cache.default' => $store,
|
||||||
|
'tenancy.bootstrappers' => $bootstrappers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($store === 'database') withTenantDatabases(true);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenant->enter();
|
||||||
|
|
||||||
|
// different stores
|
||||||
|
expect(cache()->store()->getStore() !== GlobalCache::store()->getStore())->toBeTrue();
|
||||||
|
if ($store === 'redis') {
|
||||||
|
// same underlying connection. the prefix is set ON THE STORE
|
||||||
|
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
|
||||||
|
} else {
|
||||||
|
// different connections
|
||||||
|
expect(cache()->store()->getStore()->getConnection()->getName())->toBe('tenant');
|
||||||
|
expect(global_cache()->store()->getStore()->getConnection()->getName())->toBe('central');
|
||||||
|
}
|
||||||
|
|
||||||
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
||||||
|
|
||||||
|
|
@ -81,6 +204,7 @@ test('the global_cache helper supports the same syntax as the cache helper', fun
|
||||||
|
|
||||||
expect(cache('foo'))->toBe(null); // tenant cache is not affected
|
expect(cache('foo'))->toBe(null); // tenant cache is not affected
|
||||||
})->with([
|
})->with([
|
||||||
CacheTagsBootstrapper::class,
|
['redis', [CacheTagsBootstrapper::class]],
|
||||||
CacheTenancyBootstrapper::class,
|
['redis', [CacheTenancyBootstrapper::class]],
|
||||||
|
['database', [DatabaseTenancyBootstrapper::class, DatabaseCacheBootstrapper::class]],
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,52 @@
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Tests;
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Stancl\Tenancy\Tests\TestCase;
|
use Stancl\Tenancy\Tests\TestCase;
|
||||||
use Stancl\JobPipeline\JobPipeline;
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
use Stancl\Tenancy\Events\TenantCreated;
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
uses(TestCase::class)->in(__DIR__);
|
uses(TestCase::class)->in(__DIR__);
|
||||||
|
|
||||||
function withTenantDatabases()
|
function withBootstrapping()
|
||||||
{
|
{
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTenantDatabases(bool $migrate = false)
|
||||||
|
{
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make($migrate
|
||||||
|
? [CreateDatabase::class, MigrateDatabase::class]
|
||||||
|
: [CreateDatabase::class]
|
||||||
|
)->send(function (TenantCreated $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->toListener());
|
})->toListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withCacheTables()
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function pest(): TestCase
|
function pest(): TestCase
|
||||||
{
|
{
|
||||||
return \Pest\TestSuite::getInstance()->test;
|
return \Pest\TestSuite::getInstance()->test;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||||
|
|
||||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -38,6 +39,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
|
|
||||||
ini_set('memory_limit', '1G');
|
ini_set('memory_limit', '1G');
|
||||||
|
|
||||||
|
TenancyServiceProvider::$registerForgetTenantParameterListener = true;
|
||||||
|
TenancyServiceProvider::$migrateFreshOverride = true;
|
||||||
|
TenancyServiceProvider::$adjustCacheManagerUsing = null;
|
||||||
|
|
||||||
Redis::connection('default')->flushdb();
|
Redis::connection('default')->flushdb();
|
||||||
Redis::connection('cache')->flushdb();
|
Redis::connection('cache')->flushdb();
|
||||||
Artisan::call('cache:clear memcached'); // flush memcached
|
Artisan::call('cache:clear memcached'); // flush memcached
|
||||||
|
|
@ -180,6 +185,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
// to manually register bootstrappers as singletons here.
|
// to manually register bootstrappers as singletons here.
|
||||||
$app->singleton(RedisTenancyBootstrapper::class);
|
$app->singleton(RedisTenancyBootstrapper::class);
|
||||||
$app->singleton(CacheTenancyBootstrapper::class);
|
$app->singleton(CacheTenancyBootstrapper::class);
|
||||||
|
$app->singleton(DatabaseCacheBootstrapper::class);
|
||||||
$app->singleton(BroadcastingConfigBootstrapper::class);
|
$app->singleton(BroadcastingConfigBootstrapper::class);
|
||||||
$app->singleton(BroadcastChannelPrefixBootstrapper::class);
|
$app->singleton(BroadcastChannelPrefixBootstrapper::class);
|
||||||
$app->singleton(PostgresRLSBootstrapper::class);
|
$app->singleton(PostgresRLSBootstrapper::class);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue