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:
commit
ba8cfdda85
38 changed files with 631 additions and 362 deletions
|
|
@ -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
|
||||
|
|
|
|||
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Enums;
|
||||
|
||||
enum LogMode
|
||||
{
|
||||
case NONE;
|
||||
case SILENT;
|
||||
case INSTANT;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
18
src/Listeners/CreateTenantStorage.php
Normal file
18
src/Listeners/CreateTenantStorage.php
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal file
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue