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

Merge branch 'master' into cache-prefix

This commit is contained in:
lukinovec 2023-01-31 08:17:22 +01:00 committed by GitHub
commit ba8cfdda85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 631 additions and 362 deletions

View file

@ -79,9 +79,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
}
protected static function initializeTenancyForQueue(string|int $tenantId): void
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
{
if (! $tenantId) {
if ($tenantId === null) {
// The job is not tenant-aware
if (tenancy()->initialized) {
// Tenancy was initialized, so we revert back to the central context

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Container\Container;
use Illuminate\Session\DatabaseSessionHandler;
use Illuminate\Session\SessionManager;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
/**
* This resets the database connection used by the database session driver.
*
* It runs each time tenancy is initialized or ended.
* That way the session driver always uses the current DB connection.
*/
class SessionTenancyBootstrapper implements TenancyBootstrapper
{
public function __construct(
protected Repository $config,
protected Container $container,
protected SessionManager $session,
) {
}
public function bootstrap(Tenant $tenant): void
{
$this->resetDatabaseHandler();
}
public function revert(): void
{
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
// is still bootstrapped. For that reason, we have to explicitly use the central connection
$this->resetDatabaseHandler(config('tenancy.database.central_connection'));
}
protected function resetDatabaseHandler(string $defaultConnection = null): void
{
$sessionDrivers = $this->session->getDrivers();
if (isset($sessionDrivers['database'])) {
/** @var \Illuminate\Session\Store $databaseDriver */
$databaseDriver = $sessionDrivers['database'];
$databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection));
}
}
protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler
{
// Typically returns null, so this falls back to the default DB connection
$connection = $this->config->get('session.connection') ?? $defaultConnection;
// Based on SessionManager::createDatabaseDriver
return new DatabaseSessionHandler(
$this->container->make('db')->connection($connection),
$this->config->get('session.table'),
$this->config->get('session.lifetime'),
$this->container,
);
}
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Concerns;
use Closure;
use Stancl\Tenancy\Enums\LogMode;
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
use Stancl\Tenancy\Tenancy;
// todo finish this feature
/**
* @mixin Tenancy
*/
trait Debuggable
{
protected LogMode $logMode = LogMode::NONE;
protected array $eventLog = [];
public function log(LogMode $mode = LogMode::SILENT): static
{
$this->eventLog = [];
$this->logMode = $mode;
return $this;
}
public function logMode(): LogMode
{
return $this->logMode;
}
public function getLog(): array
{
return $this->eventLog;
}
public function logEvent(TenancyEvent $event): static
{
$this->eventLog[] = ['time' => now(), 'event' => $event::class, 'tenant' => $this->tenant];
return $this;
}
public function dump(Closure $dump = null): static
{
$dump ??= dd(...);
// Dump the log if we were already logging in silent mode
// Otherwise start logging in instant mode
match ($this->logMode) {
LogMode::NONE => $this->log(LogMode::INSTANT),
LogMode::SILENT => $dump($this->eventLog),
LogMode::INSTANT => null,
};
return $this;
}
public function dd(Closure $dump = null): void
{
$dump ??= dd(...);
if ($this->logMode === LogMode::SILENT) {
$dump($this->eventLog);
} else {
$dump($this);
}
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\TenantScope;
use Stancl\Tenancy\Tenancy;
@ -13,7 +14,7 @@ use Stancl\Tenancy\Tenancy;
*/
trait BelongsToTenant
{
public function tenant()
public function tenant(): BelongsTo
{
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
}

View file

@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Events;
use Stancl\Tenancy\Tenancy;
/**
* @property string $domain
@ -28,7 +29,7 @@ class Domain extends Model implements Contracts\Domain
public function tenant(): BelongsTo
{
return $this->belongsTo(config('tenancy.models.tenant'));
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
}
protected $dispatchesEvents = [

View file

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Enums;
enum LogMode
{
case NONE;
case SILENT;
case INSTANT;
}

View file

@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Features;
use Closure;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as Router;
use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Middleware;
use Stancl\Tenancy\Tenancy;
class UniversalRoutes implements Feature
{
public static string $middlewareGroup = 'universal';
// todo docblock
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
public static array $identificationMiddlewares = [
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
];
public function bootstrap(Tenancy $tenancy): void
{
foreach (static::$identificationMiddlewares as $middleware) {
$originalOnFail = $middleware::$onFail;
$middleware::$onFail = function ($exception, $request, $next) use ($originalOnFail) {
if (static::routeHasMiddleware($request->route(), static::$middlewareGroup)) {
return $next($request);
}
if ($originalOnFail) {
return $originalOnFail($exception, $request, $next);
}
throw $exception;
};
}
}
public static function routeHasMiddleware(Route $route, string $middleware): bool
{
/** @var array $routeMiddleware */
$routeMiddleware = $route->middleware();
if (in_array($middleware, $routeMiddleware, true)) {
return true;
}
// Loop one level deep and check if the route's middleware
// groups have the searched middleware group inside them
$middlewareGroups = Router::getMiddlewareGroups();
foreach ($route->gatherMiddleware() as $inner) {
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Events\TenantCreated;
class CreateTenantStorage
{
public function handle(TenantCreated $event): void
{
$storage_path = $event->tenant->run(fn () => storage_path());
mkdir("$storage_path", 0777, true); // Create the tenant's folder inside storage/
mkdir("$storage_path/framework/cache", 0777, true); // Create /framework/cache inside the tenant's storage (used for e.g. real-time facades)
}
}

View file

@ -11,6 +11,9 @@ class DeleteTenantStorage
{
public function handle(DeletingTenant $event): void
{
// todo@lukas since this is using the 'File' facade instead of low-level PHP functions, Tenancy might affect this?
// Therefore, when Tenancy is initialized, this might look INSIDE the tenant's storage, instead of the main storage dir?
// The DeletingTenant event will be fired in the central context in 99% of cases, but sometimes it might run in the tenant context (from another tenant) so we want to make sure this works well in all contexts.
File::deleteDirectory($event->tenant->run(fn () => storage_path()));
}
}

View file

@ -22,6 +22,11 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
/** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
// Always bypass tenancy initialization when host is in central domains
return $next($request);
}
return $this->initializeTenancy(
$request,
$next,

View file

@ -28,14 +28,13 @@ class InitializeTenancyByPath extends IdentificationMiddleware
/** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{
/** @var Route $route */
$route = $request->route();
$route = $this->route($request);
// 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()) {
$this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
$this->setDefaultTenantForRouteParametersWhenInitializingTenancy();
return $this->initializeTenancy(
$request,
@ -47,7 +46,26 @@ class InitializeTenancyByPath extends IdentificationMiddleware
}
}
protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void
protected function route(Request $request): Route
{
/** @var ?Route $route */
$route = $request->route();
if (! $route) {
// Create a fake $route instance that has enough information for this middleware's needs
$route = new Route($request->method(), $request->getUri(), []);
/**
* getPathInfo() returns the path except the root domain.
* We fetch the first parameter because tenant parameter is *always* first.
*/
$route->parameters[PathTenantResolver::tenantParameterName()] = explode('/', ltrim($request->getPathInfo(), '/'))[0];
$route->parameterNames[] = PathTenantResolver::tenantParameterName();
}
return $route;
}
protected function setDefaultTenantForRouteParametersWhenInitializingTenancy(): void
{
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
/** @var Tenant $tenant */

View file

@ -27,6 +27,11 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
/** @return Response|mixed */
public function handle(Request $request, Closure $next): mixed
{
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
// Always bypass tenancy initialization when host is in central domains
return $next($request);
}
$subdomain = $this->makeSubdomain($request->getHost());
if (is_object($subdomain) && $subdomain instanceof Exception) {

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Middleware;
use Closure;
use Illuminate\Http\Request;
class PreventAccessFromCentralDomains
{
/**
* Set this property if you want to customize the on-fail behavior.
*/
public static ?Closure $abortRequest;
/** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{
if (in_array($request->getHost(), config('tenancy.central_domains'))) {
$abortRequest = static::$abortRequest ?? function () {
abort(404);
};
return $abortRequest($request, $next);
}
return $next($request);
}
}

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as Router;
// todo come up with a better name
class PreventAccessFromUnwantedDomains
{
/**
* Set this property if you want to customize the on-fail behavior.
*/
public static ?Closure $abortRequest;
/** @return \Illuminate\Http\Response|mixed */
public function handle(Request $request, Closure $next): mixed
{
/** @var Route $route */
$route = $request->route();
if ($this->routeHasMiddleware($route, 'universal')) {
return $next($request);
}
if (in_array($request->getHost(), config('tenancy.central_domains'), true)) {
$abortRequest = static::$abortRequest ?? function () {
abort(404);
};
return $abortRequest($request, $next);
}
return $next($request);
}
protected function routeHasMiddleware(Route $route, string $middleware): bool
{
/** @var array $routeMiddleware */
$routeMiddleware = $route->middleware();
if (in_array($middleware, $routeMiddleware, true)) {
return true;
}
// Loop one level deep and check if the route's middleware
// groups have the searched middleware group inside them
$middlewareGroups = Router::getMiddlewareGroups();
foreach ($route->gatherMiddleware() as $inner) {
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
return true;
}
}
return false;
}
}

View file

@ -27,9 +27,7 @@ abstract class CachedTenantResolver implements TenantResolver
$key = $this->getCacheKey(...$args);
if ($this->cache->has($key)) {
$tenant = $this->cache->get($key);
if ($tenant = $this->cache->get($key)) {
$this->resolved($tenant, ...$args);
return $tenant;

View file

@ -8,21 +8,18 @@ use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Traits\Macroable;
use Stancl\Tenancy\Concerns\Debuggable;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
class Tenancy
{
use Macroable, Debuggable;
use Macroable;
/**
* The current tenant.
*
* @var (Tenant&Model)|null
*/
public ?Tenant $tenant = null;
public (Tenant&Model)|null $tenant = null;
// todo docblock
public ?Closure $getBootstrappersUsing = null;
@ -97,9 +94,9 @@ class Tenancy
public static function model(): Tenant&Model
{
/** @var class-string<Tenant&Model> $class */
$class = config('tenancy.models.tenant');
/** @var Tenant&Model $model */
$model = new $class;
return $model;
@ -113,13 +110,9 @@ class Tenancy
/**
* Try to find a tenant using an ID.
*
* @return (Tenant&Model)|null
*/
public static function find(int|string $id): Tenant|null
public static function find(int|string $id): (Tenant&Model)|null
{
// todo update all syntax like this once we're fully on PHP 8.2
/** @var (Tenant&Model)|null */
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
return $tenant;

View file

@ -6,14 +6,11 @@ namespace Stancl\Tenancy;
use Illuminate\Cache\CacheManager;
use Illuminate\Database\Console\Migrations\FreshCommand;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\CacheManager as TenantCacheManager;
use Stancl\Tenancy\Contracts\Domain;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Enums\LogMode;
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
class TenancyServiceProvider extends ServiceProvider
@ -126,18 +123,6 @@ class TenancyServiceProvider extends ServiceProvider
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
}
Event::listen('Stancl\\Tenancy\\Events\\*', function (string $name, array $data) {
$event = $data[0];
if ($event instanceof TenancyEvent) {
match (tenancy()->logMode()) {
LogMode::SILENT => tenancy()->logEvent($event),
LogMode::INSTANT => dump($event), // todo1 perhaps still log
default => null,
};
}
});
$this->app->singleton('globalUrl', function ($app) {
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
$instance = clone $app['url'];