mirror of
https://github.com/archtechx/tenancy.git
synced 2026-05-07 06:14:03 +00:00
Merge branch 'master' into add-log-bootstrapper
This commit is contained in:
commit
39fc72bea5
57 changed files with 1127 additions and 221 deletions
|
|
@ -30,6 +30,8 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
* By providing a callback to shouldClone(), you can change how it's determined if a route should be cloned if you don't want to use middleware flags.
|
||||
*
|
||||
* Cloned routes are prefixed with '/{tenant}', flagged with 'tenant' middleware, and have their names prefixed with 'tenant.'.
|
||||
* The addition of the 'tenant' middleware can be controlled using addTenantMiddleware(array). You can specify the identification
|
||||
* middleware to be used on the cloned route using that method -- instead of using the approach that "inherits" it from a universal route.
|
||||
*
|
||||
* The addition of the tenant parameter can be controlled using addTenantParameter(true|false). Note that if you decide to disable
|
||||
* tenant parameter addition, the routes MUST differ in domains. This can be controlled using the domain(string|null) method. The
|
||||
|
|
@ -39,7 +41,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
* Routes with names that are already prefixed won't be cloned - but that's just the default behavior.
|
||||
* The cloneUsing() method allows you to completely override the default behavior and customize how the cloned routes will be defined.
|
||||
*
|
||||
* After cloning, only top-level middleware in $cloneRoutesWithMiddleware will be removed
|
||||
* After cloning, only top-level middleware in $cloneRoutesWithMiddleware (as well as any route context flags) will be removed
|
||||
* from the new route (so by default, 'clone' will be omitted from the new route's MW).
|
||||
* Middleware groups are preserved as-is, even if they contain cloning middleware.
|
||||
*
|
||||
|
|
@ -71,7 +73,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
* // cloned route can be customized using domain(string|null). By default, the cloned route will not be scoped to a domain,
|
||||
* // unless a domain() call is used. It's important to keep in mind that:
|
||||
* // 1. When addTenantParameter(false) is used, the paths will be the same, thus domains must differ.
|
||||
* // 2. If the original route (with the same path) has no domain, the cloned route will never be used due to registration order.
|
||||
* // 2. If the original route has no domain, the cloned route will override the original route as they will directly conflict.
|
||||
* $cloneAction->addTenantParameter(false)->cloneRoutesWithMiddleware(['clone'])->cloneRoute('no-tenant-parameter')->handle();
|
||||
* ```
|
||||
*
|
||||
|
|
@ -84,27 +86,50 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
*/
|
||||
class CloneRoutesAsTenant
|
||||
{
|
||||
/** @var list<Route|string> */
|
||||
protected array $routesToClone = [];
|
||||
|
||||
protected bool $addTenantParameter = true;
|
||||
protected bool $tenantParameterBeforePrefix = true;
|
||||
protected string|null $domain = null;
|
||||
protected Closure|null $cloneUsing = null; // The callback should accept Route instance or the route name (string)
|
||||
|
||||
/**
|
||||
* The callback should accept a Route instance or the route name (string).
|
||||
*
|
||||
* @var ?Closure(Route|string): void
|
||||
*/
|
||||
protected Closure|null $cloneUsing = null;
|
||||
|
||||
/** @var ?Closure(Route): bool */
|
||||
protected Closure|null $shouldClone = null;
|
||||
|
||||
/** @var list<string> */
|
||||
protected array $cloneRoutesWithMiddleware = ['clone'];
|
||||
|
||||
/** @var list<string> */
|
||||
protected array $addTenantMiddleware = ['tenant'];
|
||||
|
||||
public function __construct(
|
||||
protected Router $router,
|
||||
) {}
|
||||
|
||||
public static function make(): static
|
||||
{
|
||||
return app(static::class);
|
||||
}
|
||||
|
||||
/** Clone routes. This resets routesToClone() but not other config. */
|
||||
public function handle(): void
|
||||
{
|
||||
// If no routes were specified using cloneRoute(), get all routes
|
||||
// and for each, determine if it should be cloned
|
||||
if (! $this->routesToClone) {
|
||||
$this->routesToClone = collect($this->router->getRoutes()->get())
|
||||
/** @var list<Route> */
|
||||
$routesToClone = collect($this->router->getRoutes()->get())
|
||||
->filter(fn (Route $route) => $this->shouldBeCloned($route))
|
||||
->all();
|
||||
|
||||
$this->routesToClone = $routesToClone;
|
||||
}
|
||||
|
||||
foreach ($this->routesToClone as $route) {
|
||||
|
|
@ -118,7 +143,9 @@ class CloneRoutesAsTenant
|
|||
|
||||
if (is_string($route)) {
|
||||
$this->router->getRoutes()->refreshNameLookups();
|
||||
$route = $this->router->getRoutes()->getByName($route);
|
||||
$routeName = $route;
|
||||
$route = $this->router->getRoutes()->getByName($routeName);
|
||||
assert(! is_null($route), "Route [{$routeName}] was meant to be cloned but does not exist.");
|
||||
}
|
||||
|
||||
$this->createNewRoute($route);
|
||||
|
|
@ -143,6 +170,20 @@ class CloneRoutesAsTenant
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tenant middleware to be added to the cloned route.
|
||||
*
|
||||
* If used with early identification, make sure to include 'tenant' in this array.
|
||||
*
|
||||
* @param list<string> $middleware
|
||||
*/
|
||||
public function addTenantMiddleware(array $middleware): static
|
||||
{
|
||||
$this->addTenantMiddleware = $middleware;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** The domain the cloned route should use. Set to null if it shouldn't be scoped to a domain. */
|
||||
public function domain(string|null $domain): static
|
||||
{
|
||||
|
|
@ -151,7 +192,11 @@ class CloneRoutesAsTenant
|
|||
return $this;
|
||||
}
|
||||
|
||||
/** Provide a custom callback for cloning routes, instead of the default behavior. */
|
||||
/**
|
||||
* Provide a custom callback for cloning routes, instead of the default behavior.
|
||||
*
|
||||
* @param ?Closure(Route|string): void $cloneUsing
|
||||
*/
|
||||
public function cloneUsing(Closure|null $cloneUsing): static
|
||||
{
|
||||
$this->cloneUsing = $cloneUsing;
|
||||
|
|
@ -159,7 +204,11 @@ class CloneRoutesAsTenant
|
|||
return $this;
|
||||
}
|
||||
|
||||
/** Specify which middleware should serve as "flags" telling this action to clone those routes. */
|
||||
/**
|
||||
* Specify which middleware should serve as "flags" telling this action to clone those routes.
|
||||
*
|
||||
* @param list<string> $middleware
|
||||
*/
|
||||
public function cloneRoutesWithMiddleware(array $middleware): static
|
||||
{
|
||||
$this->cloneRoutesWithMiddleware = $middleware;
|
||||
|
|
@ -170,7 +219,9 @@ class CloneRoutesAsTenant
|
|||
/**
|
||||
* Provide a custom callback for determining whether a route should be cloned.
|
||||
* Overrides the default middleware-based detection.
|
||||
* */
|
||||
*
|
||||
* @param Closure(Route): bool $shouldClone
|
||||
*/
|
||||
public function shouldClone(Closure|null $shouldClone): static
|
||||
{
|
||||
$this->shouldClone = $shouldClone;
|
||||
|
|
@ -193,6 +244,18 @@ class CloneRoutesAsTenant
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone individual routes.
|
||||
*
|
||||
* @param list<Route|string> $routes
|
||||
*/
|
||||
public function cloneRoutes(array $routes): static
|
||||
{
|
||||
$this->routesToClone = array_merge($this->routesToClone, $routes);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function shouldBeCloned(Route $route): bool
|
||||
{
|
||||
// Don't clone routes that already have tenant parameter or prefix
|
||||
|
|
@ -258,17 +321,15 @@ class CloneRoutesAsTenant
|
|||
return $newRoute;
|
||||
}
|
||||
|
||||
/** Removes top-level cloneRoutesWithMiddleware and adds 'tenant' middleware. */
|
||||
/** Removes top-level cloneRoutesWithMiddleware and context flags, adds 'tenant' middleware. */
|
||||
protected function processMiddlewareForCloning(array $middleware): array
|
||||
{
|
||||
$processedMiddleware = array_filter(
|
||||
$middleware,
|
||||
fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware)
|
||||
fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware) && ! in_array($mw, ['central', 'tenant', 'universal'])
|
||||
);
|
||||
|
||||
$processedMiddleware[] = 'tenant';
|
||||
|
||||
return array_unique($processedMiddleware);
|
||||
return array_unique(array_merge($processedMiddleware, $this->addTenantMiddleware));
|
||||
}
|
||||
|
||||
/** Check if route already has tenant parameter or name prefix. */
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use Stancl\Tenancy\Overrides\TenancyBroadcastManager;
|
|||
class BroadcastingConfigBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfigBootstrapper).
|
||||
*
|
||||
* For example:
|
||||
* [
|
||||
|
|
|
|||
|
|
@ -102,14 +102,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
|
|||
if ($this->config->get('tenancy.cache.scope_sessions', true)) {
|
||||
// These are the only cache driven session backends (see Laravel's config/session.php)
|
||||
if (! in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)) {
|
||||
if (app()->environment('production')) {
|
||||
// We only throw this exception in prod to make configuration a little easier. Developers
|
||||
// may have scope_sessions set to true while using different session drivers e.g. in tests.
|
||||
// Previously we just silently ignored this, however since session scoping is of high importance
|
||||
// in production, we make sure to notify the developer, by throwing an exception, that session
|
||||
// scoping isn't happening as expected/configured due to an incompatible session driver.
|
||||
throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_session');
|
||||
}
|
||||
throw new Exception('Session driver [' . $this->config->get('session.driver') . '] cannot be scoped by tenancy.cache.scope_sessions');
|
||||
} else {
|
||||
// Scoping sessions using this bootstrapper implicitly adds the session store to $names
|
||||
$names[] = $this->getSessionCacheStoreName();
|
||||
|
|
|
|||
|
|
@ -63,13 +63,17 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
|
|||
$stores = $this->scopedStoreNames();
|
||||
|
||||
foreach ($stores as $storeName) {
|
||||
$this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection");
|
||||
$this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection");
|
||||
$this->originalConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.connection") ?? config('tenancy.database.central_connection');
|
||||
$this->originalLockConnections[$storeName] = $this->config->get("cache.stores.{$storeName}.lock_connection") ?? config('tenancy.database.central_connection');
|
||||
|
||||
$this->config->set("cache.stores.{$storeName}.connection", 'tenant');
|
||||
$this->config->set("cache.stores.{$storeName}.lock_connection", 'tenant');
|
||||
|
||||
$this->cache->purge($storeName);
|
||||
/** @var DatabaseStore $store */
|
||||
$store = $this->cache->store($storeName)->getStore();
|
||||
|
||||
$store->setConnection(DB::connection('tenant'));
|
||||
$store->setLockConnection(DB::connection('tenant'));
|
||||
}
|
||||
|
||||
if (static::$adjustGlobalCacheManager) {
|
||||
|
|
@ -78,8 +82,8 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
|
|||
// *from here* being executed repeatedly in a loop on reinitialization. For that reason we do not do that
|
||||
// (this is our only use of $adjustCacheManagerUsing anyway) but ideally at some point we'd have a better solution.
|
||||
$originalConnections = array_combine($stores, array_map(fn (string $storeName) => [
|
||||
'connection' => $this->originalConnections[$storeName] ?? config('tenancy.database.central_connection'),
|
||||
'lockConnection' => $this->originalLockConnections[$storeName] ?? config('tenancy.database.central_connection'),
|
||||
'connection' => $this->originalConnections[$storeName],
|
||||
'lockConnection' => $this->originalLockConnections[$storeName],
|
||||
], $stores));
|
||||
|
||||
TenancyServiceProvider::$adjustCacheManagerUsing = static function (CacheManager $manager) use ($originalConnections) {
|
||||
|
|
@ -100,7 +104,11 @@ class DatabaseCacheBootstrapper implements TenancyBootstrapper
|
|||
$this->config->set("cache.stores.{$storeName}.connection", $originalConnection);
|
||||
$this->config->set("cache.stores.{$storeName}.lock_connection", $this->originalLockConnections[$storeName]);
|
||||
|
||||
$this->cache->purge($storeName);
|
||||
/** @var DatabaseStore $store */
|
||||
$store = $this->cache->store($storeName)->getStore();
|
||||
|
||||
$store->setConnection(DB::connection($this->originalConnections[$storeName]));
|
||||
$store->setLockConnection(DB::connection($this->originalLockConnections[$storeName]));
|
||||
}
|
||||
|
||||
TenancyServiceProvider::$adjustCacheManagerUsing = null;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Session\FileSessionHandler;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
|
|
@ -22,13 +21,6 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
) {
|
||||
$this->originalAssetUrl = $this->app['config']['app.asset_url'];
|
||||
$this->originalStoragePath = $app->storagePath();
|
||||
|
||||
$this->app['url']->macro('setAssetRoot', function ($root) {
|
||||
/** @var UrlGenerator $this */
|
||||
$this->assetRoot = $root;
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
|
|
@ -78,6 +70,15 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
return;
|
||||
}
|
||||
|
||||
$path = $suffix
|
||||
? $this->tenantStoragePath($suffix) . '/framework/cache'
|
||||
: $this->originalStoragePath . '/framework/cache';
|
||||
|
||||
if (! is_dir($path)) {
|
||||
// Create tenant framework/cache directory if it does not exist
|
||||
mkdir($path, 0750, true);
|
||||
}
|
||||
|
||||
if ($suffix === false) {
|
||||
$this->app->useStoragePath($this->originalStoragePath);
|
||||
} else {
|
||||
|
|
@ -98,22 +99,33 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
if ($suffix === false) {
|
||||
$this->app['config']['app.asset_url'] = $this->originalAssetUrl;
|
||||
$this->app['url']->setAssetRoot($this->originalAssetUrl);
|
||||
$this->app['url']->useAssetOrigin($this->originalAssetUrl);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->originalAssetUrl) {
|
||||
$this->app['config']['app.asset_url'] = $this->originalAssetUrl . "/$suffix";
|
||||
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
|
||||
$this->app['url']->useAssetOrigin($this->app['config']['app.asset_url']);
|
||||
} else {
|
||||
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
|
||||
$this->app['url']->useAssetOrigin($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
|
||||
}
|
||||
}
|
||||
|
||||
protected function forgetDisks(): void
|
||||
{
|
||||
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
|
||||
$tenantDisks = $this->app['config']['tenancy.filesystem.disks'];
|
||||
$scopedDisks = [];
|
||||
|
||||
foreach ($this->app['config']['filesystems.disks'] as $name => $disk) {
|
||||
if (isset($disk['driver'], $disk['disk'])
|
||||
&& $disk['driver'] === 'scoped'
|
||||
&& in_array($disk['disk'], $tenantDisks, true)) {
|
||||
$scopedDisks[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
Storage::forgetDisk(array_merge($tenantDisks, $scopedDisks));
|
||||
}
|
||||
|
||||
protected function diskRoot(string $disk, Tenant|false $tenant): void
|
||||
|
|
@ -211,7 +223,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
if (! is_dir($path)) {
|
||||
// Create tenant framework/sessions directory if it does not exist
|
||||
mkdir($path, 0755, true);
|
||||
mkdir($path, 0750, true);
|
||||
}
|
||||
|
||||
$this->app['config']['session.files'] = $path;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use Stancl\Tenancy\Contracts\Tenant;
|
|||
class MailConfigBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfigBootstrapper).
|
||||
*
|
||||
* For example:
|
||||
* [
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Bootstrappers;
|
|||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||
|
|
@ -78,6 +79,10 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
|||
}
|
||||
}
|
||||
|
||||
// Inherit scheme (http/https) from the original generator
|
||||
$originalScheme = Str::before($this->originalUrlGenerator->formatScheme(), '://');
|
||||
$newGenerator->forceScheme($originalScheme);
|
||||
|
||||
$newGenerator->defaults($defaultParameters);
|
||||
|
||||
$newGenerator->setSessionResolver(function () {
|
||||
|
|
|
|||
38
src/Commands/PurgeImpersonationTokens.php
Normal file
38
src/Commands/PurgeImpersonationTokens.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Stancl\Tenancy\Features\UserImpersonation;
|
||||
|
||||
/**
|
||||
* Clears expired impersonation tokens.
|
||||
*
|
||||
* Tokens older than UserImpersonation::$ttl are considered expired.
|
||||
*
|
||||
* @see Stancl\Tenancy\Features\UserImpersonation
|
||||
*/
|
||||
class PurgeImpersonationTokens extends Command
|
||||
{
|
||||
protected $signature = 'tenants:purge-impersonation-tokens';
|
||||
|
||||
protected $description = 'Clear expired impersonation tokens.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->components->info('Deleting expired impersonation tokens.');
|
||||
|
||||
$expirationDate = now()->subSeconds(UserImpersonation::$ttl);
|
||||
|
||||
$impersonationTokenModel = UserImpersonation::modelClass();
|
||||
|
||||
$deletedTokenCount = $impersonationTokenModel::where('created_at', '<', $expirationDate)
|
||||
->delete();
|
||||
|
||||
$this->components->info($deletedTokenCount . ' expired impersonation ' . str('token')->plural($deletedTokenCount) . ' deleted.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ class TenantDump extends DumpCommand
|
|||
protected function getOptions(): array
|
||||
{
|
||||
return array_merge([
|
||||
['tenant', null, InputOption::VALUE_OPTIONAL, '', null],
|
||||
new InputOption('tenant', null, InputOption::VALUE_OPTIONAL, '', null),
|
||||
], parent::getOptions());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ trait HasTenantOptions
|
|||
protected function getOptions()
|
||||
{
|
||||
return array_merge([
|
||||
['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null],
|
||||
['with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'],
|
||||
new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null),
|
||||
new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs
|
||||
], parent::getOptions());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ trait ManagesRLSPolicies
|
|||
$policies = static::getRLSPolicies($table);
|
||||
|
||||
foreach ($policies as $policy) {
|
||||
DB::statement('DROP POLICY ? ON ?', [$policy, $table]);
|
||||
DB::statement("DROP POLICY {$policy} ON {$table}");
|
||||
}
|
||||
|
||||
return count($policies);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ trait BelongsToPrimaryModel
|
|||
abstract public function getRelationshipToPrimaryModel(): string;
|
||||
|
||||
public static function bootBelongsToPrimaryModel(): void
|
||||
{
|
||||
if (method_exists(static::class, 'whenBooted')) {
|
||||
// Laravel 13
|
||||
// For context see https://github.com/calebporzio/sushi/commit/62ff7f432cac736cb1da9f46d8f471cb78914b92
|
||||
static::whenBooted(fn () => static::configureBelongsToPrimaryModelScope());
|
||||
} else {
|
||||
static::configureBelongsToPrimaryModelScope();
|
||||
}
|
||||
}
|
||||
|
||||
protected static function configureBelongsToPrimaryModelScope()
|
||||
{
|
||||
$implicitRLS = config('tenancy.rls.manager') === TraitRLSManager::class && TraitRLSManager::$implicitRLS;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,26 @@ trait BelongsToTenant
|
|||
{
|
||||
use FillsCurrentTenant;
|
||||
|
||||
/**
|
||||
* @return BelongsTo<\Illuminate\Database\Eloquent\Model&\Stancl\Tenancy\Contracts\Tenant, $this>
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||
}
|
||||
|
||||
public static function bootBelongsToTenant(): void
|
||||
{
|
||||
if (method_exists(static::class, 'whenBooted')) {
|
||||
// Laravel 13
|
||||
// For context see https://github.com/calebporzio/sushi/commit/62ff7f432cac736cb1da9f46d8f471cb78914b92
|
||||
static::whenBooted(fn () => static::configureBelongsToTenantScope());
|
||||
} else {
|
||||
static::configureBelongsToTenantScope();
|
||||
}
|
||||
}
|
||||
|
||||
protected static function configureBelongsToTenantScope(): void
|
||||
{
|
||||
// If TraitRLSManager::$implicitRLS is true or this model implements RLSModel
|
||||
// Postgres RLS is used for scoping, so we don't enable the scope used with single-database tenancy.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Stancl\Tenancy\Contracts\Domain;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
|
|
@ -14,7 +15,10 @@ use Stancl\Tenancy\Tenancy;
|
|||
*/
|
||||
trait HasDomains
|
||||
{
|
||||
public function domains()
|
||||
/**
|
||||
* @return HasMany<\Illuminate\Database\Eloquent\Model&\Stancl\Tenancy\Contracts\Domain, $this>
|
||||
*/
|
||||
public function domains(): HasMany
|
||||
{
|
||||
return $this->hasMany(config('tenancy.models.domain'), Tenancy::tenantKeyColumn());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,13 +49,15 @@ trait HasPending
|
|||
*/
|
||||
public static function createPending(array $attributes = []): Model&Tenant
|
||||
{
|
||||
$tenant = null;
|
||||
|
||||
try {
|
||||
$tenant = static::create($attributes);
|
||||
$tenant = static::create(array_merge(static::getPendingAttributes($attributes), $attributes));
|
||||
event(new CreatingPendingTenant($tenant));
|
||||
} finally {
|
||||
// Update the pending_since value only after the tenant is created so it's
|
||||
// not marked as pending until after migrations, seeders, etc are run.
|
||||
$tenant->update([
|
||||
$tenant?->update([
|
||||
'pending_since' => now()->timestamp,
|
||||
]);
|
||||
}
|
||||
|
|
@ -65,6 +67,17 @@ trait HasPending
|
|||
return $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes to be set when a pending tenant is initially created.
|
||||
*
|
||||
* @param array<string, mixed> $attributes The attributes passed to createPending() (will be merged with the returned array)
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getPendingAttributes(array $attributes): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a pending tenant from the pool or create a new one if the pool is empty.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -10,13 +10,6 @@ use Illuminate\Database\Eloquent\Scope;
|
|||
|
||||
class PendingScope implements Scope
|
||||
{
|
||||
/**
|
||||
* All of the extensions to be added to the builder.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $extensions = ['WithPending', 'WithoutPending', 'OnlyPending'];
|
||||
|
||||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
*
|
||||
|
|
@ -32,26 +25,21 @@ class PendingScope implements Scope
|
|||
}
|
||||
|
||||
/**
|
||||
* Extend the query builder with the needed functions.
|
||||
* Add methods to the query builder.
|
||||
*
|
||||
* @param Builder<\Stancl\Tenancy\Contracts\Tenant&Model> $builder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function extend(Builder $builder)
|
||||
public function extend(Builder $builder): void
|
||||
{
|
||||
foreach ($this->extensions as $extension) {
|
||||
$this->{"add{$extension}"}($builder);
|
||||
}
|
||||
$this->addWithPending($builder);
|
||||
$this->addWithoutPending($builder);
|
||||
$this->addOnlyPending($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the with-pending extension to the builder.
|
||||
*
|
||||
* @param Builder<\Stancl\Tenancy\Contracts\Tenant&Model> $builder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addWithPending(Builder $builder)
|
||||
protected function addWithPending(Builder $builder): void
|
||||
{
|
||||
$builder->macro('withPending', function (Builder $builder, $withPending = true) {
|
||||
if (! $withPending) {
|
||||
|
|
@ -63,13 +51,9 @@ class PendingScope implements Scope
|
|||
}
|
||||
|
||||
/**
|
||||
* Add the without-pending extension to the builder.
|
||||
*
|
||||
* @param Builder<\Stancl\Tenancy\Contracts\Tenant&Model> $builder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addWithoutPending(Builder $builder)
|
||||
protected function addWithoutPending(Builder $builder): void
|
||||
{
|
||||
$builder->macro('withoutPending', function (Builder $builder) {
|
||||
$builder->withoutGlobalScope(static::class)
|
||||
|
|
@ -81,13 +65,9 @@ class PendingScope implements Scope
|
|||
}
|
||||
|
||||
/**
|
||||
* Add the only-pending extension to the builder.
|
||||
*
|
||||
* @param Builder<\Stancl\Tenancy\Contracts\Tenant&Model> $builder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addOnlyPending(Builder $builder)
|
||||
protected function addOnlyPending(Builder $builder): void
|
||||
{
|
||||
$builder->macro('onlyPending', function (Builder $builder) {
|
||||
$builder->withoutGlobalScope(static::class)->whereNotNull($builder->getModel()->getColumnForQuery('pending_since'));
|
||||
|
|
|
|||
|
|
@ -44,12 +44,20 @@ class UserImpersonation implements Feature
|
|||
|
||||
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
|
||||
|
||||
abort_if($tokenExpired, 403);
|
||||
if ($tokenExpired) {
|
||||
$token->delete();
|
||||
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$tokenTenantId = (string) $token->getAttribute(Tenancy::tenantKeyColumn());
|
||||
$currentTenantId = (string) tenant()->getTenantKey();
|
||||
|
||||
abort_unless($tokenTenantId === $currentTenantId, 403);
|
||||
if ($tokenTenantId !== $currentTenantId) {
|
||||
$token->delete();
|
||||
|
||||
abort(403);
|
||||
}
|
||||
|
||||
Auth::guard($token->auth_guard)->loginUsingId($token->user_id, $token->remember);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,25 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||
|
||||
/**
|
||||
* Can be used to manually create framework directories in the tenant storage when storage_path() is scoped.
|
||||
*
|
||||
* Useful when using real-time facades which use the framework/cache directory.
|
||||
*
|
||||
* Generally not needed anymore as the directory is also created by the FilesystemTenancyBootstrapper.
|
||||
*/
|
||||
class CreateTenantStorage
|
||||
{
|
||||
public function handle(TenantCreated $event): void
|
||||
public function handle(TenantEvent $event): void
|
||||
{
|
||||
$storage_path = tenancy()->run($event->tenant, fn () => storage_path());
|
||||
$cache_path = "$storage_path/framework/cache";
|
||||
|
||||
if (! is_dir($cache_path)) {
|
||||
// Create the tenant's storage directory and /framework/cache within (used for e.g. real-time facades)
|
||||
mkdir($cache_path, 0777, true);
|
||||
mkdir($cache_path, 0750, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Stancl\Tenancy\Events\DeletingTenant;
|
||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||
|
||||
class DeleteTenantStorage
|
||||
{
|
||||
public function handle(DeletingTenant $event): void
|
||||
public function handle(TenantEvent $event): void
|
||||
{
|
||||
$path = tenancy()->run($event->tenant, fn () => storage_path());
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,15 @@ class TenancyUrlGenerator extends UrlGenerator
|
|||
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
||||
}
|
||||
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type
|
||||
$wrappedParameters = Arr::wrap($parameters);
|
||||
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, $wrappedParameters); // @phpstan-ignore argument.type
|
||||
|
||||
if (isset($wrappedParameters[static::$bypassParameter])) {
|
||||
// If the bypass parameter was passed, we need to add it back to the parameters after prepareRouteInputs() removes it,
|
||||
// so that the underlying route() call in parent::temporarySignedRoute() can bypass the behavior modification as well.
|
||||
$parameters[static::$bypassParameter] = $wrappedParameters[static::$bypassParameter];
|
||||
}
|
||||
|
||||
return parent::temporarySignedRoute($name, $expiration, $parameters, $absolute);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class CentralResourceNotAvailableInPivotException extends Exception
|
|||
parent::__construct(
|
||||
'Central resource is not accessible in pivot model.
|
||||
To attach a resource to a tenant, use $centralResource->tenants()->attach($tenant) instead of $tenant->resources()->attach($centralResource) (same for detaching).
|
||||
To make this work both ways, you can make your pivot implement PivotWithRelation and return the related model in getRelatedModel() or extend MorphPivot.'
|
||||
To make this work both ways, you can make your pivot implement PivotWithCentralResource and return the related model in getCentralResourceClass() or extend MorphPivot.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/ResourceSyncing/Events/SyncedResourceDeleted.php
Normal file
18
src/ResourceSyncing/Events/SyncedResourceDeleted.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
|
||||
class SyncedResourceDeleted
|
||||
{
|
||||
public function __construct(
|
||||
public Syncable&Model $model,
|
||||
public TenantWithDatabase|null $tenant,
|
||||
public bool $forceDelete,
|
||||
) {}
|
||||
}
|
||||
40
src/ResourceSyncing/Listeners/DeleteAllTenantMappings.php
Normal file
40
src/ResourceSyncing/Listeners/DeleteAllTenantMappings.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
|
||||
/**
|
||||
* Cleans up pivot records related to the deleted tenant.
|
||||
*
|
||||
* The listener only cleans up the pivot tables specified
|
||||
* in the $pivotTables property (see the property for details),
|
||||
* and is intended for use with tables that do not have tenant
|
||||
* foreign key constraints with onDelete('cascade').
|
||||
*/
|
||||
class DeleteAllTenantMappings extends QueueableListener
|
||||
{
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
/**
|
||||
* Pivot tables to clean up after a tenant is deleted, in the
|
||||
* ['table_name' => 'tenant_key_column'] format.
|
||||
*
|
||||
* Since we cannot automatically detect which pivot tables
|
||||
* are being used, they have to be specified here manually.
|
||||
*
|
||||
* The default value follows the polymorphic table used by default.
|
||||
*/
|
||||
public static array $pivotTables = ['tenant_resources' => 'tenant_id'];
|
||||
|
||||
public function handle(TenantDeleted $event): void
|
||||
{
|
||||
foreach (static::$pivotTables as $table => $tenantKeyColumn) {
|
||||
DB::table($table)->where($tenantKeyColumn, $event->tenant->getKey())->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/ResourceSyncing/Listeners/DeleteResourceMapping.php
Normal file
60
src/ResourceSyncing/Listeners/DeleteResourceMapping.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
/**
|
||||
* Deletes pivot records when a synced resource is deleted.
|
||||
*
|
||||
* If a SyncMaster (central resource) is deleted, all pivot records for that resource are deleted.
|
||||
* If a Syncable (tenant resource) is deleted, only delete the pivot record for that tenant.
|
||||
*/
|
||||
class DeleteResourceMapping extends QueueableListener
|
||||
{
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle(SyncedResourceDeleted $event): void
|
||||
{
|
||||
$centralResource = $this->getCentralResource($event->model);
|
||||
|
||||
if (! $centralResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete pivot records if the central resource doesn't use soft deletes
|
||||
// or the central resource was deleted using forceDelete()
|
||||
if ($event->forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
|
||||
Pivot::withoutEvents(function () use ($centralResource, $event) {
|
||||
// If detach() is called with null -- if $event->tenant is null -- this means a central resource was deleted and detaches all tenants.
|
||||
// If detach() is called with a specific tenant, it means the resource was deleted in that tenant, and we only delete that single mapping.
|
||||
$centralResource->tenants()->detach($event->tenant);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getCentralResource(Syncable&Model $resource): SyncMaster|null
|
||||
{
|
||||
if ($resource instanceof SyncMaster) {
|
||||
return $resource;
|
||||
}
|
||||
|
||||
$centralResourceClass = $resource->getCentralModelName();
|
||||
|
||||
/** @var (SyncMaster&Model)|null $centralResource */
|
||||
$centralResource = $centralResourceClass::firstWhere(
|
||||
$resource->getGlobalIdentifierKeyName(),
|
||||
$resource->getGlobalIdentifierKey()
|
||||
);
|
||||
|
||||
return $centralResource;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
|
||||
|
|
@ -21,12 +20,6 @@ class DeleteResourcesInTenants extends QueueableListener
|
|||
|
||||
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) {
|
||||
$this->deleteSyncedResource($centralResource, $forceDelete);
|
||||
|
||||
// Delete pivot records if the central resource doesn't use soft deletes
|
||||
// or the central resource was deleted using forceDelete()
|
||||
if ($forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
|
||||
$centralResource->tenants()->detach(tenant());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
src/ResourceSyncing/PivotWithCentralResource.php
Normal file
11
src/ResourceSyncing/PivotWithCentralResource.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
interface PivotWithCentralResource
|
||||
{
|
||||
/** @return class-string<\Illuminate\Database\Eloquent\Model&Syncable> */
|
||||
public function getCentralResourceClass(): string;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
interface PivotWithRelation
|
||||
{
|
||||
/**
|
||||
* E.g. return $this->users()->getModel().
|
||||
*/
|
||||
public function getRelatedModel(): Model;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
|||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
||||
|
|
@ -19,37 +20,34 @@ trait ResourceSyncing
|
|||
{
|
||||
public static function bootResourceSyncing(): void
|
||||
{
|
||||
static::saved(function (Syncable&Model $model) {
|
||||
static::saved(static function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && ($model->wasRecentlyCreated || $model->wasChanged($model->getSyncedAttributeNames()))) {
|
||||
$model->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
static::deleted(static function (Syncable&Model $model) {
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerDeleteEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (Syncable&Model $model) {
|
||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||
$model->setAttribute(
|
||||
$model->getGlobalIdentifierKeyName(),
|
||||
app(UniqueIdentifierGenerator::class)->generate($model)
|
||||
);
|
||||
static::creating(static function (Syncable&Model $model) {
|
||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName())) {
|
||||
$model->generateGlobalIdentifierKey();
|
||||
}
|
||||
});
|
||||
|
||||
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
||||
static::forceDeleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
static::forceDeleting(static function (Syncable&Model $model) {
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerDeleteEvent(true);
|
||||
}
|
||||
});
|
||||
|
||||
static::restoring(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
$model->triggerRestoredEvent();
|
||||
static::restoring(static function (Syncable&Model $model) {
|
||||
if ($model instanceof SyncMaster && $model->shouldSync()) {
|
||||
$model->triggerRestoreEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -67,9 +65,11 @@ trait ResourceSyncing
|
|||
/** @var SyncMaster&Model $this */
|
||||
event(new SyncMasterDeleted($this, $forceDelete));
|
||||
}
|
||||
|
||||
event(new SyncedResourceDeleted($this, tenant(), $forceDelete));
|
||||
}
|
||||
|
||||
public function triggerRestoredEvent(): void
|
||||
public function triggerRestoreEvent(): void
|
||||
{
|
||||
if ($this instanceof SyncMaster && in_array(SoftDeletes::class, class_uses_recursive($this), true)) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
|
|
@ -105,6 +105,9 @@ trait ResourceSyncing
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<\Illuminate\Database\Eloquent\Model&\Stancl\Tenancy\Database\Contracts\TenantWithDatabase, $this>
|
||||
*/
|
||||
public function tenants(): BelongsToMany
|
||||
{
|
||||
return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', $this->getGlobalIdentifierKeyName())
|
||||
|
|
@ -116,8 +119,18 @@ trait ResourceSyncing
|
|||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKey(): string
|
||||
public function getGlobalIdentifierKey(): string|int
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
|
||||
protected function generateGlobalIdentifierKey(): void
|
||||
{
|
||||
if (! app()->bound(UniqueIdentifierGenerator::class)) return;
|
||||
|
||||
$this->setAttribute(
|
||||
$this->getGlobalIdentifierKeyName(),
|
||||
app(UniqueIdentifierGenerator::class)->generate($this),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,5 @@ interface SyncMaster extends Syncable
|
|||
|
||||
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
||||
|
||||
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||
|
||||
public function triggerRestoredEvent(): void;
|
||||
public function triggerRestoreEvent(): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ interface Syncable
|
|||
|
||||
public function triggerSyncEvent(): void;
|
||||
|
||||
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||
|
||||
/**
|
||||
* Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Stancl\Tenancy\ResourceSyncing;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
|
||||
|
|
@ -20,14 +21,14 @@ trait TriggerSyncingEvents
|
|||
{
|
||||
public static function bootTriggerSyncingEvents(): void
|
||||
{
|
||||
static::saving(function (self $pivot) {
|
||||
static::saving(static function (self $pivot) {
|
||||
// Try getting the central resource to see if it is available
|
||||
// If it is not available, throw an exception to interrupt the saving process
|
||||
// And prevent creating a pivot record without a central resource
|
||||
$pivot->getCentralResourceAndTenant();
|
||||
});
|
||||
|
||||
static::saved(function (self $pivot) {
|
||||
static::saved(static function (self $pivot) {
|
||||
/**
|
||||
* @var static&Pivot $pivot
|
||||
* @var SyncMaster|null $centralResource
|
||||
|
|
@ -40,7 +41,7 @@ trait TriggerSyncingEvents
|
|||
}
|
||||
});
|
||||
|
||||
static::deleting(function (self $pivot) {
|
||||
static::deleting(static function (self $pivot) {
|
||||
/**
|
||||
* @var static&Pivot $pivot
|
||||
* @var SyncMaster|null $centralResource
|
||||
|
|
@ -79,13 +80,13 @@ trait TriggerSyncingEvents
|
|||
*/
|
||||
protected function getResourceClass(): string
|
||||
{
|
||||
/** @var $this&(Pivot|MorphPivot|((Pivot|MorphPivot)&PivotWithRelation)) $this */
|
||||
if ($this instanceof PivotWithRelation) {
|
||||
return $this->getRelatedModel()::class;
|
||||
/** @var $this&(Pivot|MorphPivot|((Pivot|MorphPivot)&PivotWithCentralResource)) $this */
|
||||
if ($this instanceof PivotWithCentralResource) {
|
||||
return $this->getCentralResourceClass();
|
||||
}
|
||||
|
||||
if ($this instanceof MorphPivot) {
|
||||
return $this->morphClass;
|
||||
return Relation::getMorphedModel($this->morphClass) ?? $this->morphClass;
|
||||
}
|
||||
|
||||
throw new CentralResourceNotAvailableInPivotException;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,26 @@ class Tenancy
|
|||
$this->initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* End tenancy and initialize it again for the current tenant.
|
||||
*
|
||||
* This can be helpful when changing "dependencies" of bootstrappers such as
|
||||
* attributes of the current tenant that are only read once, during bootstrap().
|
||||
*
|
||||
* If tenancy is not initialized, this method is a no-op.
|
||||
*/
|
||||
public function reinitialize(): void
|
||||
{
|
||||
if ($this->tenant === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenant = $this->tenant;
|
||||
$this->end();
|
||||
|
||||
$this->initialize($tenant);
|
||||
}
|
||||
|
||||
/** @return TenancyBootstrapper[] */
|
||||
public function getBootstrappers(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy;
|
|||
|
||||
use Closure;
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
use Illuminate\Routing\Events\RouteMatched;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -119,6 +120,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Commands\MigrateFresh::class,
|
||||
Commands\ClearPendingTenants::class,
|
||||
Commands\CreatePendingTenants::class,
|
||||
Commands\PurgeImpersonationTokens::class,
|
||||
Commands\CreateUserWithRLSPolicies::class,
|
||||
]);
|
||||
|
||||
|
|
@ -156,12 +158,13 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
|
||||
}
|
||||
|
||||
$this->app->singleton('globalUrl', function ($app) {
|
||||
$this->app->singleton('globalUrl', function (Container $app) {
|
||||
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
|
||||
$instance = clone $app['url'];
|
||||
$instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalAssetUrl);
|
||||
/** @var \Illuminate\Routing\UrlGenerator */
|
||||
$instance = clone $app->make('url');
|
||||
$instance->useAssetOrigin($app->make(FilesystemTenancyBootstrapper::class)->originalAssetUrl);
|
||||
} else {
|
||||
$instance = $app['url'];
|
||||
$instance = $app->make('url');
|
||||
}
|
||||
|
||||
return $instance;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Illuminate\Support\Str;
|
|||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Generates a UUID for the tenant key.
|
||||
* Generates a ULID for the tenant key.
|
||||
*/
|
||||
class ULIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid;
|
|||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Generates a UUID for the tenant key.
|
||||
* Generates a UUIDv4 for the tenant key.
|
||||
*/
|
||||
class UUIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
|
|
|
|||
20
src/UniqueIdentifierGenerators/UUIDv7Generator.php
Normal file
20
src/UniqueIdentifierGenerators/UUIDv7Generator.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Generates a UUIDv7 for the tenant key.
|
||||
*/
|
||||
class UUIDv7Generator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return Str::uuid7()->toString();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue