From 37b2a91aa9f08ba097b8ac3c00bab5284c4ab9c0 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 14 Jan 2026 11:18:15 +0100 Subject: [PATCH 1/6] [4.x] Fix URL override example in TenancyServiceProvider stub (#1426) This PR fixes the URL override example in TenancyServiceProvider stub (the commented `overrideUrlInTenantContext()` segment). If the tenant doesn't have any domain, set the root URL back to the original one. --- assets/TenancyServiceProvider.stub.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 603e44e7..1cb358de 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -167,6 +167,10 @@ class TenancyServiceProvider extends ServiceProvider // ? $tenant->domain // : $tenant->domains->first()->domain; // + // if (is_null($tenantDomain)) { + // return $originalRootUrl; + // } + // // $scheme = str($originalRootUrl)->before('://'); // // if (str_contains($tenantDomain, '.')) { From 16861d25998d9b281a9a8901e3bb41d119595a20 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 9 Mar 2026 02:07:02 +0100 Subject: [PATCH 2/6] [4.x] Make `URL::temporarySignedRoute()` respect the bypass parameter (#1438) Using `URL::temporarySignedRoute()` in tenant context with `UrlGeneratorBootstrapper` enabled doesn't work the same as `route()`. The bypass parameter doesn't actually bypass the route name prefixing. `route()` is called in the `parent::temporarySignedRoute()` call, and because the bypass parameter is removed before calling `parent::temporarySignedRoute()`, the underlying `route()` call doesn't get the bypass parameter and it ends up attempting to generate URL for a route with the name prefixed with 'tenant.'. This PR adds the bypass parameter back after `prepareRouteInputs()`, so that `parent::temporarySignedRoute()` receives it, and the underlying `route()` call respects it. Also added basic tests for the `URL::temporarySignedRoute()` behavior (the new bypass parameter test works as a regression test). --- src/Overrides/TenancyUrlGenerator.php | 10 ++++- .../UrlGeneratorBootstrapperTest.php | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index 88ae54f3..f7ed9a84 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -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); } diff --git a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php index 647422da..f089207a 100644 --- a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php +++ b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php @@ -2,6 +2,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Routing\UrlGenerator; +use Illuminate\Support\Facades\URL; use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; @@ -25,12 +26,16 @@ beforeEach(function () { Event::listen(TenancyEnded::class, RevertToCentralContext::class); TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = false; + TenancyUrlGenerator::$overrides = []; + TenancyUrlGenerator::$bypassParameter = 'central'; UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false; }); afterEach(function () { TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = false; + TenancyUrlGenerator::$overrides = []; + TenancyUrlGenerator::$bypassParameter = 'central'; UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false; }); @@ -359,3 +364,40 @@ test('both the name prefixing and the tenant parameter logic gets skipped when b expect(route('home', ['bypassParameter' => false, 'tenant' => $tenant->getTenantKey()]))->toBe($tenantRouteUrl) ->not()->toContain('bypassParameter'); }); + +test('the temporarySignedRoute method can automatically prefix the passed route name', function() { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + Route::get('/{tenant}/foo', fn () => 'foo')->name('tenant.foo')->middleware([InitializeTenancyByPath::class]); + + TenancyUrlGenerator::$prefixRouteNames = true; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // Route name ('foo') gets prefixed automatically (will be 'tenant.foo') + $tenantSignedUrl = URL::temporarySignedRoute('foo', now()->addMinutes(2), ['tenant' => $tenantKey = $tenant->getTenantKey()]); + + expect($tenantSignedUrl)->toContain("localhost/{$tenantKey}/foo"); +}); + +test('the bypass parameter works correctly with temporarySignedRoute', function() { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + Route::get('/foo', fn () => 'foo')->name('central.foo'); + + TenancyUrlGenerator::$prefixRouteNames = true; + TenancyUrlGenerator::$bypassParameter = 'central'; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // Bypass parameter allows us to generate URL for the 'central.foo' route in tenant context + $centralSignedUrl = URL::temporarySignedRoute('central.foo', now()->addMinutes(2), ['central' => true]); + + expect($centralSignedUrl) + ->toContain('localhost/foo') + ->not()->toContain('central='); // Bypass parameter gets removed from the generated URL +}); From 8f3ea6297f3b24e972a7598c01f202064edfb2f7 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Mon, 9 Mar 2026 02:11:07 +0100 Subject: [PATCH 3/6] phpstan: change InputOption syntax --- src/Commands/TenantDump.php | 2 +- src/Concerns/HasTenantOptions.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 32677efc..97f9d539 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -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()); } } diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index 5beb3268..c1ea221f 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -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'], // todo@pending should we also offer without-pending? if we add this, mention in docs + 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()); } From c4960b76cb9978aa3601d61ab5ec491d2766b1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 18 Mar 2026 19:17:28 +0100 Subject: [PATCH 4/6] [4.x] Laravel 13 support (#1443) - Update ci.yml and composer.json - Wrap single database tenancy trait scopes in whenBooted() - Update SessionSeparationTest to use laravel-cache- prefix in L13 and laravel_cache_ in <=L12. Our own prefix remains tenant_%tenant%_ (as configured in tenancy.cache.prefix). We could update this to be tenant-%tenant%- from now on for consistency with Laravel's prefixes (changed in https://github.com/laravel/framework/pull/56172) but I'm not sure yet. _ seems to read a bit better but perhaps consistency is more important. We may change this later and it can be adjusted in userland easily (since it's just a config option). --- .github/workflows/ci.yml | 1 + composer.json | 10 ++--- .../Concerns/BelongsToPrimaryModel.php | 11 ++++++ src/Database/Concerns/BelongsToTenant.php | 11 ++++++ tests/SessionSeparationTest.php | 37 ++++++++++++------- 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca7c20f6..48b1ffd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: matrix: include: - laravel: "^12.0" + - laravel: "^13.0" steps: - name: Checkout diff --git a/composer.json b/composer.json index b03e1b2f..4f843504 100644 --- a/composer.json +++ b/composer.json @@ -18,17 +18,17 @@ "require": { "php": "^8.4", "ext-json": "*", - "illuminate/support": "^12.0", - "laravel/tinker": "^2.0", + "illuminate/support": "^12.0|^13.0", + "laravel/tinker": "^2.0|^3.0", "ramsey/uuid": "^4.7.3", - "stancl/jobpipeline": "2.0.0-rc6", + "stancl/jobpipeline": "2.0.0-rc7", "stancl/virtualcolumn": "^1.5.0", "spatie/invade": "*", "laravel/prompts": "0.*" }, "require-dev": { - "laravel/framework": "^12.0", - "orchestra/testbench": "^10.0", + "laravel/framework": "^13.0", + "orchestra/testbench": "^10.0|^11.0", "league/flysystem-aws-s3-v3": "^3.12.2", "doctrine/dbal": "^3.6.0", "spatie/valuestore": "^1.2.5", diff --git a/src/Database/Concerns/BelongsToPrimaryModel.php b/src/Database/Concerns/BelongsToPrimaryModel.php index 2c8c435f..ca3ba66f 100644 --- a/src/Database/Concerns/BelongsToPrimaryModel.php +++ b/src/Database/Concerns/BelongsToPrimaryModel.php @@ -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; diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index da5dc84a..5c0f50fb 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -26,6 +26,17 @@ trait BelongsToTenant } 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. diff --git a/tests/SessionSeparationTest.php b/tests/SessionSeparationTest.php index d699bc61..6c7a8aa1 100644 --- a/tests/SessionSeparationTest.php +++ b/tests/SessionSeparationTest.php @@ -23,6 +23,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Tests\Etc\Tenant; + use function Stancl\Tenancy\Tests\pest; // todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup @@ -100,7 +101,7 @@ test('redis sessions are separated using the redis bootstrapper', function (bool expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled); expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) { - return str($key)->startsWith("tenant_{$tenant->id}_laravel_cache_"); + return str($key)->startsWith(formatLaravelCacheKey(prefix: "tenant_{$tenant->id}_")); }))->toHaveCount($bootstrappedEnabled ? 1 : 0); })->with([true, false]); @@ -118,13 +119,13 @@ test('redis sessions are separated using the cache bootstrapper', function (bool Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar'); pest()->get("/{$tenant->id}/foo"); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions); tenancy()->end(); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_'); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey()); expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) { - return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}"); + return str($key)->startsWith(formatLaravelCacheKey(prefix: 'foo', suffix: "tenant_{$tenant->id}")); }))->toHaveCount($scopeSessions ? 1 : 0); })->with([true, false]); @@ -148,14 +149,14 @@ test('memcached sessions are separated using the cache bootstrapper', function ( Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar'); pest()->get("/{$tenant->id}/foo"); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions); tenancy()->end(); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_'); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey()); sleep(1.1); // 1s+ sleep is necessary for getAllKeys() to work. if this causes race conditions or we want to avoid the delay, we can refactor this to some type of a mock expect(array_filter($allMemcachedKeys(), function (string $key) use ($tenant) { - return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}"); + return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}")); }))->toHaveCount($scopeSessions ? 1 : 0); Artisan::call('cache:clear memcached'); @@ -177,13 +178,13 @@ test('dynamodb sessions are separated using the cache bootstrapper', function (b Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar'); pest()->get("/{$tenant->id}/foo"); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions); tenancy()->end(); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_'); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey()); expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) { - return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}"); + return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}")); }))->toHaveCount($scopeSessions ? 1 : 0); })->with([true, false]); @@ -202,13 +203,13 @@ test('apc sessions are separated using the cache bootstrapper', function (bool $ Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar'); pest()->get("/{$tenant->id}/foo"); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === formatLaravelCacheKey("tenant_{$tenant->id}_"))->toBe($scopeSessions); tenancy()->end(); - expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_'); + expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe(formatLaravelCacheKey()); expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) { - return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}"); + return str($key)->startsWith(formatLaravelCacheKey("tenant_{$tenant->id}")); }))->toHaveCount($scopeSessions ? 1 : 0); })->with([true, false]); @@ -250,3 +251,13 @@ test('database sessions are separated regardless of whether the session bootstra // [false, true], // when the connection IS set, the session bootstrapper becomes necessary [false, false], ]); + +function formatLaravelCacheKey(string $suffix = '', string $prefix = ''): string +{ + // todo@release if we drop Laravel 12 support we can just switch to - syntax everywhere + if (version_compare(app()->version(), '13.0.0') >= 0) { + return $prefix . 'laravel-cache-' . $suffix; + } else { + return $prefix . 'laravel_cache_' . $suffix; + } +} From fb654e7a6bb3a6163ffd2362d7f9f1b770e559b7 Mon Sep 17 00:00:00 2001 From: Samuel Mwangi Date: Mon, 30 Mar 2026 10:44:53 +0300 Subject: [PATCH 5/6] [4.x] Update Pest to v4 (#1430) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f843504..180cbaab 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "league/flysystem-aws-s3-v3": "^3.12.2", "doctrine/dbal": "^3.6.0", "spatie/valuestore": "^1.2.5", - "pestphp/pest": "^3.0", + "pestphp/pest": "^4.0", "larastan/larastan": "^3.0", "league/flysystem-path-prefixing": "^3.0" }, From 60dd5226c44adeb3e005f137f41bd67adf2a7d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 8 Apr 2026 19:21:43 +0200 Subject: [PATCH 6/6] [4.x] Add Tenancy::reinitialize() method (#1449) Some bootstrappers read attributes of the tenant during bootstrap() but don't respond to changes made to the tenant afterwards. Therefore, when making changes to the tenant that'd affect the behavior of a bootstrapper, it's necessary to reinitialize tenancy (if it matters that changes are reflected immediately). This adds a convenience helper for that purpose. --- src/Tenancy.php | 20 +++++++++++++++++++ tests/AutomaticModeTest.php | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/Tenancy.php b/src/Tenancy.php index f9c9c9ae..a2271bed 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -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 { diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php index fbeb06fc..599d14d9 100644 --- a/tests/AutomaticModeTest.php +++ b/tests/AutomaticModeTest.php @@ -103,6 +103,33 @@ test('central helper doesnt change tenancy state when called in central context' expect(tenant())->toBeNull(); }); +test('reinitialize method does nothing in the central context', function () { + expect(tenancy()->initialized)->toBe(false); + expect(fn () => tenancy()->reinitialize())->not()->toThrow(\Throwable::class); + expect(tenancy()->initialized)->toBe(false); +}); + +test('reinitialize method runs bootstrappers again for the current tenant', function () { + config(['tenancy.bootstrappers' => [ + ReinitBootstrapper::class, + ]]); + + tenancy()->initialize($tenant = Tenant::create(['reinit_bootstrapper_key' => 'foo'])); + + expect(tenant()->getKey())->toBe($tenant->getKey()); + expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo'); + + $tenant->update(['reinit_bootstrapper_key' => 'bar']); + + // Unchanged until we reinitialize... + expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo'); + + tenancy()->reinitialize(); + + expect(tenant()->getKey())->toBe($tenant->getKey()); + expect(app('tenancy_reinit_bootstrapper_key'))->toBe('bar'); +}); + class MyBootstrapper implements TenancyBootstrapper { public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void @@ -115,3 +142,16 @@ class MyBootstrapper implements TenancyBootstrapper app()->instance('tenancy_ended', true); } } + +class ReinitBootstrapper implements TenancyBootstrapper +{ + public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void + { + app()->instance('tenancy_reinit_bootstrapper_key', $tenant->getAttribute('reinit_bootstrapper_key')); + } + + public function revert(): void + { + app()->instance('tenancy_reinit_bootstrapper_key', null); + } +}