1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 12:54:05 +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:
Samuel Štancl 2024-03-28 03:18:11 +01:00 committed by GitHub
parent dc430666ba
commit 0c11f29c19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 370 additions and 88 deletions

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
@ -12,7 +13,7 @@ trait InvalidatesResolverCache
{
public static function bootInvalidatesResolverCache(): void
{
static::saved(function (Tenant $tenant) {
static::saved(function (Tenant&Model $tenant) {
foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */
$resolver = app($resolver);

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantColumnNotWhitelistedException extends TenantCouldNotBeIdentifiedException
{
public function __construct(int|string $tenant_id)
{
$this
->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id (column not whitelisted)")
->title('Tenant could not be identified on this route because the used column is not whitelisted.')
->description('Please add the column to the list of allowed columns in the PathTenantResolver config.');
}
}

View file

@ -13,8 +13,8 @@ class TenantCouldNotBeIdentifiedByIdException extends TenantCouldNotBeIdentified
public function __construct(int|string $tenant_id)
{
$this
->tenantCouldNotBeIdentified("by tenant id: $tenant_id")
->title('Tenant could not be identified with that ID')
->description('Are you sure the ID is correct and the tenant exists?');
->tenantCouldNotBeIdentified("by tenant key: $tenant_id")
->title('Tenant could not be identified with that key')
->description('Are you sure the key is correct and the tenant exists?');
}
}

View file

@ -11,7 +11,7 @@ class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifi
public function __construct(int|string $tenant_id)
{
$this
->tenantCouldNotBeIdentified("on path with tenant id: $tenant_id")
->tenantCouldNotBeIdentified("on path with tenant key: $tenant_id")
->title('Tenant could not be identified on this path')
->description('Did you forget to create a tenant for this path?');
}

View file

@ -39,7 +39,7 @@ class InitializeTenancyByPath extends IdentificationMiddleware implements Usable
// Only initialize tenancy if the route has the tenant parameter.
// We don't want to initialize tenancy if the tenant is
// simply injected into some route controller action.
// simply injected into some central route action.
if (in_array(PathTenantResolver::tenantParameterName(), $route->parameterNames())) {
return $this->initializeTenancy(
$request,

View file

@ -8,6 +8,7 @@ use Closure;
use Illuminate\Routing\Route;
use Stancl\Tenancy\Resolvers\PathTenantResolver;
// todo a lot of duplicate logic with PathTenantResolver, ideally remove this class
class PathIdentificationManager
{
public static Closure|null $tenantParameterName = null;

View file

@ -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

View file

@ -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);
}
}

View file

@ -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') ?? [];
}
}

View file

@ -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()),
];
}
}

View file

@ -137,10 +137,10 @@ class Tenancy
/**
* Try to find a tenant using an ID.
*/
public static function find(int|string $id): Tenant|null
public static function find(int|string $id, string $column = null): (Tenant&Model)|null
{
/** @var (Tenant&Model)|null */
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
/** @var (Tenant&Model)|null $tenant */
$tenant = static::model()->firstWhere($column ?? static::model()->getTenantKeyName(), $id);
return $tenant;
}