mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-06 01:44:04 +00:00
Refactor cached tenant resolvers with decorator pattern
This commit is contained in:
parent
8e3b74f9d1
commit
97e45ab9cc
15 changed files with 424 additions and 251 deletions
|
|
@ -196,4 +196,28 @@ return [
|
||||||
'--class' => 'DatabaseSeeder', // root seeder class
|
'--class' => 'DatabaseSeeder', // root seeder class
|
||||||
// '--force' => true,
|
// '--force' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'tenant_resolvers' => [
|
||||||
|
\Stancl\Tenancy\Resolvers\DomainTenantResolver::class => [
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600,
|
||||||
|
'cache_store' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
\Stancl\Tenancy\Resolvers\PathTenantResolver::class => [
|
||||||
|
'parameter_name' => 'tenant',
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600,
|
||||||
|
'cache_store' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
\Stancl\Tenancy\Resolvers\RequestDataTenantResolver::class => [
|
||||||
|
'cache' => false,
|
||||||
|
'cache_ttl' => 3600,
|
||||||
|
'cache_store' => null,
|
||||||
|
]
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
|
||||||
52
src/Repository/IlluminateTenantRepository.php
Normal file
52
src/Repository/IlluminateTenantRepository.php
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Repository;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
class IlluminateTenantRepository implements TenantRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var class-string<Model & Tenant> */
|
||||||
|
public readonly string $modelClass,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int|string $id): ?Tenant
|
||||||
|
{
|
||||||
|
return $this->query()->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findForDomain(string $domain): ?Tenant
|
||||||
|
{
|
||||||
|
return $this->query()
|
||||||
|
->whereRelation('domains', 'domain', $domain)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function all(): iterable
|
||||||
|
{
|
||||||
|
return $this->query()->cursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function whereKeyIn(string|int ...$ids): iterable
|
||||||
|
{
|
||||||
|
return $this->query()->whereKey($ids)->cursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<Model & Tenant>
|
||||||
|
*/
|
||||||
|
private function query(): Builder
|
||||||
|
{
|
||||||
|
return (new $this->modelClass)::query();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Repository/TenantRepository.php
Normal file
22
src/Repository/TenantRepository.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Repository;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
interface TenantRepository
|
||||||
|
{
|
||||||
|
public function find(string|int $id): ?Tenant;
|
||||||
|
|
||||||
|
public function findForDomain(string $domain): ?Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<Tenant>
|
||||||
|
*/
|
||||||
|
public function all(): iterable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<Tenant>
|
||||||
|
*/
|
||||||
|
public function whereKeyIn(string|int ...$ids): iterable;
|
||||||
|
}
|
||||||
34
src/Resolvers/CachedTenantResolver.php
Normal file
34
src/Resolvers/CachedTenantResolver.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||||
|
|
||||||
|
class CachedTenantResolver implements TenantResolver
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TenantResolver $tenantResolver,
|
||||||
|
private readonly CacheRepository $cache,
|
||||||
|
private readonly string $prefix,
|
||||||
|
private readonly int $ttl = 3600,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(mixed ...$args): Tenant
|
||||||
|
{
|
||||||
|
return $this->cache->remember(
|
||||||
|
key: $this->getCacheKey(...$args),
|
||||||
|
ttl: $this->ttl,
|
||||||
|
callback: fn() => $this->tenantResolver->resolve(...$args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheKey(mixed ...$args): string
|
||||||
|
{
|
||||||
|
return sprintf('%s:%s', $this->prefix, json_encode($args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Resolvers\Contracts;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Cache\Factory;
|
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
|
||||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
|
||||||
|
|
||||||
abstract class CachedTenantResolver implements TenantResolver
|
|
||||||
{
|
|
||||||
public static bool $shouldCache = false; // todo docblocks for these
|
|
||||||
|
|
||||||
public static int $cacheTTL = 3600; // seconds
|
|
||||||
|
|
||||||
public static string|null $cacheStore = null; // default
|
|
||||||
|
|
||||||
/** @var Repository */
|
|
||||||
protected $cache;
|
|
||||||
|
|
||||||
public function __construct(Factory $cache)
|
|
||||||
{
|
|
||||||
$this->cache = $cache->store(static::$cacheStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resolve(mixed ...$args): Tenant
|
|
||||||
{
|
|
||||||
if (! static::$shouldCache) {
|
|
||||||
return $this->resolveWithoutCache(...$args);
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = $this->getCacheKey(...$args);
|
|
||||||
|
|
||||||
if ($this->cache->has($key)) {
|
|
||||||
$tenant = $this->cache->get($key);
|
|
||||||
|
|
||||||
$this->resolved($tenant, ...$args);
|
|
||||||
|
|
||||||
return $tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenant = $this->resolveWithoutCache(...$args);
|
|
||||||
$this->cache->put($key, $tenant, static::$cacheTTL);
|
|
||||||
|
|
||||||
return $tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidateCache(Tenant $tenant): void
|
|
||||||
{
|
|
||||||
if (! static::$shouldCache) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->getArgsForTenant($tenant) as $args) {
|
|
||||||
$this->cache->forget($this->getCacheKey(...$args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCacheKey(mixed ...$args): string
|
|
||||||
{
|
|
||||||
return '_tenancy_resolver:' . static::class . ':' . json_encode($args);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract public function resolveWithoutCache(mixed ...$args): Tenant;
|
|
||||||
|
|
||||||
public function resolved(Tenant $tenant, ...$args): void
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the arg combinations for resolve() that can be used to find this tenant.
|
|
||||||
*
|
|
||||||
* @return array[]
|
|
||||||
*/
|
|
||||||
abstract public function getArgsForTenant(Tenant $tenant): array;
|
|
||||||
}
|
|
||||||
|
|
@ -4,57 +4,24 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Resolvers;
|
namespace Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Stancl\Tenancy\Contracts\Domain;
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
|
use Stancl\Tenancy\Repository\TenantRepository;
|
||||||
|
|
||||||
class DomainTenantResolver extends Contracts\CachedTenantResolver
|
class DomainTenantResolver implements TenantResolver
|
||||||
{
|
{
|
||||||
/** The model representing the domain that the tenant was identified on. */
|
public function __construct(
|
||||||
public static Domain $currentDomain; // todo |null?
|
private readonly TenantRepository $tenantRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public static bool $shouldCache = false;
|
public function resolve(mixed ...$args): Tenant
|
||||||
|
|
||||||
public static int $cacheTTL = 3600; // seconds
|
|
||||||
|
|
||||||
public static string|null $cacheStore = null; // default
|
|
||||||
|
|
||||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
|
||||||
{
|
{
|
||||||
$domain = $args[0];
|
$domain = $args[0];
|
||||||
|
|
||||||
/** @var Tenant|null $tenant */
|
$tenant = $this->tenantRepository->findForDomain($domain);
|
||||||
$tenant = config('tenancy.tenant_model')::query()
|
|
||||||
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
|
|
||||||
->with('domains')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($tenant) {
|
return $tenant ?? throw new TenantCouldNotBeIdentifiedOnDomainException($args[0]);
|
||||||
$this->setCurrentDomain($tenant, $domain);
|
|
||||||
|
|
||||||
return $tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TenantCouldNotBeIdentifiedOnDomainException($args[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resolved(Tenant $tenant, ...$args): void
|
|
||||||
{
|
|
||||||
$this->setCurrentDomain($tenant, $args[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setCurrentDomain(Tenant $tenant, string $domain): void
|
|
||||||
{
|
|
||||||
static::$currentDomain = $tenant->domains->where('domain', $domain)->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getArgsForTenant(Tenant $tenant): array
|
|
||||||
{
|
|
||||||
$tenant->unsetRelation('domains');
|
|
||||||
|
|
||||||
return $tenant->domains->map(function (Domain $domain) {
|
|
||||||
return [$domain->domain];
|
|
||||||
})->toArray();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,38 +6,31 @@ namespace Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
use Illuminate\Routing\Route;
|
use Illuminate\Routing\Route;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
||||||
|
use Stancl\Tenancy\Repository\TenantRepository;
|
||||||
|
|
||||||
class PathTenantResolver extends Contracts\CachedTenantResolver
|
class PathTenantResolver implements TenantResolver
|
||||||
{
|
{
|
||||||
public static string $tenantParameterName = 'tenant';
|
public function __construct(
|
||||||
|
private readonly TenantRepository $tenantRepository,
|
||||||
|
private readonly string $tenantParameterName = 'tenant',
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public static bool $shouldCache = false;
|
public function resolve(mixed ...$args): Tenant
|
||||||
|
|
||||||
public static int $cacheTTL = 3600; // seconds
|
|
||||||
|
|
||||||
public static string|null $cacheStore = null; // default
|
|
||||||
|
|
||||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
|
||||||
{
|
{
|
||||||
/** @var Route $route */
|
/** @var Route $route */
|
||||||
$route = $args[0];
|
[$route] = $args;
|
||||||
|
|
||||||
if ($id = $route->parameter(static::$tenantParameterName)) {
|
if ($id = $route->parameter($this->tenantParameterName)) {
|
||||||
$route->forgetParameter(static::$tenantParameterName);
|
$route->forgetParameter($this->tenantParameterName);
|
||||||
|
|
||||||
if ($tenant = tenancy()->find($id)) {
|
if ($tenant = $this->tenantRepository->find($id)) {
|
||||||
return $tenant;
|
return $tenant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TenantCouldNotBeIdentifiedByPathException($id);
|
throw new TenantCouldNotBeIdentifiedByPathException($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArgsForTenant(Tenant $tenant): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[$tenant->id],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,25 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Resolvers;
|
namespace Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
||||||
|
use Stancl\Tenancy\Repository\TenantRepository;
|
||||||
|
|
||||||
class RequestDataTenantResolver extends Contracts\CachedTenantResolver
|
class RequestDataTenantResolver implements TenantResolver
|
||||||
{
|
{
|
||||||
public static bool $shouldCache = false;
|
public function __construct(
|
||||||
|
private readonly TenantRepository $tenantRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public static int $cacheTTL = 3600; // seconds
|
public function resolve(mixed ...$args): Tenant
|
||||||
|
|
||||||
public static string|null $cacheStore = null; // default
|
|
||||||
|
|
||||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
|
||||||
{
|
{
|
||||||
$payload = $args[0];
|
$payload = $args[0];
|
||||||
|
|
||||||
if ($payload && $tenant = tenancy()->find($payload)) {
|
if ($payload && $tenant = $this->tenantRepository->find($payload)) {
|
||||||
return $tenant;
|
return $tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
|
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArgsForTenant(Tenant $tenant): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[$tenant->id],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy;
|
namespace Stancl\Tenancy;
|
||||||
|
|
||||||
use Illuminate\Cache\CacheManager;
|
use Illuminate\Cache\CacheManager;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
|
@ -12,7 +13,12 @@ use Stancl\Tenancy\Contracts\Domain;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Enums\LogMode;
|
use Stancl\Tenancy\Enums\LogMode;
|
||||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||||
|
use Stancl\Tenancy\Repository\IlluminateTenantRepository;
|
||||||
|
use Stancl\Tenancy\Repository\TenantRepository;
|
||||||
|
use Stancl\Tenancy\Resolvers\CachedTenantResolver;
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||||
|
|
||||||
class TenancyServiceProvider extends ServiceProvider
|
class TenancyServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +46,63 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
return $app[Tenancy::class]->tenant;
|
return $app[Tenancy::class]->tenant;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->app->bind(TenantRepository::class, function () {
|
||||||
|
return new IlluminateTenantRepository(config('tenancy.tenant_model'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(DomainTenantResolver::class, function () {
|
||||||
|
$tenantResolver = new DomainTenantResolver(
|
||||||
|
tenantRepository: $this->app->make(TenantRepository::class),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (($config = config('tenancy.tenant_resolvers.' . DomainTenantResolver::class)) && ($config['cache'] ?? false)) {
|
||||||
|
return new CachedTenantResolver(
|
||||||
|
tenantResolver: $tenantResolver,
|
||||||
|
cache: Cache::store(config('tenancy.tenant_resolvers')),
|
||||||
|
prefix: '_tenancy_resolver:domain:',
|
||||||
|
ttl: $config['ttl'] ?? 3600,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return $tenantResolver;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(PathTenantResolver::class, function () {
|
||||||
|
$config = config('tenancy.tenant_resolvers.' . PathTenantResolver::class);
|
||||||
|
$tenantResolver = new PathTenantResolver(
|
||||||
|
tenantRepository: $this->app->make(TenantRepository::class),
|
||||||
|
tenantParameterName: $config['parameter_name'] ?? 'tenant',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($config['cache'] ?? false) {
|
||||||
|
return new CachedTenantResolver(
|
||||||
|
tenantResolver: $tenantResolver,
|
||||||
|
cache: Cache::store(config('tenancy.tenant_resolvers')),
|
||||||
|
prefix: '_tenancy_resolver:path:',
|
||||||
|
ttl: $config['ttl'] ?? 3600,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return $tenantResolver;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->singleton(RequestDataTenantResolver::class, function () {
|
||||||
|
$tenantResolver = new RequestDataTenantResolver(
|
||||||
|
tenantRepository: $this->app->make(TenantRepository::class),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (($config = config('tenancy.tenant_resolvers.' . RequestDataTenantResolver::class)) && ($config['cache'] ?? false)) {
|
||||||
|
return new CachedTenantResolver(
|
||||||
|
tenantResolver: $tenantResolver,
|
||||||
|
cache: Cache::store(config('tenancy.tenant_resolvers')),
|
||||||
|
prefix: '_tenancy_resolver:request_data:',
|
||||||
|
ttl: $config['ttl'] ?? 3600,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return $tenantResolver;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$this->app->bind(Domain::class, function () {
|
$this->app->bind(Domain::class, function () {
|
||||||
return DomainTenantResolver::$currentDomain;
|
return DomainTenantResolver::$currentDomain;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
DomainTenantResolver::$shouldCache = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tenants can be resolved using the cached resolver', function () {
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue()->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the underlying resolver is not touched when using the cached resolver', function () {
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::enableQueryLog();
|
|
||||||
|
|
||||||
DomainTenantResolver::$shouldCache = false;
|
|
||||||
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
|
||||||
|
|
||||||
DomainTenantResolver::$shouldCache = true;
|
|
||||||
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
expect(DB::getQueryLog())->toBeEmpty(); // empty
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cache is invalidated when the tenant is updated', function () {
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenant->createDomain([
|
|
||||||
'domain' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::enableQueryLog();
|
|
||||||
|
|
||||||
DomainTenantResolver::$shouldCache = true;
|
|
||||||
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
expect(DB::getQueryLog())->toBeEmpty(); // empty
|
|
||||||
|
|
||||||
$tenant->update([
|
|
||||||
'foo' => 'bar',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cache is invalidated when a tenants domain is changed', function () {
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenant->createDomain([
|
|
||||||
'domain' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::enableQueryLog();
|
|
||||||
|
|
||||||
DomainTenantResolver::$shouldCache = true;
|
|
||||||
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
expect(DB::getQueryLog())->toBeEmpty(); // empty
|
|
||||||
|
|
||||||
$tenant->createDomain([
|
|
||||||
'domain' => 'bar',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
|
|
||||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
|
||||||
|
|
||||||
DB::flushQueryLog();
|
|
||||||
expect($tenant->is(app(DomainTenantResolver::class)->resolve('bar')))->toBeTrue();
|
|
||||||
pest()->assertNotEmpty(DB::getQueryLog()); // not empty
|
|
||||||
});
|
|
||||||
41
tests/Repository/InMemoryTenantRepository.php
Normal file
41
tests/Repository/InMemoryTenantRepository.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests\Repository;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
use Stancl\Tenancy\Repository\TenantRepository;
|
||||||
|
|
||||||
|
class InMemoryTenantRepository implements TenantRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var Collection<Tenant> */
|
||||||
|
private Collection $tenants = new Collection(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int|string $id): ?Tenant
|
||||||
|
{
|
||||||
|
return $this->tenants->first(fn (Tenant $tenant) => $tenant->getTenantKey() == $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findForDomain(string $domain): ?Tenant
|
||||||
|
{
|
||||||
|
return $this->tenants->firstWhere(fn (Tenant $tenant) => in_array($domain, $tenant->domains ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(): iterable
|
||||||
|
{
|
||||||
|
return $this->tenants->lazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whereKeyIn(string|int ...$ids): iterable
|
||||||
|
{
|
||||||
|
return $this->tenants->filter(fn (Tenant $tenant) => in_array($tenant->getTenantKey(), $ids))->lazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
$this->tenants->push($tenant);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
tests/Resolvers/CachedTenantResolverTest.php
Normal file
50
tests/Resolvers/CachedTenantResolverTest.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Cache\Repository;
|
||||||
|
use Illuminate\Contracts\Cache\Store;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||||
|
use Stancl\Tenancy\Resolvers\CachedTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
it('uses the underlying resolver if cache is stale', function () {
|
||||||
|
$underlying = Mockery::mock(TenantResolver::class);
|
||||||
|
$cache = new Repository($store = Mockery::mock(Store::class));
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'id' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
$resolver = new CachedTenantResolver(
|
||||||
|
tenantResolver: $underlying,
|
||||||
|
cache: $cache,
|
||||||
|
prefix: '_tenant_resolver',
|
||||||
|
);
|
||||||
|
|
||||||
|
$store->expects('get')->withAnyArgs()->andReturnNull();
|
||||||
|
$underlying->expects('resolve')->andReturn($tenant = new Tenant());
|
||||||
|
$store->expects('put')->withSomeOfArgs($tenant);
|
||||||
|
|
||||||
|
expect($resolver->resolve($args))->toBe($tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips the underlying resolver if cache is valid', function () {
|
||||||
|
$underlying = Mockery::mock(TenantResolver::class);
|
||||||
|
$cache = new Repository($store = Mockery::mock(Store::class));
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'id' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
$resolver = new CachedTenantResolver(
|
||||||
|
tenantResolver: $underlying,
|
||||||
|
cache: $cache,
|
||||||
|
prefix: '_tenant_resolver',
|
||||||
|
);
|
||||||
|
|
||||||
|
$cache->expects('get')->withAnyArgs()->andReturn($tenant = new Tenant());
|
||||||
|
$underlying->expects('resolve')->never();
|
||||||
|
|
||||||
|
expect($resolver->resolve($args))->toBe($tenant);
|
||||||
|
});
|
||||||
33
tests/Resolvers/DomainTenantResolverTest.php
Normal file
33
tests/Resolvers/DomainTenantResolverTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Stancl\Tenancy\Tests\Repository\InMemoryTenantRepository;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->repository = new InMemoryTenantRepository();
|
||||||
|
|
||||||
|
$this->tenant = new Tenant();
|
||||||
|
$this->tenant->domains = ['acme'];
|
||||||
|
|
||||||
|
$this->repository->store($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the tenant with domain', function () {
|
||||||
|
$resolver = new DomainTenantResolver(
|
||||||
|
tenantRepository: $this->repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $resolver->resolve('acme');
|
||||||
|
|
||||||
|
expect($result)->toBe($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when unable to find tenant', function () {
|
||||||
|
$resolver = new DomainTenantResolver(
|
||||||
|
tenantRepository: $this->repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
$resolver->resolve('foo');
|
||||||
|
})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);
|
||||||
40
tests/Resolvers/PathTenantResolverTest.php
Normal file
40
tests/Resolvers/PathTenantResolverTest.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Route;
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
||||||
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tests\Repository\InMemoryTenantRepository;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->repository = new InMemoryTenantRepository();
|
||||||
|
|
||||||
|
$this->tenant = new Tenant();
|
||||||
|
$this->tenant->id = 1;
|
||||||
|
|
||||||
|
$this->repository->store($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the tenant from path', function () {
|
||||||
|
$resolver = new PathTenantResolver(
|
||||||
|
tenantRepository: $this->repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
$route = (new Route('get', '/{tenant}/foo', fn () => null))
|
||||||
|
->bind(Request::create('/1/foo'));
|
||||||
|
|
||||||
|
$result = $resolver->resolve($route);
|
||||||
|
|
||||||
|
expect($result)->toBe($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when unable to find tenant', function () {
|
||||||
|
$resolver = new PathTenantResolver(
|
||||||
|
tenantRepository: new InMemoryTenantRepository(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$route = (new Route('GET', '/{tenant}/foo', fn () => null))
|
||||||
|
->bind(Request::create('/2/foo'));
|
||||||
|
|
||||||
|
$resolver->resolve($route);
|
||||||
|
})->throws(TenantCouldNotBeIdentifiedByPathException::class);
|
||||||
33
tests/Resolvers/RequestDataTenantResolverTest.php
Normal file
33
tests/Resolvers/RequestDataTenantResolverTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Stancl\Tenancy\Tests\Repository\InMemoryTenantRepository;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->repository = new InMemoryTenantRepository();
|
||||||
|
|
||||||
|
$this->tenant = new Tenant();
|
||||||
|
$this->tenant->id = 1;
|
||||||
|
|
||||||
|
$this->repository->store($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the tenant', function () {
|
||||||
|
$resolver = new DomainTenantResolver(
|
||||||
|
tenantRepository: $this->repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $resolver->resolve(id: 1);
|
||||||
|
|
||||||
|
expect($result)->toBe($this->tenant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when unable to find tenant', function () {
|
||||||
|
$resolver = new DomainTenantResolver(
|
||||||
|
tenantRepository: $this->repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
$resolver->resolve('foo');
|
||||||
|
})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue