1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 18:44:03 +00:00

Add identification section to config, refactor static properties

This commit is contained in:
Samuel Štancl 2022-10-01 20:01:18 +02:00
parent e5bc8ddb77
commit ccaba05272
17 changed files with 153 additions and 97 deletions

View file

@ -144,16 +144,8 @@ class TenancyServiceProvider extends ServiceProvider
protected function makeTenancyMiddlewareHighestPriority() protected function makeTenancyMiddlewareHighestPriority()
{ {
$tenancyMiddleware = [
// Even higher priority than the initialization middleware // Even higher priority than the initialization middleware
Middleware\PreventAccessFromCentralDomains::class, $tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
foreach (array_reverse($tenancyMiddleware) as $middleware) { foreach (array_reverse($tenancyMiddleware) as $middleware) {
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware); $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Middleware;
use Stancl\Tenancy\Resolvers;
return [ return [
'tenant_model' => Tenant::class, 'tenant_model' => Tenant::class,
@ -21,6 +23,56 @@ return [
'localhost', 'localhost',
], ],
'identification' => [
/**
* The default middleware used for tenant identification.
*
* If you use multiple forms of identification, you can set this to the "main" approach you use.
*/
'default_middleware' => Middleware\InitializeTenancyByDomain::class,// todo@identification add this to a 'tenancy' mw group
/**
* All of the identification middleware used by the package.
*
* If you write your own, make sure to add them to this array.
*/
'middleware' => [
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
],
/**
* Tenant resolvers used by the package.
*
* Resolvers which implement the CachedTenantResolver contract have options for configuring the caching details.
* If you add your own resolvers, do not add the 'cache' key unless your resolver is based on CachedTenantResolver.
*/
'resolvers' => [
Resolvers\DomainTenantResolver::class => [
'cache' => false,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // default
],
Resolvers\PathTenantResolver::class => [
'tenant_parameter_name' => 'tenant',
'cache' => false,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // default
],
Resolvers\RequestDataTenantResolver::class => [
'cache' => false,
'cache_ttl' => 3600, // seconds
'cache_store' => null, // default
],
],
// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
],
/** /**
* Tenancy bootstrappers are executed when tenancy is initialized. * Tenancy bootstrappers are executed when tenancy is initialized.
* Their responsibility is making Laravel features tenant-aware. * Their responsibility is making Laravel features tenant-aware.

View file

@ -3,7 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Controllers\TenantAssetController;
Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
->where('path', '(.*)') ->where('path', '(.*)')
->name('stancl.tenancy.asset'); ->name('stancl.tenancy.asset');

View file

@ -4,18 +4,16 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Controllers; namespace Stancl\Tenancy\Controllers;
use Closure;
use Illuminate\Routing\Controller; use Illuminate\Routing\Controller;
use Stancl\Tenancy\Tenancy;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Throwable; use Throwable;
class TenantAssetsController extends Controller // todo rename this to TenantAssetController & update references in docs class TenantAssetController extends Controller // todo@docs this was renamed from TenantAssetsController
{ {
public static string|array|Closure $tenancyMiddleware = \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class;
public function __construct() public function __construct()
{ {
$this->middleware(static::$tenancyMiddleware); $this->middleware(Tenancy::defaultMiddleware());
} }
/** /**

View file

@ -5,22 +5,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
trait InvalidatesResolverCache trait InvalidatesResolverCache
{ {
/** @var array<class-string<CachedTenantResolver>> */
public static $resolvers = [ // todo@deprecated, move this to a config key? related to a todo in InvalidatesTenantsResolverCache
Resolvers\DomainTenantResolver::class,
Resolvers\PathTenantResolver::class,
Resolvers\RequestDataTenantResolver::class,
];
public static function bootInvalidatesResolverCache(): void public static function bootInvalidatesResolverCache(): void
{ {
static::saved(function (Tenant $tenant) { static::saved(function (Tenant $tenant) {
foreach (static::$resolvers as $resolver) { foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */ /** @var CachedTenantResolver $resolver */
$resolver = app($resolver); $resolver = app($resolver);

View file

@ -5,25 +5,18 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
/** /**
* Meant to be used on models that belong to tenants. * Meant to be used on models that belong to tenants.
*/ */
trait InvalidatesTenantsResolverCache trait InvalidatesTenantsResolverCache
{ {
/** @var array<class-string<CachedTenantResolver>> */
public static array $resolvers = [ // todo single source of truth for this here and in InvalidatesResolverCache
Resolvers\DomainTenantResolver::class,
Resolvers\PathTenantResolver::class,
Resolvers\RequestDataTenantResolver::class,
];
public static function bootInvalidatesTenantsResolverCache(): void public static function bootInvalidatesTenantsResolverCache(): void
{ {
static::saved(function (Model $model) { static::saved(function (Model $model) {
foreach (static::$resolvers as $resolver) { foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */ /** @var CachedTenantResolver $resolver */
$resolver = app($resolver); $resolver = app($resolver);

View file

@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception
{ {
public function __construct() public function __construct()
{ {
$parameter = PathTenantResolver::$tenantParameterName; $parameter = PathTenantResolver::tenantParameterName();
parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter)."); parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter).");
} }

View file

@ -34,14 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware
// Only initialize tenancy if tenant is the first parameter // Only initialize tenancy if tenant is the first parameter
// We don't want to initialize tenancy if the tenant is // We don't want to initialize tenancy if the tenant is
// simply injected into some route controller action. // simply injected into some route controller action.
if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
// Set tenant as a default parameter for the URLs in the current request $this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
/** @var Tenant $tenant */
$tenant = $event->tenancy->tenant;
URL::defaults([PathTenantResolver::$tenantParameterName => $tenant->getTenantKey()]);
});
return $this->initializeTenancy( return $this->initializeTenancy(
$request, $request,
@ -54,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware
return $next($request); return $next($request);
} }
protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void
{
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
/** @var Tenant $tenant */
$tenant = $event->tenancy->tenant;
URL::defaults([
PathTenantResolver::tenantParameterName() => $tenant->getTenantKey(),
]);
});
}
} }

View file

@ -11,23 +11,17 @@ use Stancl\Tenancy\Contracts\TenantResolver;
abstract class CachedTenantResolver implements 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 */ /** @var Repository */
protected $cache; protected $cache;
public function __construct(Factory $cache) public function __construct(Factory $cache)
{ {
$this->cache = $cache->store(static::$cacheStore); $this->cache = $cache->store(static::cacheStore());
} }
public function resolve(mixed ...$args): Tenant public function resolve(mixed ...$args): Tenant
{ {
if (! static::$shouldCache) { if (! static::shouldCache()) {
return $this->resolveWithoutCache(...$args); return $this->resolveWithoutCache(...$args);
} }
@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver
} }
$tenant = $this->resolveWithoutCache(...$args); $tenant = $this->resolveWithoutCache(...$args);
$this->cache->put($key, $tenant, static::$cacheTTL); $this->cache->put($key, $tenant, static::cacheTTL());
return $tenant; return $tenant;
} }
public function invalidateCache(Tenant $tenant): void public function invalidateCache(Tenant $tenant): void
{ {
if (! static::$shouldCache) { if (! static::shouldCache()) {
return; return;
} }
@ -75,4 +69,19 @@ abstract class CachedTenantResolver implements TenantResolver
* @return array[] * @return array[]
*/ */
abstract public function getArgsForTenant(Tenant $tenant): array; abstract public function getArgsForTenant(Tenant $tenant): array;
public static function shouldCache(): bool
{
return config('tenancy.identification.resolvers.' . static::class . '.cache') ?? false;
}
public static function cacheTTL(): int
{
return config('tenancy.identification.resolvers.' . static::class . '.cache_ttl') ?? 3600;
}
public static function cacheStore(): string|null
{
return config('tenancy.identification.resolvers.' . static::class . '.cache_store');
}
} }

View file

@ -14,12 +14,6 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
/** The model representing the domain that the tenant was identified on. */ /** The model representing the domain that the tenant was identified on. */
public static Domain $currentDomain; // todo |null? public static Domain $currentDomain; // todo |null?
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 resolveWithoutCache(mixed ...$args): Tenant
{ {
$domain = $args[0]; $domain = $args[0];

View file

@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
class PathTenantResolver extends Contracts\CachedTenantResolver class PathTenantResolver extends Contracts\CachedTenantResolver
{ {
public static 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 resolveWithoutCache(mixed ...$args): Tenant
{ {
/** @var Route $route */ /** @var Route $route */
$route = $args[0]; $route = $args[0];
if ($id = (string) $route->parameter(static::$tenantParameterName)) { if ($id = (string) $route->parameter(static::tenantParameterName())) {
$route->forgetParameter(static::$tenantParameterName); $route->forgetParameter(static::tenantParameterName());
if ($tenant = tenancy()->find($id)) { if ($tenant = tenancy()->find($id)) {
return $tenant; return $tenant;
@ -40,4 +32,9 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
[$tenant->getTenantKey()], [$tenant->getTenantKey()],
]; ];
} }
public static function tenantParameterName(): string
{
return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant';
}
} }

View file

@ -42,7 +42,7 @@ class Tenancy
} }
} }
// todo0 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property // todo1 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property
if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) { if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) {
return; return;
} }
@ -157,7 +157,7 @@ class Tenancy
$tenants = is_string($tenants) ? [$tenants] : $tenants; $tenants = is_string($tenants) ? [$tenants] : $tenants;
// Use all tenants if $tenants is falsey // Use all tenants if $tenants is falsey
$tenants = $tenants ?: $this->model()->cursor(); // todo0 phpstan thinks this isn't needed, but tests fail without it $tenants = $tenants ?: $this->model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it
$originalTenant = $this->tenant; $originalTenant = $this->tenant;
@ -177,4 +177,41 @@ class Tenancy
$this->end(); $this->end();
} }
} }
/**
* Cached tenant resolvers used by the package.
*
* @return array<class-string<Resolvers\Contracts\CachedTenantResolver>>
*/
public static function cachedResolvers(): array
{
$resolvers = config('tenancy.identification.resolvers', []);
$cachedResolvers = array_filter($resolvers, function (array $options) {
// Resolvers based on CachedTenantResolver have the 'cache' option in the resolver config
return isset($options['cache']);
});
return array_keys($cachedResolvers);
}
/**
* Tenant identification middleware used by the package.
*
* @return array<class-string<Middleware\IdentificationMiddleware>>
*/
public static function middleware(): array
{
return config('tenancy.identification.middleware', []);
}
/**
* Default tenant identification middleware used by the package.
*
* @return class-string<Middleware\IdentificationMiddleware>
*/
public static function defaultMiddleware(): string
{
return config('tenancy.identification.default_middleware', Middleware\InitializeTenancyByDomain::class);
}
} }

View file

@ -120,7 +120,7 @@ class TenancyServiceProvider extends ServiceProvider
if ($event instanceof TenancyEvent) { if ($event instanceof TenancyEvent) {
match (tenancy()->logMode()) { match (tenancy()->logMode()) {
LogMode::SILENT => tenancy()->logEvent($event), LogMode::SILENT => tenancy()->logEvent($event),
LogMode::INSTANT => dump($event), // todo0 perhaps still log LogMode::INSTANT => dump($event), // todo1 perhaps still log
default => null, default => null,
}; };
} }

View file

@ -6,9 +6,7 @@ use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
afterEach(function () { // todo@v4 test this with other resolvers as well?
DomainTenantResolver::$shouldCache = false;
});
test('tenants can be resolved using the cached resolver', function () { test('tenants can be resolved using the cached resolver', function () {
$tenant = Tenant::create(); $tenant = Tenant::create();
@ -27,14 +25,14 @@ test('the underlying resolver is not touched when using the cached resolver', fu
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = false; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => false]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
pest()->assertNotEmpty(DB::getQueryLog()); // not empty pest()->assertNotEmpty(DB::getQueryLog()); // not empty
DomainTenantResolver::$shouldCache = true; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
@ -50,7 +48,7 @@ test('cache is invalidated when the tenant is updated', function () {
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = true; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();
@ -74,7 +72,7 @@ test('cache is invalidated when a tenants domain is changed', function () {
DB::enableQueryLog(); DB::enableQueryLog();
DomainTenantResolver::$shouldCache = true; config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]);
expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
DB::flushQueryLog(); DB::flushQueryLog();

View file

@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () { beforeEach(function () {
PathTenantResolver::$tenantParameterName = 'tenant';
Route::group([ Route::group([
'prefix' => '/{tenant}', 'prefix' => '/{tenant}',
'middleware' => InitializeTenancyByPath::class, 'middleware' => InitializeTenancyByPath::class,
@ -26,11 +24,6 @@ beforeEach(function () {
}); });
}); });
afterEach(function () {
// Global state cleanup
PathTenantResolver::$tenantParameterName = 'tenant';
});
test('tenant can be identified by path', function () { test('tenant can be identified by path', function () {
Tenant::create([ Tenant::create([
'id' => 'acme', 'id' => 'acme',
@ -101,7 +94,7 @@ test('an exception is thrown when the routes first parameter is not tenant', fun
}); });
test('tenant parameter name can be customized', function () { test('tenant parameter name can be customized', function () {
PathTenantResolver::$tenantParameterName = 'team'; config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']);
Route::group([ Route::group([
'prefix' => '/{team}', 'prefix' => '/{team}',

View file

@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Controllers\TenantAssetsController;
use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
@ -21,13 +19,8 @@ beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
}); });
afterEach(function () {
// Cleanup
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class;
});
test('asset can be accessed using the url returned by the tenant asset helper', function () { test('asset can be accessed using the url returned by the tenant asset helper', function () {
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
$tenant = Tenant::create(); $tenant = Tenant::create();
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
@ -95,7 +88,7 @@ test('asset helper tenancy can be disabled', function () {
}); });
test('test asset controller returns a 404 when no path is provided', function () { test('test asset controller returns a 404 when no path is provided', function () {
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
$tenant = Tenant::create(); $tenant = Tenant::create();

View file

@ -103,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--realpath' => true, '--realpath' => true,
'--force' => true, '--force' => true,
], ],
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo0 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
'queue.connections.central' => [ 'queue.connections.central' => [
'driver' => 'sync', 'driver' => 'sync',
'central' => true, 'central' => true,