mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 19:54: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
|
||||
// '--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;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Stancl\Tenancy\Contracts\Domain;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
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 static Domain $currentDomain; // todo |null?
|
||||
public function __construct(
|
||||
private readonly TenantRepository $tenantRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static bool $shouldCache = false;
|
||||
|
||||
public static int $cacheTTL = 3600; // seconds
|
||||
|
||||
public static string|null $cacheStore = null; // default
|
||||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
public function resolve(mixed ...$args): Tenant
|
||||
{
|
||||
$domain = $args[0];
|
||||
|
||||
/** @var Tenant|null $tenant */
|
||||
$tenant = config('tenancy.tenant_model')::query()
|
||||
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
|
||||
->with('domains')
|
||||
->first();
|
||||
$tenant = $this->tenantRepository->findForDomain($domain);
|
||||
|
||||
if ($tenant) {
|
||||
$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();
|
||||
return $tenant ?? throw new TenantCouldNotBeIdentifiedOnDomainException($args[0]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,38 +6,31 @@ namespace Stancl\Tenancy\Resolvers;
|
|||
|
||||
use Illuminate\Routing\Route;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
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 static int $cacheTTL = 3600; // seconds
|
||||
|
||||
public static string|null $cacheStore = null; // default
|
||||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
public function resolve(mixed ...$args): Tenant
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $args[0];
|
||||
[$route] = $args;
|
||||
|
||||
if ($id = $route->parameter(static::$tenantParameterName)) {
|
||||
$route->forgetParameter(static::$tenantParameterName);
|
||||
if ($id = $route->parameter($this->tenantParameterName)) {
|
||||
$route->forgetParameter($this->tenantParameterName);
|
||||
|
||||
if ($tenant = tenancy()->find($id)) {
|
||||
if ($tenant = $this->tenantRepository->find($id)) {
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
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 static string|null $cacheStore = null; // default
|
||||
|
||||
public function resolveWithoutCache(mixed ...$args): Tenant
|
||||
public function resolve(mixed ...$args): Tenant
|
||||
{
|
||||
$payload = $args[0];
|
||||
|
||||
if ($payload && $tenant = tenancy()->find($payload)) {
|
||||
if ($payload && $tenant = $this->tenantRepository->find($payload)) {
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
return [
|
||||
[$tenant->id],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
|
|
@ -12,7 +13,12 @@ use Stancl\Tenancy\Contracts\Domain;
|
|||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Enums\LogMode;
|
||||
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\PathTenantResolver;
|
||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -40,6 +46,63 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
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 () {
|
||||
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