mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 17:24:03 +00:00
Resolver refactor, path identification improvements (#41)
* resolver refactor * Fix code style (php-cs-fixer) * make tenant column used in PathTenantResolver configurable, fix phpstan errors, minor improvements * support binding route fields, write tests for customizable tenant columns * Invalidate cache for all possible columns in path resolver * implement proper cache separation logic for different columns used by PathTenantResolver * improve return type --------- Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
parent
dc430666ba
commit
0c11f29c19
17 changed files with 370 additions and 88 deletions
|
|
@ -6,7 +6,9 @@ namespace Stancl\Tenancy\Resolvers\Contracts;
|
|||
|
||||
use Illuminate\Contracts\Cache\Factory;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
|
||||
abstract class CachedTenantResolver implements TenantResolver
|
||||
|
|
@ -19,13 +21,21 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
$this->cache = $cache->store(static::cacheStore());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tenant using some value passed from the middleware.
|
||||
*
|
||||
* @throws TenantCouldNotBeIdentifiedException
|
||||
*/
|
||||
public function resolve(mixed ...$args): Tenant
|
||||
{
|
||||
if (! static::shouldCache()) {
|
||||
return $this->resolveWithoutCache(...$args);
|
||||
$tenant = $this->resolveWithoutCache(...$args);
|
||||
$this->resolved($tenant, ...$args);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
$key = $this->getCacheKey(...$args);
|
||||
$key = $this->formatCacheKey(...$args);
|
||||
|
||||
if ($tenant = $this->cache->get($key)) {
|
||||
$this->resolved($tenant, ...$args);
|
||||
|
|
@ -35,44 +45,51 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
|
||||
$tenant = $this->resolveWithoutCache(...$args);
|
||||
$this->cache->put($key, $tenant, static::cacheTTL());
|
||||
$this->resolved($tenant, ...$args);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function invalidateCache(Tenant $tenant): void
|
||||
/**
|
||||
* Invalidate this resolver's cache for a tenant.
|
||||
*/
|
||||
public function invalidateCache(Tenant&Model $tenant): void
|
||||
{
|
||||
if (! static::shouldCache()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getArgsForTenant($tenant) as $args) {
|
||||
$this->cache->forget($this->getCacheKey(...$args));
|
||||
foreach ($this->getPossibleCacheKeys($tenant) as $key) {
|
||||
$this->cache->forget($key);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(mixed ...$args): string
|
||||
public function formatCacheKey(mixed ...$args): string
|
||||
{
|
||||
return '_tenancy_resolver:' . static::class . ':' . json_encode($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tenant using $args passed from middleware, without using cache.
|
||||
*
|
||||
* @throws TenantCouldNotBeIdentifiedException
|
||||
*/
|
||||
abstract public function resolveWithoutCache(mixed ...$args): Tenant;
|
||||
|
||||
/**
|
||||
* Called after a tenant has been resolved from cache or without cache.
|
||||
*
|
||||
* Used for side effects like removing the tenant parameter from the request route.
|
||||
*/
|
||||
public function resolved(Tenant $tenant, mixed ...$args): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible argument combinations for resolve() which can be used for caching the tenant.
|
||||
*
|
||||
* This is used during tenant cache invalidation.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
abstract public function getArgsForTenant(Tenant $tenant): array;
|
||||
abstract public function getPossibleCacheKeys(Tenant&Model $tenant): array;
|
||||
|
||||
public static function shouldCache(): bool
|
||||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.cache') ?? false;
|
||||
return (config('tenancy.identification.resolvers.' . static::class . '.cache') ?? false) && static::cacheTTL() > 0;
|
||||
}
|
||||
|
||||
public static function cacheTTL(): int
|
||||
|
|
|
|||
|
|
@ -56,21 +56,22 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
}
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
public function getPossibleCacheKeys(Tenant&Model $tenant): array
|
||||
{
|
||||
if ($tenant instanceof SingleDomainTenant) {
|
||||
/** @var SingleDomainTenant&Model $tenant */
|
||||
return [
|
||||
[$tenant->getOriginal('domain')], // Previous domain
|
||||
[$tenant->domain], // Current domain
|
||||
];
|
||||
$domains = array_filter([
|
||||
$tenant->getOriginal('domain'), // Previous domain
|
||||
$tenant->domain, // Current domain
|
||||
]);
|
||||
} else {
|
||||
/** @var Tenant&Model $tenant */
|
||||
$tenant->unsetRelation('domains');
|
||||
|
||||
$domains = $tenant->domains->map(function (Domain&Model $domain) {
|
||||
return $domain->domain;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/** @var Tenant&Model $tenant */
|
||||
$tenant->unsetRelation('domains');
|
||||
|
||||
return $tenant->domains->map(function (Domain&Model $domain) {
|
||||
return [$domain->domain];
|
||||
})->toArray();
|
||||
return array_map(fn (string $domain) => $this->formatCacheKey($domain), $domains);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Resolvers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Routing\Route;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantColumnNotWhitelistedException;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
|
||||
use Stancl\Tenancy\PathIdentificationManager;
|
||||
|
||||
class PathTenantResolver extends Contracts\CachedTenantResolver
|
||||
{
|
||||
|
|
@ -16,26 +17,30 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
/** @var Route $route */
|
||||
$route = $args[0];
|
||||
|
||||
/** @var string $id */
|
||||
$id = $route->parameter(static::tenantParameterName());
|
||||
/** @var string $key */
|
||||
$key = $route->parameter(static::tenantParameterName());
|
||||
$column = $route->bindingFieldFor(static::tenantParameterName()) ?? static::tenantModelColumn();
|
||||
|
||||
if ($id) {
|
||||
// Forget the tenant parameter so that we don't have to accept it in route action methods
|
||||
$route->forgetParameter(static::tenantParameterName());
|
||||
if ($column !== static::tenantModelColumn() && ! in_array($column, static::allowedExtraModelColumns())) {
|
||||
throw new TenantColumnNotWhitelistedException($key);
|
||||
}
|
||||
|
||||
if ($tenant = tenancy()->find($id)) {
|
||||
if ($key) {
|
||||
if ($tenant = tenancy()->find($key, $column)) {
|
||||
/** @var Tenant $tenant */
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TenantCouldNotBeIdentifiedByPathException($id);
|
||||
throw new TenantCouldNotBeIdentifiedByPathException($key);
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
public function getPossibleCacheKeys(Tenant&Model $tenant): array
|
||||
{
|
||||
return [
|
||||
[$tenant->getTenantKey()],
|
||||
];
|
||||
$columns = array_unique(array_merge(static::allowedExtraModelColumns(), [static::tenantModelColumn()]));
|
||||
$columnValuePairs = array_map(fn ($column) => [$column, $tenant->getAttribute($column)], $columns);
|
||||
|
||||
return array_map(fn ($columnValuePair) => $this->formatCacheKey(...$columnValuePair), $columnValuePairs);
|
||||
}
|
||||
|
||||
public function resolved(Tenant $tenant, mixed ...$args): void
|
||||
|
|
@ -43,21 +48,22 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
/** @var Route $route */
|
||||
$route = $args[0];
|
||||
|
||||
$route->forgetParameter(PathIdentificationManager::getTenantParameterName());
|
||||
// Forget the tenant parameter so that we don't have to accept it in route action methods
|
||||
$route->forgetParameter(static::tenantParameterName());
|
||||
}
|
||||
|
||||
public function getCacheKey(mixed ...$args): string
|
||||
public function formatCacheKey(mixed ...$args): string
|
||||
{
|
||||
// todo@samuel fix the coupling here. when this is called from the cachedresolver, $args are the tenant key. when it's called from within this class, $args are a Route instance
|
||||
// the logic shouldn't have to be coupled to where it's being called from
|
||||
// When called in resolve(), $args contains the route
|
||||
// When called in getPossibleCacheKeys(), $args contains the column-value pair
|
||||
if ($args[0] instanceof Route) {
|
||||
$column = $args[0]->bindingFieldFor(static::tenantParameterName()) ?? static::tenantModelColumn();
|
||||
$value = $args[0]->parameter(static::tenantParameterName());
|
||||
} else {
|
||||
[$column, $value] = $args;
|
||||
}
|
||||
|
||||
// todo@samuel also make the tenant column configurable
|
||||
|
||||
// $args[0] can be either a Route instance with the tenant key as a parameter
|
||||
// Or the tenant key
|
||||
$args = [$args[0] instanceof Route ? $args[0]->parameter(static::tenantParameterName()) : $args[0]];
|
||||
|
||||
return '_tenancy_resolver:' . static::class . ':' . json_encode($args);
|
||||
return parent::formatCacheKey($column, $value);
|
||||
}
|
||||
|
||||
public static function tenantParameterName(): string
|
||||
|
|
@ -69,4 +75,15 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? static::tenantParameterName() . '.';
|
||||
}
|
||||
|
||||
public static function tenantModelColumn(): string
|
||||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName();
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public static function allowedExtraModelColumns(): array
|
||||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.allowed_extra_model_columns') ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Resolvers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
|
||||
|
||||
|
|
@ -26,10 +27,10 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver
|
|||
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
public function getPossibleCacheKeys(Tenant&Model $tenant): array
|
||||
{
|
||||
return [
|
||||
[$tenant->getTenantKey()],
|
||||
$this->formatCacheKey($tenant->getTenantKey()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue