1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 08:04: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()
{
$tenancyMiddleware = [
// Even higher priority than the initialization middleware
Middleware\PreventAccessFromCentralDomains::class,
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
// Even higher priority than the initialization middleware
$tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
foreach (array_reverse($tenancyMiddleware) as $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\Tenant;
use Stancl\Tenancy\Middleware;
use Stancl\Tenancy\Resolvers;
return [
'tenant_model' => Tenant::class,
@ -21,6 +23,56 @@ return [
'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.
* Their responsibility is making Laravel features tenant-aware.

View file

@ -3,7 +3,8 @@
declare(strict_types=1);
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', '(.*)')
->name('stancl.tenancy.asset');

View file

@ -4,18 +4,16 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Controllers;
use Closure;
use Illuminate\Routing\Controller;
use Stancl\Tenancy\Tenancy;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
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()
{
$this->middleware(static::$tenancyMiddleware);
$this->middleware(Tenancy::defaultMiddleware());
}
/**

View file

@ -5,22 +5,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
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
{
static::saved(function (Tenant $tenant) {
foreach (static::$resolvers as $resolver) {
foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */
$resolver = app($resolver);

View file

@ -5,25 +5,18 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy;
/**
* Meant to be used on models that belong to tenants.
*/
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
{
static::saved(function (Model $model) {
foreach (static::$resolvers as $resolver) {
foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */
$resolver = app($resolver);

View file

@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception
{
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).");
}

View file

@ -34,14 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware
// Only initialize tenancy if tenant is the first parameter
// We don't want to initialize tenancy if the tenant is
// simply injected into some route controller action.
if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) {
// Set tenant as a default parameter for the URLs in the current request
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
/** @var Tenant $tenant */
$tenant = $event->tenancy->tenant;
URL::defaults([PathTenantResolver::$tenantParameterName => $tenant->getTenantKey()]);
});
if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
$this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
return $this->initializeTenancy(
$request,
@ -54,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware
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
{
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);
$this->cache = $cache->store(static::cacheStore());
}
public function resolve(mixed ...$args): Tenant
{
if (! static::$shouldCache) {
if (! static::shouldCache()) {
return $this->resolveWithoutCache(...$args);
}
@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver
}
$tenant = $this->resolveWithoutCache(...$args);
$this->cache->put($key, $tenant, static::$cacheTTL);
$this->cache->put($key, $tenant, static::cacheTTL());
return $tenant;
}
public function invalidateCache(Tenant $tenant): void
{
if (! static::$shouldCache) {
if (! static::shouldCache()) {
return;
}
@ -75,4 +69,19 @@ abstract class CachedTenantResolver implements TenantResolver
* @return 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. */
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
{
$domain = $args[0];

View file

@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
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
{
/** @var Route $route */
$route = $args[0];
if ($id = (string) $route->parameter(static::$tenantParameterName)) {
$route->forgetParameter(static::$tenantParameterName);
if ($id = (string) $route->parameter(static::tenantParameterName())) {
$route->forgetParameter(static::tenantParameterName());
if ($tenant = tenancy()->find($id)) {
return $tenant;
@ -40,4 +32,9 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
[$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()) {
return;
}
@ -157,7 +157,7 @@ class Tenancy
$tenants = is_string($tenants) ? [$tenants] : $tenants;
// 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;
@ -177,4 +177,41 @@ class Tenancy
$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) {
match (tenancy()->logMode()) {
LogMode::SILENT => tenancy()->logEvent($event),
LogMode::INSTANT => dump($event), // todo0 perhaps still log
LogMode::INSTANT => dump($event), // todo1 perhaps still log
default => null,
};
}

View file

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

View file

@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () {
PathTenantResolver::$tenantParameterName = 'tenant';
Route::group([
'prefix' => '/{tenant}',
'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 () {
Tenant::create([
'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 () {
PathTenantResolver::$tenantParameterName = 'team';
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']);
Route::group([
'prefix' => '/{team}',

View file

@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Controllers\TenantAssetsController;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant;
@ -21,13 +19,8 @@ beforeEach(function () {
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 () {
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
$tenant = Tenant::create();
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 () {
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
$tenant = Tenant::create();

View file

@ -103,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--realpath' => 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' => [
'driver' => 'sync',
'central' => true,