├── sprout.png ├── src ├── Exceptions │ ├── DeprecatedException.php │ ├── SproutException.php │ ├── TenancyMissingException.php │ ├── TenantMissingException.php │ ├── NoTenantFoundException.php │ ├── TenantMismatchException.php │ ├── ServiceOverrideException.php │ ├── TenantRelationException.php │ ├── CompatibilityException.php │ └── MisconfigurationException.php ├── Database │ └── Eloquent │ │ ├── Contracts │ │ └── OptionalTenant.php │ │ ├── Tenant.php │ │ ├── Concerns │ │ ├── BelongsToTenant.php │ │ ├── BelongsToManyTenants.php │ │ ├── HasTenantResources.php │ │ ├── IsTenant.php │ │ └── IsTenantChild.php │ │ └── Scopes │ │ ├── TenantChildScope.php │ │ ├── BelongsToTenantScope.php │ │ └── BelongsToManyTenantsScope.php ├── Support │ ├── Services.php │ ├── Settings.php │ ├── ResolutionHook.php │ ├── BaseTenantProvider.php │ ├── PlaceholderHelper.php │ ├── SettingsRepository.php │ ├── GenericTenant.php │ ├── ResolutionHelper.php │ └── BaseIdentityResolver.php ├── Concerns │ ├── AwareOfSprout.php │ ├── AwareOfApp.php │ └── AwareOfTenant.php ├── Attributes │ ├── TenantRelation.php │ ├── CurrentTenancy.php │ ├── CurrentTenant.php │ ├── Override.php │ ├── Tenancy.php │ ├── Provider.php │ └── Resolver.php ├── Events │ ├── ServiceOverrideBooted.php │ ├── ServiceOverrideRegistered.php │ ├── TenantLoaded.php │ ├── TenantIdentified.php │ ├── ServiceOverrideEvent.php │ ├── TenantFound.php │ └── CurrentTenantChanged.php ├── Contracts │ ├── TenantHasResources.php │ ├── BootableServiceOverride.php │ ├── IdentityResolverUsesParameters.php │ ├── TenantAware.php │ ├── Tenant.php │ ├── ServiceOverride.php │ ├── TenantProvider.php │ ├── IdentityResolver.php │ └── Tenancy.php ├── Listeners │ ├── PerformIdentityResolverSetup.php │ ├── RefreshTenantAwareDependencies.php │ ├── SetCurrentTenantContext.php │ ├── SetupServiceOverrides.php │ ├── CleanupServiceOverrides.php │ ├── SetCurrentTenantForJob.php │ └── IdentifyTenantOnRouting.php ├── Facades │ ├── Tenancies.php │ ├── Providers.php │ ├── Resolvers.php │ ├── Overrides.php │ └── Sprout.php ├── Overrides │ ├── JobOverride.php │ ├── FilesystemManagerOverride.php │ ├── Session │ │ ├── SproutFileSessionHandlerCreator.php │ │ ├── SproutDatabaseSessionHandlerCreator.php │ │ ├── SproutFileSessionHandler.php │ │ └── SproutDatabaseSessionHandler.php │ ├── CookieOverride.php │ ├── Filesystem │ │ └── SproutFilesystemManager.php │ ├── AuthGuardOverride.php │ ├── BaseOverride.php │ ├── CacheOverride.php │ ├── Auth │ │ └── SproutAuthPasswordBrokerManager.php │ ├── AuthPasswordOverride.php │ ├── FilesystemOverride.php │ └── Cache │ │ └── SproutCacheDriverCreator.php ├── Http │ ├── RouteCreator.php │ ├── RouterMethods.php │ ├── Middleware │ │ ├── AddTenantHeaderToResponse.php │ │ ├── SproutOptionalTenantContextMiddleware.php │ │ └── SproutTenantContextMiddleware.php │ └── Resolvers │ │ └── HeaderIdentityResolver.php ├── helpers.php ├── Managers │ ├── TenancyManager.php │ └── TenantProviderManager.php ├── TenancyOptions.php └── Providers │ └── EloquentTenantProvider.php ├── testbench.yaml ├── CHANGELOG.md ├── LICENSE.md ├── resources └── config │ ├── overrides.php │ ├── core.php │ └── multitenancy.php ├── composer.json └── README.md /sprout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sprout-laravel/sprout/HEAD/sprout.png -------------------------------------------------------------------------------- /src/Exceptions/DeprecatedException.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 18 | 19 | return $this; 20 | } 21 | 22 | public function getSprout(): Sprout 23 | { 24 | return $this->sprout; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Attributes/TenantRelation.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @package Overrides 16 | * 17 | * @codeCoverageIgnore 18 | */ 19 | final class ServiceOverrideBooted extends ServiceOverrideEvent 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | providers: 2 | - Sprout\SproutServiceProvider 3 | 4 | migrations: 5 | - workbench/database/migrations 6 | 7 | seeders: 8 | - Workbench\Database\Seeders\DatabaseSeeder 9 | 10 | workbench: 11 | start: '/' 12 | install: true 13 | health: false 14 | discovers: 15 | web: false 16 | api: false 17 | commands: false 18 | components: false 19 | views: false 20 | build: 21 | - asset-publish 22 | - create-sqlite-db 23 | - migrate:refresh 24 | assets: 25 | - sprout-config 26 | sync: [] 27 | -------------------------------------------------------------------------------- /src/Support/Settings.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | 19 | return $this; 20 | } 21 | 22 | public function getApp(): Application 23 | { 24 | return $this->app; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Tenant.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * @package Overrides 17 | * 18 | * @codeCoverageIgnore 19 | */ 20 | final class ServiceOverrideRegistered extends ServiceOverrideEvent 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/TenantLoaded.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * @package Core 18 | * 19 | * @codeCoverageIgnore 20 | */ 21 | final readonly class TenantLoaded extends TenantFound 22 | { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/TenantIdentified.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * @package Core 18 | * 19 | * @codeCoverageIgnore 20 | */ 21 | final readonly class TenantIdentified extends TenantFound 22 | { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Support/ResolutionHook.php: -------------------------------------------------------------------------------- 1 | $event 24 | * 25 | * @return void 26 | */ 27 | public function handle(CurrentTenantChanged $event): void 28 | { 29 | $event->tenancy->resolver()?->setup($event->tenancy, $event->current); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exceptions/TenantMismatchException.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @package Core 19 | */ 20 | abstract class BaseTenantProvider implements TenantProvider 21 | { 22 | /** 23 | * @var string 24 | */ 25 | private string $name; 26 | 27 | /** 28 | * Create a new instance of the tenant provider 29 | * 30 | * @param string $name 31 | */ 32 | public function __construct(string $name) 33 | { 34 | $this->name = $name; 35 | } 36 | 37 | /** 38 | * Get the registered name of the provider 39 | * 40 | * @return string 41 | */ 42 | public function getName(): string 43 | { 44 | return $this->name; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ollie Read 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Attributes/CurrentTenancy.php: -------------------------------------------------------------------------------- 1 | |null 32 | */ 33 | public function resolve(CurrentTenancy $attribute, Container $container): ?Tenancy 34 | { 35 | return sprout()->getCurrentTenancy(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/ServiceOverrideException.php: -------------------------------------------------------------------------------- 1 | app = $app; 29 | } 30 | 31 | /** 32 | * @template TenantClass of \Sprout\Contracts\Tenant 33 | * 34 | * @param \Sprout\Events\CurrentTenantChanged $event 35 | * 36 | * @return void 37 | */ 38 | public function handle(CurrentTenantChanged $event): void 39 | { 40 | if ($event->current !== null) { 41 | $this->app->forgetExtenders(Tenant::class); 42 | $this->app->extend(Tenant::class, fn (?Tenant $tenant) => $tenant); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Listeners/SetCurrentTenantContext.php: -------------------------------------------------------------------------------- 1 | $event 24 | * 25 | * @return void 26 | */ 27 | public function handle(CurrentTenantChanged $event): void 28 | { 29 | $contextKey = 'sprout.tenants'; 30 | $context = []; 31 | 32 | if (Context::has($contextKey)) { 33 | /** @var array $context */ 34 | $context = Context::get($contextKey, []); 35 | } 36 | 37 | if ($event->current === null) { 38 | unset($context[$event->tenancy->getName()]); 39 | } else { 40 | $context[$event->tenancy->getName()] = $event->current->getTenantKey(); 41 | } 42 | 43 | Context::add($contextKey, $context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Contracts/IdentityResolverUsesParameters.php: -------------------------------------------------------------------------------- 1 | $tenancy 24 | * 25 | * @return string 26 | */ 27 | public function getRouteParameterName(Tenancy $tenancy): string; 28 | 29 | /** 30 | * Get an identifier from the route 31 | * 32 | * Locates a tenant identifier within the provided route and returns it. 33 | * 34 | * @template TenantClass of \Sprout\Contracts\Tenant 35 | * 36 | * @param \Illuminate\Routing\Route $route 37 | * @param \Sprout\Contracts\Tenancy $tenancy 38 | * @param \Illuminate\Http\Request $request 39 | * 40 | * @return string|null 41 | */ 42 | public function resolveFromRoute(Route $route, Tenancy $tenancy, Request $request): ?string; 43 | } 44 | -------------------------------------------------------------------------------- /src/Facades/Overrides.php: -------------------------------------------------------------------------------- 1 | overrides = $overrides; 32 | } 33 | 34 | /** 35 | * Handle the event 36 | * 37 | * @template TenantClass of \Sprout\Contracts\Tenant 38 | * 39 | * @param \Sprout\Events\CurrentTenantChanged $event 40 | * 41 | * @return void 42 | */ 43 | public function handle(CurrentTenantChanged $event): void 44 | { 45 | // If there's no current tenant, we aren't interested 46 | if ($event->current === null) { 47 | return; 48 | } 49 | 50 | $this->overrides->setupOverrides($event->tenancy, $event->current); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Scopes/TenantChildScope.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $extensions = []; 26 | 27 | /** 28 | * Extend the query builder with the necessary macros 29 | * 30 | * @template ModelClass of \Illuminate\Database\Eloquent\Model 31 | * 32 | * @param \Illuminate\Database\Eloquent\Builder $builder 33 | * 34 | * @return void 35 | */ 36 | public function extend(Builder $builder): void 37 | { 38 | $builder->macro('withoutTenants', function (Builder $builder) { 39 | /** @phpstan-ignore-next-line */ 40 | return $builder->withoutGlobalScope($this); 41 | }); 42 | 43 | foreach ($this->extensions as $macro => $method) { 44 | $builder->macro($macro, $this->$method(...)); // @codeCoverageIgnore 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Overrides/JobOverride.php: -------------------------------------------------------------------------------- 1 | listen(JobProcessing::class, SetCurrentTenantForJob::class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Contracts/TenantAware.php: -------------------------------------------------------------------------------- 1 | |null 44 | */ 45 | public function getTenancy(): ?Tenancy; 46 | 47 | /** 48 | * Check if there is a tenancy 49 | * 50 | * @return bool 51 | */ 52 | public function hasTenancy(): bool; 53 | 54 | /** 55 | * Set the tenancy 56 | * 57 | * @template TenantClass of \Sprout\Contracts\Tenant 58 | * @param \Sprout\Contracts\Tenancy|null $tenancy 59 | * 60 | * @return static 61 | */ 62 | public function setTenancy(?Tenancy $tenancy): static; 63 | } 64 | -------------------------------------------------------------------------------- /src/Events/ServiceOverrideEvent.php: -------------------------------------------------------------------------------- 1 | service = $service; 51 | $this->override = $override; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Overrides/FilesystemManagerOverride.php: -------------------------------------------------------------------------------- 1 | resolved('filesystem')) { 32 | // We'll grab the manager 33 | $original = $app->make('filesystem'); 34 | // and then tell the container to forget it 35 | $app->forgetInstance('filesystem'); 36 | } 37 | 38 | // Bind a replacement filesystem manager to enable Sprout features 39 | $app->singleton('filesystem', fn ($app) => new SproutFilesystemManager($app, $original)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Contracts/Tenant.php: -------------------------------------------------------------------------------- 1 | overrides = $overrides; 32 | } 33 | 34 | /** 35 | * Handle event 36 | * 37 | * @template TenantClass of \Sprout\Contracts\Tenant 38 | * 39 | * @param \Sprout\Events\CurrentTenantChanged $event 40 | * 41 | * @return void 42 | * 43 | * @throws \Sprout\Exceptions\ServiceOverrideException 44 | */ 45 | public function handle(CurrentTenantChanged $event): void 46 | { 47 | // If there's no previous tenant, we aren't interested 48 | if ($event->previous === null) { 49 | return; 50 | } 51 | 52 | $this->overrides->cleanupOverrides($event->tenancy, $event->previous); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Listeners/SetCurrentTenantForJob.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 35 | $this->tenancies = $tenancies; 36 | } 37 | 38 | public function handle(JobProcessing $event): void 39 | { 40 | /** @var array $tenants */ 41 | $tenants = Context::get('sprout.tenants', []); 42 | 43 | /** 44 | * @var string $tenancyName 45 | * @var int|string $key 46 | */ 47 | foreach ($tenants as $tenancyName => $key) { 48 | /** @var \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy */ 49 | $tenancy = $this->tenancies->get($tenancyName); 50 | 51 | // It's always the key, so we load instead of identifying 52 | $tenancy->load($key); 53 | 54 | $this->sprout->setCurrentTenancy($tenancy); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/config/overrides.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'driver' => \Sprout\Overrides\StackedOverride::class, 24 | 'overrides' => [ 25 | \Sprout\Overrides\FilesystemManagerOverride::class, 26 | \Sprout\Overrides\FilesystemOverride::class, 27 | ], 28 | ], 29 | 30 | 'job' => [ 31 | 'driver' => \Sprout\Overrides\JobOverride::class, 32 | ], 33 | 34 | 'cache' => [ 35 | 'driver' => \Sprout\Overrides\CacheOverride::class, 36 | ], 37 | 38 | 'auth' => [ 39 | 'driver' => \Sprout\Overrides\StackedOverride::class, 40 | 'overrides' => [ 41 | \Sprout\Overrides\AuthGuardOverride::class, 42 | \Sprout\Overrides\AuthPasswordOverride::class, 43 | ], 44 | ], 45 | 46 | 'cookie' => [ 47 | 'driver' => \Sprout\Overrides\CookieOverride::class, 48 | ], 49 | 50 | 'session' => [ 51 | 'driver' => \Sprout\Overrides\SessionOverride::class, 52 | 'database' => false, 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /src/Attributes/CurrentTenant.php: -------------------------------------------------------------------------------- 1 | tenancy = $tenancy; 40 | } 41 | 42 | /** 43 | * Resolve the tenant using this attribute 44 | * 45 | * @param \Sprout\Attributes\CurrentTenant $tenant 46 | * @param \Illuminate\Container\Container $container 47 | * 48 | * @return \Sprout\Contracts\Tenant|null 49 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 50 | */ 51 | public function resolve(CurrentTenant $tenant, Container $container): ?Tenant 52 | { 53 | /** 54 | * It's not nullable, it'll be an exception 55 | * 56 | * @noinspection NullPointerExceptionInspection 57 | */ 58 | return $container->make(TenancyManager::class)->get($this->tenancy)->tenant(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Attributes/Override.php: -------------------------------------------------------------------------------- 1 | service = $service; 40 | } 41 | 42 | /** 43 | * Resolve the tenancy using this attribute 44 | * 45 | * @param \Sprout\Attributes\Override $attribute 46 | * @param \Illuminate\Container\Container $container 47 | * 48 | * @return \Sprout\Contracts\ServiceOverride|null 49 | * 50 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 51 | */ 52 | public function resolve(Override $attribute, Container $container): ?ServiceOverride 53 | { 54 | /** 55 | * It's not nullable, it'll be an exception 56 | * 57 | * @noinspection NullPointerExceptionInspection 58 | */ 59 | return $container->make(ServiceOverrideManager::class)->get($this->service); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/config/core.php: -------------------------------------------------------------------------------- 1 | [ 17 | // \Sprout\Support\ResolutionHook::Booting, 18 | \Sprout\Support\ResolutionHook::Routing, 19 | \Sprout\Support\ResolutionHook::Middleware, 20 | ], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | The event listeners used to bootstrap a tenancy 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This value contains all the listeners that should be run for the 28 | | \Sprout\Events\CurrentTenantChanged event to bootstrap a tenancy. 29 | | 30 | */ 31 | 32 | 'bootstrappers' => [ 33 | // Set the current tenant within the Laravel context 34 | \Sprout\Listeners\SetCurrentTenantContext::class, 35 | // Calls the setup method on the current identity resolver 36 | \Sprout\Listeners\PerformIdentityResolverSetup::class, 37 | // Performs any clean-up from the previous tenancy 38 | \Sprout\Listeners\CleanupServiceOverrides::class, 39 | // Sets up service overrides for the current tenancy 40 | \Sprout\Listeners\SetupServiceOverrides::class, 41 | // Refresh anything that's tenant-aware 42 | \Sprout\Listeners\RefreshTenantAwareDependencies::class, 43 | ], 44 | 45 | ]; 46 | -------------------------------------------------------------------------------- /src/Overrides/Session/SproutFileSessionHandlerCreator.php: -------------------------------------------------------------------------------- 1 | app = $app; 29 | $this->sprout = $sprout; 30 | } 31 | 32 | /** 33 | * Create the tenant-aware session file driver 34 | * 35 | * @return \Sprout\Overrides\Session\SproutFileSessionHandler 36 | * 37 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 38 | */ 39 | public function __invoke(): SproutFileSessionHandler 40 | { 41 | /** @var string $originalPath */ 42 | $originalPath = config('session.files'); 43 | $path = rtrim($originalPath, '/'); 44 | 45 | /** @var int $lifetime */ 46 | $lifetime = config('session.lifetime'); 47 | 48 | $handler = new SproutFileSessionHandler( 49 | $this->app->make('files'), 50 | $path, 51 | $lifetime 52 | ); 53 | 54 | if ($this->sprout->withinContext()) { 55 | $tenancy = $this->sprout->getCurrentTenancy(); 56 | 57 | $handler->setTenancy($tenancy); 58 | $handler->setTenant($tenancy?->tenant()); 59 | } 60 | 61 | return $handler; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Attributes/Tenancy.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | } 39 | 40 | /** 41 | * Resolve the tenancy using this attribute 42 | * 43 | * @param \Sprout\Attributes\Tenancy $attribute 44 | * @param \Illuminate\Container\Container $container 45 | * 46 | * @return \Sprout\Contracts\Tenancy<*>|null 47 | * 48 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 49 | * @throws \Sprout\Exceptions\MisconfigurationException 50 | */ 51 | public function resolve(Tenancy $attribute, Container $container): ?TenancyContract 52 | { 53 | /** 54 | * It's not nullable, it'll be an exception 55 | * 56 | * @noinspection NullPointerExceptionInspection 57 | */ 58 | return $container->make(TenancyManager::class)->get($this->name); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Attributes/Provider.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | } 39 | 40 | /** 41 | * Resolve the tenancy using this attribute 42 | * 43 | * @param \Sprout\Attributes\Provider $attribute 44 | * @param \Illuminate\Container\Container $container 45 | * 46 | * @return \Sprout\Contracts\TenantProvider<*>|null 47 | * 48 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 49 | * @throws \Sprout\Exceptions\MisconfigurationException 50 | */ 51 | public function resolve(Provider $attribute, Container $container): ?TenantProvider 52 | { 53 | /** 54 | * It's not nullable, it'll be an exception 55 | * 56 | * @noinspection NullPointerExceptionInspection 57 | */ 58 | return $container->make(TenantProviderManager::class)->get($this->name); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Attributes/Resolver.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | } 39 | 40 | /** 41 | * Resolve the tenancy using this attribute 42 | * 43 | * @param \Sprout\Attributes\Resolver $attribute 44 | * @param \Illuminate\Container\Container $container 45 | * 46 | * @return \Sprout\Contracts\IdentityResolver|null 47 | * 48 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 49 | * @throws \Sprout\Exceptions\MisconfigurationException 50 | */ 51 | public function resolve(Resolver $attribute, Container $container): ?IdentityResolver 52 | { 53 | /** 54 | * It's not nullable, it'll be an exception 55 | * 56 | * @noinspection NullPointerExceptionInspection 57 | */ 58 | return $container->make(IdentityResolverManager::class)->get($this->name); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Http/RouteCreator.php: -------------------------------------------------------------------------------- 1 | make(IdentityResolverManager::class)->get($resolver); 23 | 24 | if ($optional && $resolverInstance instanceof IdentityResolverUsesParameters) { 25 | throw CompatibilityException::optionalMiddleware($resolverInstance->getName()); 26 | } 27 | 28 | $tenancyInstance = app()->make(TenancyManager::class)->get($tenancy); 29 | $middleware = $optional ? SproutOptionalTenantContextMiddleware::ALIAS : SproutTenantContextMiddleware::ALIAS; 30 | $options = [$resolverInstance->getName(), $tenancyInstance->getName()]; 31 | 32 | return Route::middleware([$middleware . ':' . implode(',', $options)]) 33 | ->group(function (Router $router) use ($routes, $resolverInstance, $tenancyInstance) { 34 | $registrar = new RouteRegistrar($router); 35 | 36 | $resolverInstance->configureRoute($registrar, $tenancyInstance); 37 | 38 | $registrar->group($routes); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Overrides/Session/SproutDatabaseSessionHandlerCreator.php: -------------------------------------------------------------------------------- 1 | app = $app; 29 | $this->sprout = $sprout; 30 | } 31 | 32 | /** 33 | * Create the tenant-aware session database driver 34 | * 35 | * @return \Sprout\Overrides\Session\SproutDatabaseSessionHandler 36 | * 37 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 38 | */ 39 | public function __invoke(): SproutDatabaseSessionHandler 40 | { 41 | $table = config('session.table'); 42 | $lifetime = config('session.lifetime'); 43 | $connection = config('session.connection'); 44 | 45 | /** 46 | * @var string|null $connection 47 | * @var string $table 48 | * @var int $lifetime 49 | */ 50 | 51 | $handler = new SproutDatabaseSessionHandler( 52 | $this->app->make('db')->connection($connection), 53 | $table, 54 | $lifetime, 55 | $this->app 56 | ); 57 | 58 | if ($this->sprout->withinContext()) { 59 | $tenancy = $this->sprout->getCurrentTenancy(); 60 | 61 | $handler->setTenancy($tenancy) 62 | ->setTenant($tenancy?->tenant()); 63 | } 64 | 65 | return $handler; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Overrides/CookieOverride.php: -------------------------------------------------------------------------------- 1 | $tenancy 28 | * @param \Sprout\Contracts\Tenant $tenant 29 | * 30 | * @return void 31 | */ 32 | public function setup(Tenancy $tenancy, Tenant $tenant): void 33 | { 34 | // Collect the values 35 | $path = $this->getSprout()->settings()->getUrlPath(config('session.path') ?? '/'); // @phpstan-ignore-line 36 | $domain = $this->getSprout()->settings()->getUrlDomain(config('session.domain')); // @phpstan-ignore-line 37 | $secure = $this->getSprout()->settings()->shouldCookieBeSecure(config('session.secure', false)); // @phpstan-ignore-line 38 | $sameSite = $this->getSprout()->settings()->getCookieSameSite(config('session.same_site')); // @phpstan-ignore-line 39 | 40 | /** 41 | * This is here to make PHPStan quiet down 42 | * 43 | * @var string $path 44 | * @var string|null $domain 45 | * @var bool|null $secure 46 | * @var string|null $sameSite 47 | */ 48 | 49 | // Set the default values on the cookiejar 50 | $this->getApp() 51 | ->make(CookieJar::class) 52 | ->setDefaultPathAndDomain($path, $domain, $secure, $sameSite); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Facades/Sprout.php: -------------------------------------------------------------------------------- 1 | getAllCurrentTenancies() 23 | * @method static ResolutionHook|null getCurrentHook() 24 | * @method static Tenancy|null getCurrentTenancy() 25 | * @method static bool hasCurrentTenancy() 26 | * @method static bool isCurrentHook(ResolutionHook|null $hook) 27 | * @method static \Sprout\Sprout markAsInContext() 28 | * @method static \Sprout\Sprout markAsOutsideContext() 29 | * @method static ServiceOverrideManager overrides() 30 | * @method static TenantProviderManager providers() 31 | * @method static \Sprout\Sprout resetTenancies() 32 | * @method static IdentityResolverManager resolvers() 33 | * @method static string route(string $name, Tenant $tenant, string|null $resolver = null, string|null $tenancy = null, array $parameters = [], bool $absolute = true) 34 | * @method static \Sprout\Sprout setCurrentHook(ResolutionHook|null $hook) 35 | * @method static void setCurrentTenancy(Tenancy $tenancy) 36 | * @method static mixed setting(string $key, mixed $default = null) 37 | * @method static SettingsRepository settings() 38 | * @method static bool supportsHook(ResolutionHook $hook) 39 | * @method static TenancyManager tenancies() 40 | * @method static bool withinContext() 41 | */ 42 | final class Sprout extends Facade 43 | { 44 | protected static function getFacadeAccessor(): string 45 | { 46 | return \Sprout\Sprout::class; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Events/TenantFound.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public Tenancy $tenancy; 41 | 42 | /** 43 | * The tenant that was found 44 | * 45 | * @var \Sprout\Contracts\Tenant 46 | * 47 | * @phpstan-var TenantClass 48 | */ 49 | public Tenant $tenant; 50 | 51 | /** 52 | * Create a new instance 53 | * 54 | * @param \Sprout\Contracts\Tenant $tenant 55 | * @param \Sprout\Contracts\Tenancy $tenancy 56 | * 57 | * @phpstan-param TenantClass $tenant 58 | * @phpstan-param \Sprout\Contracts\Tenancy $tenancy 59 | */ 60 | public function __construct( 61 | Tenant $tenant, 62 | Tenancy $tenancy, 63 | ) 64 | { 65 | $this->tenant = $tenant; 66 | $this->tenancy = $tenancy; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Contracts/ServiceOverride.php: -------------------------------------------------------------------------------- 1 | $config 20 | */ 21 | public function __construct(string $service, array $config); 22 | 23 | /** 24 | * Set up the service override 25 | * 26 | * This method should perform any necessary setup actions for the service 27 | * override. 28 | * It is called when a new tenant is marked as the current tenant. 29 | * 30 | * @template TenantClass of \Sprout\Contracts\Tenant 31 | * 32 | * @param \Sprout\Contracts\Tenancy $tenancy 33 | * @param \Sprout\Contracts\Tenant $tenant 34 | * 35 | * @phpstan-param TenantClass $tenant 36 | * 37 | * @return void 38 | */ 39 | public function setup(Tenancy $tenancy, Tenant $tenant): void; 40 | 41 | /** 42 | * Clean up the service override 43 | * 44 | * This method should perform any necessary setup actions for the service 45 | * override. 46 | * It is called when the current tenant is unset, either to be replaced 47 | * by another tenant, or none. 48 | * 49 | * It will be called before {@see self::setup()}, but only if the previous 50 | * tenant was not null. 51 | * 52 | * @template TenantClass of \Sprout\Contracts\Tenant 53 | * 54 | * @param \Sprout\Contracts\Tenancy $tenancy 55 | * @param \Sprout\Contracts\Tenant $tenant 56 | * 57 | * @phpstan-param TenantClass $tenant 58 | * 59 | * @return void 60 | */ 61 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void; 62 | } 63 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | settings(); 33 | } 34 | 35 | /** 36 | * Get an identity resolver 37 | * 38 | * @param string|null $name 39 | * 40 | * @return \Sprout\Contracts\IdentityResolver 41 | * 42 | * @throws \Sprout\Exceptions\MisconfigurationException 43 | * 44 | * @codeCoverageIgnore 45 | */ 46 | function resolver(?string $name = null): IdentityResolver 47 | { 48 | return sprout()->resolvers()->get($name); 49 | } 50 | 51 | /** 52 | * Get a tenancy 53 | * 54 | * @param string|null $name 55 | * 56 | * @return \Sprout\Contracts\Tenancy<*> 57 | * 58 | * @throws \Sprout\Exceptions\MisconfigurationException 59 | * 60 | * @codeCoverageIgnore 61 | */ 62 | function tenancy(?string $name = null): Tenancy 63 | { 64 | return sprout()->tenancies()->get($name); 65 | } 66 | 67 | /** 68 | * Get a tenant provider 69 | * 70 | * @param string|null $name 71 | * 72 | * @return \Sprout\Contracts\TenantProvider<*> 73 | * 74 | * @throws \Sprout\Exceptions\MisconfigurationException 75 | * 76 | * @codeCoverageIgnore 77 | */ 78 | function provider(?string $name = null): TenantProvider 79 | { 80 | return sprout()->providers()->get($name); 81 | } 82 | 83 | /** 84 | * Get a service override 85 | * 86 | * @param string $service 87 | * 88 | * @return \Sprout\Contracts\ServiceOverride|null 89 | * 90 | * @codeCoverageIgnore 91 | */ 92 | function override(string $service): ?ServiceOverride 93 | { 94 | return sprout()->overrides()->get($service); 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/RouterMethods.php: -------------------------------------------------------------------------------- 1 | getAttribute($model->getTenantResourceKeyName()) === null) { 34 | $model->setAttribute( 35 | $model->getTenantResourceKeyName(), 36 | method_exists($model, 'generateNewResourceKey') 37 | ? $model->generateNewResourceKey() 38 | : Str::uuid() 39 | ); 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Generate a new resource key 46 | * 47 | * @return mixed 48 | */ 49 | public function generateNewResourceKey(): mixed 50 | { 51 | return Str::uuid(); 52 | } 53 | 54 | /** 55 | * Get the resource key used to identify the tenants resources 56 | * 57 | * @return string 58 | */ 59 | public function getTenantResourceKey(): string 60 | { 61 | return (string)$this->getAttribute($this->getTenantResourceKeyName()); 62 | } 63 | 64 | /** 65 | * Gets the name of the resource key used to identify the tenants resources 66 | * 67 | * @return string 68 | */ 69 | public function getTenantResourceKeyName(): string 70 | { 71 | return 'resource_key'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Http/Middleware/AddTenantHeaderToResponse.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 36 | } 37 | 38 | /** 39 | * Handle the request 40 | * 41 | * @param \Illuminate\Http\Request $request 42 | * @param \Closure $next 43 | * @param string ...$options 44 | * 45 | * @return \Symfony\Component\HttpFoundation\Response 46 | * 47 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 48 | * @throws \Sprout\Exceptions\MisconfigurationException 49 | */ 50 | public function handle(Request $request, Closure $next, string ...$options): Response 51 | { 52 | [$resolverName, $tenancyName] = ResolutionHelper::parseOptions($options); 53 | 54 | /** @var \Illuminate\Http\Response $response */ 55 | $response = $next($request); 56 | 57 | /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ 58 | $tenancy = $this->sprout->tenancies()->get($tenancyName); 59 | 60 | if (! $tenancy->check()) { 61 | return $response; 62 | } 63 | 64 | $resolver = $tenancy->resolver(); 65 | 66 | if (! ($resolver instanceof HeaderIdentityResolver) || $resolver->getName() !== $resolverName) { 67 | return $response; 68 | } 69 | 70 | return $response->withHeaders([ 71 | $resolver->getRequestHeaderName($tenancy) => $tenancy->identifier(), 72 | ]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Support/PlaceholderHelper.php: -------------------------------------------------------------------------------- 1 | $placeholders 11 | * 12 | * @return string 13 | */ 14 | public static function replaceForParameter(string $pattern, array $placeholders = []): string 15 | { 16 | return self::replace($pattern, $placeholders, true); 17 | } 18 | 19 | /** 20 | * @param string $pattern 21 | * @param array $placeholders 22 | * @param bool $forParameter 23 | * 24 | * @return string 25 | */ 26 | public static function replace(string $pattern, array $placeholders = [], bool $forParameter = false): string 27 | { 28 | $newString = $pattern; 29 | 30 | foreach ($placeholders as $placeholder => $replacement) { 31 | $newString = self::replacePlaceholder( 32 | $newString, 33 | $placeholder, 34 | ! is_string($replacement) ? $replacement() : $replacement, 35 | $forParameter 36 | ); 37 | } 38 | 39 | return $newString; 40 | } 41 | 42 | /** 43 | * @param string $string 44 | * @param string $placeholder 45 | * @param string $value 46 | * 47 | * @return string 48 | */ 49 | private static function replacePlaceholder(string $string, string $placeholder, string $value, bool $forParameter): string 50 | { 51 | $search = [ 52 | '{' . strtolower($placeholder) . '}', 53 | '{' . ucfirst($placeholder) . '}', 54 | '{' . strtoupper($placeholder) . '}', 55 | ]; 56 | 57 | $replace = [ 58 | $value, 59 | ucfirst($value), 60 | strtoupper($value), 61 | ]; 62 | 63 | if ($forParameter) { 64 | $search[] = '-'; 65 | $replace[] = '_'; 66 | } 67 | 68 | return str_replace( 69 | $search, 70 | $replace, 71 | $string 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Concerns/IsTenant.php: -------------------------------------------------------------------------------- 1 | getAttribute($this->getTenantIdentifierName()); 32 | } 33 | 34 | /** 35 | * Get the name of the tenant identifier 36 | * 37 | * Retrieve the storage name for the tenant identifier, whether that's an 38 | * attribute, column name, array key or something else. 39 | * Used primarily by {@see \Sprout\Contracts\TenantProvider}. 40 | * 41 | * @return string 42 | * 43 | * @infection-ignore-all 44 | */ 45 | public function getTenantIdentifierName(): string 46 | { 47 | return 'identifier'; 48 | } 49 | 50 | /** 51 | * Get the tenant key 52 | * 53 | * Retrieve the key used to identify a tenant internally. 54 | * 55 | * @return int|string 56 | * 57 | * @infection-ignore-all 58 | */ 59 | public function getTenantKey(): int|string 60 | { 61 | /** @phpstan-ignore return.type */ 62 | return $this->getKey(); 63 | } 64 | 65 | /** 66 | * Get the name of the tenant key 67 | * 68 | * Retrieve the storage name for the tenant key, whether that's an 69 | * attribute, column name, array key or something else. 70 | * Used primarily by {@see \Sprout\Contracts\TenantProvider}. 71 | * 72 | * @return string 73 | * 74 | * @infection-ignore-all 75 | */ 76 | public function getTenantKeyName(): string 77 | { 78 | return $this->getKeyName(); // @codeCoverageIgnore 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Managers/TenancyManager.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class TenancyManager extends BaseFactory 19 | { 20 | /** 21 | * @var \Sprout\Managers\TenantProviderManager 22 | */ 23 | private TenantProviderManager $providerManager; 24 | 25 | /** 26 | * Create a new instance 27 | * 28 | * @param \Illuminate\Contracts\Foundation\Application $app 29 | * @param \Sprout\Managers\TenantProviderManager $providerManager 30 | */ 31 | public function __construct(Application $app, TenantProviderManager $providerManager) 32 | { 33 | parent::__construct($app); 34 | 35 | $this->providerManager = $providerManager; 36 | } 37 | 38 | /** 39 | * Get the name used by this factory 40 | * 41 | * @return string 42 | */ 43 | public function getFactoryName(): string 44 | { 45 | return 'tenancy'; 46 | } 47 | 48 | /** 49 | * Get the config key for the given name 50 | * 51 | * @param string $name 52 | * 53 | * @return string 54 | */ 55 | public function getConfigKey(string $name): string 56 | { 57 | return 'multitenancy.tenancies.' . $name; 58 | } 59 | 60 | /** 61 | * Create the default implementation 62 | * 63 | * @param array $config 64 | * @param string $name 65 | * 66 | * @phpstan-param array{provider?: string|null, options?: list} $config 67 | * 68 | * @return \Sprout\Support\DefaultTenancy<\Sprout\Contracts\Tenant> 69 | */ 70 | protected function createDefaultTenancy(array $config, string $name): DefaultTenancy 71 | { 72 | return new DefaultTenancy( 73 | $name, 74 | $this->providerManager->get($config['provider'] ?? null), 75 | $config['options'] ?? [] 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Support/SettingsRepository.php: -------------------------------------------------------------------------------- 1 | set(Settings::URL_PATH, $path); 19 | } 20 | 21 | public function getUrlPath(?string $default = null): ?string 22 | { 23 | /** @var string|null $path */ 24 | $path = $this->get(Settings::URL_PATH, $default); 25 | 26 | return $path; 27 | } 28 | 29 | public function setUrlDomain(?string $domain): void 30 | { 31 | $this->set(Settings::URL_DOMAIN, $domain); 32 | } 33 | 34 | public function getUrlDomain(?string $default = null): ?string 35 | { 36 | /** @var string|null $domain */ 37 | $domain = $this->get(Settings::URL_DOMAIN, $default); 38 | 39 | return $domain; 40 | } 41 | 42 | public function setCookieSecure(bool $secure): void 43 | { 44 | $this->set(Settings::COOKIE_SECURE, $secure); 45 | } 46 | 47 | public function shouldCookieBeSecure(?bool $default = null): ?bool 48 | { 49 | /** @var bool|null $value */ 50 | $value = $this->get(Settings::COOKIE_SECURE, $default); 51 | 52 | return $value; 53 | } 54 | 55 | public function setCookieSameSite(?string $sameSite): void 56 | { 57 | $this->set(Settings::COOKIE_SAME_SITE, $sameSite); 58 | } 59 | 60 | public function getCookieSameSite(?string $default = null): ?string 61 | { 62 | /** 63 | * This is only here because the config repository has terrible support 64 | * for typing, as you'd expect. 65 | * 66 | * @var string|null $sameSite 67 | */ 68 | $sameSite = $this->get(Settings::COOKIE_SAME_SITE, $default); 69 | 70 | return $sameSite; 71 | } 72 | 73 | public function doNotOverrideTheDatabase(): void 74 | { 75 | $this->set(Settings::NO_DATABASE_OVERRIDE, true); 76 | } 77 | 78 | public function shouldNotOverrideTheDatabase(bool $default = false): bool 79 | { 80 | return $this->boolean(Settings::NO_DATABASE_OVERRIDE, $default); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Http/Middleware/SproutOptionalTenantContextMiddleware.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 45 | } 46 | 47 | /** 48 | * Handle the request 49 | * 50 | * @param \Illuminate\Http\Request $request 51 | * @param \Closure $next 52 | * @param string ...$options 53 | * 54 | * @return \Symfony\Component\HttpFoundation\Response 55 | * 56 | * @throws \Sprout\Exceptions\NoTenantFoundException 57 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 58 | * @throws \Sprout\Exceptions\MisconfigurationException 59 | */ 60 | public function handle(Request $request, Closure $next, string ...$options): Response 61 | { 62 | [$resolverName, $tenancyName] = ResolutionHelper::parseOptions($options); 63 | 64 | if ($this->sprout->supportsHook(ResolutionHook::Middleware)) { 65 | ResolutionHelper::handleResolution( 66 | $request, 67 | ResolutionHook::Middleware, 68 | $this->sprout, 69 | $resolverName, 70 | $tenancyName, 71 | false, 72 | true 73 | ); 74 | } 75 | 76 | return $next($request); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Events/CurrentTenantChanged.php: -------------------------------------------------------------------------------- 1 | null. 18 | * 19 | * Sprout makes heavy use of this event internally to bootstrap tenants tenancies. 20 | * 21 | * @template TenantClass of Tenant 22 | * 23 | * @method static void dispatch(Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) 24 | * @method static void dispatchIf(bool $condition, Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) 25 | * @method static void dispatchUnless(bool $condition, Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) 26 | * 27 | * @package Core 28 | * 29 | * @codeCoverageIgnore 30 | */ 31 | final readonly class CurrentTenantChanged 32 | { 33 | use Dispatchable; 34 | 35 | /** 36 | * The tenancy whose current tenant changed 37 | * 38 | * @var \Sprout\Contracts\Tenancy 39 | */ 40 | public Tenancy $tenancy; 41 | 42 | /** 43 | * The current tenant 44 | * 45 | * @var \Sprout\Contracts\Tenant|null 46 | * 47 | * @phpstan-var TenantClass|null 48 | */ 49 | public ?Tenant $current; 50 | 51 | /** 52 | * The previous tenant 53 | * 54 | * @var \Sprout\Contracts\Tenant|null 55 | * 56 | * @phpstan-var TenantClass|null 57 | */ 58 | public ?Tenant $previous; 59 | 60 | /** 61 | * Create a new instance 62 | * 63 | * @param \Sprout\Contracts\Tenancy $tenancy 64 | * @param \Sprout\Contracts\Tenant|null $previous 65 | * @param \Sprout\Contracts\Tenant|null $current 66 | * 67 | * @phpstan-param TenantClass|null $previous 68 | * @phpstan-param TenantClass|null $current 69 | */ 70 | public function __construct( 71 | Tenancy $tenancy, 72 | ?Tenant $previous = null, 73 | ?Tenant $current = null 74 | ) 75 | { 76 | $this->tenancy = $tenancy; 77 | $this->previous = $previous; 78 | $this->current = $current; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Overrides/Filesystem/SproutFilesystemManager.php: -------------------------------------------------------------------------------- 1 | syncOriginal($original); 20 | } 21 | } 22 | 23 | /** 24 | * Check if this manager override was synced from the original 25 | * 26 | * @return bool 27 | */ 28 | public function wasSyncedFromOriginal(): bool 29 | { 30 | return $this->syncedFromOriginal; 31 | } 32 | 33 | /** 34 | * Sync the original manager in case things have been registered 35 | * 36 | * @param \Illuminate\Filesystem\FilesystemManager $original 37 | * 38 | * @return void 39 | */ 40 | private function syncOriginal(FilesystemManager $original): void 41 | { 42 | $this->disks = array_merge($original->disks, $this->disks); 43 | $this->customCreators = array_merge($original->customCreators, $this->customCreators); 44 | $this->syncedFromOriginal = true; 45 | } 46 | 47 | /** 48 | * Resolve the given disk. 49 | * 50 | * @param string $name 51 | * @param array|null $config 52 | * 53 | * @return \Illuminate\Contracts\Filesystem\Filesystem 54 | * 55 | * @throws \InvalidArgumentException 56 | */ 57 | protected function resolve($name, $config = null): Filesystem 58 | { 59 | $config ??= $this->getConfig($name); 60 | 61 | if (empty($config['driver'])) { 62 | throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver."); 63 | } 64 | 65 | $config['name'] = $name; 66 | 67 | $driver = $config['driver']; 68 | 69 | if (isset($this->customCreators[$driver])) { 70 | return $this->callCustomCreator($config); 71 | } 72 | 73 | $driverMethod = 'create' . ucfirst($driver) . 'Driver'; 74 | 75 | if (! method_exists($this, $driverMethod)) { 76 | throw new InvalidArgumentException("Driver [{$driver}] is not supported."); 77 | } 78 | 79 | return $this->{$driverMethod}($config, $name); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Contracts/TenantProvider.php: -------------------------------------------------------------------------------- 1 | $tenancy 33 | * @param \Sprout\Contracts\Tenant $tenant 34 | * 35 | * @return void 36 | */ 37 | public function setup(Tenancy $tenancy, Tenant $tenant): void 38 | { 39 | $this->forgetGuards(); 40 | } 41 | 42 | /** 43 | * Clean up the service override 44 | * 45 | * This method should perform any necessary setup actions for the service 46 | * override. 47 | * It is called when the current tenant is unset, either to be replaced 48 | * by another tenant, or none. 49 | * 50 | * It will be called before {@see self::setup()}, but only if the previous 51 | * tenant was not null. 52 | * 53 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 54 | * @param \Sprout\Contracts\Tenant $tenant 55 | * 56 | * @return void 57 | */ 58 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void 59 | { 60 | $this->forgetGuards(); 61 | } 62 | 63 | /** 64 | * Forget all resolved guards 65 | * 66 | * @return void 67 | */ 68 | protected function forgetGuards(): void 69 | { 70 | // Since this class isn't deferred because it has to rely on 71 | // multiple services, we only want to actually run this code if 72 | // the auth manager has been resolved. 73 | if ($this->getApp()->resolved('auth')) { 74 | /** @var \Illuminate\Auth\AuthManager $authManager */ 75 | $authManager = $this->getApp()->make(AuthManager::class); 76 | 77 | if ($authManager->hasResolvedGuards()) { 78 | $authManager->forgetGuards(); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Overrides/BaseOverride.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $config; 22 | 23 | /** 24 | * Create a new instance of the service override 25 | * 26 | * @param string $service 27 | * @param array $config 28 | */ 29 | public function __construct(string $service, array $config) 30 | { 31 | $this->config = $config; 32 | $this->service = $service; 33 | } 34 | 35 | /** 36 | * Get the service config 37 | * 38 | * @return array 39 | */ 40 | public function getConfig(): array 41 | { 42 | return $this->config; 43 | } 44 | 45 | /** 46 | * Set up the service override 47 | * 48 | * This method should perform any necessary setup actions for the service 49 | * override. 50 | * It is called when a new tenant is marked as the current tenant. 51 | * 52 | * @template TenantClass of \Sprout\Contracts\Tenant 53 | * 54 | * @param \Sprout\Contracts\Tenancy $tenancy 55 | * @param \Sprout\Contracts\Tenant $tenant 56 | * 57 | * @phpstan-param TenantClass $tenant 58 | * 59 | * @return void 60 | */ 61 | public function setup(Tenancy $tenancy, Tenant $tenant): void 62 | { 63 | // I'm intentionally empty 64 | } 65 | 66 | /** 67 | * Clean up the service override 68 | * 69 | * This method should perform any necessary setup actions for the service 70 | * override. 71 | * It is called when the current tenant is unset, either to be replaced 72 | * by another tenant, or none. 73 | * 74 | * It will be called before {@see self::setup()}, but only if the previous 75 | * tenant was not null. 76 | * 77 | * @template TenantClass of \Sprout\Contracts\Tenant 78 | * 79 | * @param \Sprout\Contracts\Tenancy $tenancy 80 | * @param \Sprout\Contracts\Tenant $tenant 81 | * 82 | * @phpstan-param TenantClass $tenant 83 | * 84 | * @return void 85 | */ 86 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void 87 | { 88 | // I'm intentionally empty 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/Concerns/AwareOfTenant.php: -------------------------------------------------------------------------------- 1 | |null 21 | */ 22 | private ?Tenancy $tenancy; 23 | 24 | /** 25 | * Should the tenancy and tenant be refreshed when they change? 26 | * 27 | * @return bool 28 | */ 29 | public function shouldBeRefreshed(): bool 30 | { 31 | return true; // @codeCoverageIgnore 32 | } 33 | 34 | /** 35 | * Get the tenant if there is one 36 | * 37 | * @return \Sprout\Contracts\Tenant|null 38 | */ 39 | public function getTenant(): ?Tenant 40 | { 41 | return $this->tenant ?? null; 42 | } 43 | 44 | /** 45 | * Check if there is a tenant 46 | * 47 | * @return bool 48 | * 49 | * @phpstan-assert-if-true !null $this->tenant 50 | * @phpstan-assert-if-false null $this->tenant 51 | */ 52 | public function hasTenant(): bool 53 | { 54 | return $this->getTenant() !== null; 55 | } 56 | 57 | /** 58 | * Set the tenant 59 | * 60 | * @param \Sprout\Contracts\Tenant|null $tenant 61 | * 62 | * @return static 63 | */ 64 | public function setTenant(?Tenant $tenant): static 65 | { 66 | $this->tenant = $tenant; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the tenancy if there is one 73 | * 74 | * @return \Sprout\Contracts\Tenancy<*>|null 75 | */ 76 | public function getTenancy(): ?Tenancy 77 | { 78 | return $this->tenancy ?? null; 79 | } 80 | 81 | /** 82 | * Check if there is a tenancy 83 | * 84 | * @return bool 85 | * 86 | * @phpstan-assert-if-true !null $this->tenancy 87 | * @phpstan-assert-if-false null $this->tenancy 88 | */ 89 | public function hasTenancy(): bool 90 | { 91 | return $this->getTenancy() !== null; 92 | } 93 | 94 | /** 95 | * Set the tenancy 96 | * 97 | * @template TenantClass of \Sprout\Contracts\Tenant 98 | * @param \Sprout\Contracts\Tenancy|null $tenancy 99 | * 100 | * @return static 101 | */ 102 | public function setTenancy(?Tenancy $tenancy): static 103 | { 104 | $this->tenancy = $tenancy; 105 | 106 | return $this; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Http/Middleware/SproutTenantContextMiddleware.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 43 | } 44 | 45 | /** 46 | * Handle the request 47 | * 48 | * @param \Illuminate\Http\Request $request 49 | * @param \Closure $next 50 | * @param string ...$options 51 | * 52 | * @return \Symfony\Component\HttpFoundation\Response 53 | * 54 | * @throws \Sprout\Exceptions\NoTenantFoundException 55 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 56 | * @throws \Sprout\Exceptions\MisconfigurationException 57 | */ 58 | public function handle(Request $request, Closure $next, string ...$options): Response 59 | { 60 | [$resolverName, $tenancyName] = ResolutionHelper::parseOptions($options); 61 | 62 | if ($this->sprout->supportsHook(ResolutionHook::Middleware)) { 63 | ResolutionHelper::handleResolution( 64 | $request, 65 | ResolutionHook::Middleware, 66 | $this->sprout, 67 | $resolverName, 68 | $tenancyName, 69 | ); 70 | } 71 | 72 | if (! $this->sprout->hasCurrentTenancy() || ! $this->sprout->getCurrentTenancy()?->check()) { 73 | $defaultResolver = config('multitenancy.defaults.resolver'); 74 | $defaultTenancy = config('multitenancy.defaults.tenancy'); 75 | 76 | /** 77 | * @var string $defaultResolver 78 | * @var string $defaultTenancy 79 | */ 80 | 81 | throw NoTenantFoundException::make( 82 | $resolverName ?? $defaultResolver, 83 | $tenancyName ?? $defaultTenancy 84 | ); 85 | } 86 | 87 | return $next($request); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TenancyOptions.php: -------------------------------------------------------------------------------- 1 | $overrides 41 | * 42 | * @return array> 43 | */ 44 | public static function overrides(array $overrides): array 45 | { 46 | return [ 47 | 'overrides' => $overrides, 48 | ]; 49 | } 50 | 51 | /** 52 | * Enable all overrides for the tenancy 53 | * 54 | * @return string 55 | */ 56 | public static function allOverrides(): string 57 | { 58 | return 'overrides.all'; 59 | } 60 | 61 | /** 62 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 63 | * 64 | * @return bool 65 | */ 66 | public static function shouldHydrateTenantRelation(Tenancy $tenancy): bool 67 | { 68 | return $tenancy->hasOption(static::hydrateTenantRelation()); 69 | } 70 | 71 | /** 72 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 73 | * 74 | * @return bool 75 | */ 76 | public static function shouldThrowIfNotRelated(Tenancy $tenancy): bool 77 | { 78 | return $tenancy->hasOption(static::throwIfNotRelated()); 79 | } 80 | 81 | /** 82 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 83 | * 84 | * @return list|null 85 | */ 86 | public static function enabledOverrides(Tenancy $tenancy): array|null 87 | { 88 | return $tenancy->optionConfig('overrides'); // @phpstan-ignore-line 89 | } 90 | 91 | /** 92 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 93 | * 94 | * @return bool 95 | */ 96 | public static function shouldEnableAllOverrides(Tenancy $tenancy): bool 97 | { 98 | return $tenancy->hasOption(static::allOverrides()); 99 | } 100 | 101 | /** 102 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 103 | * @param string $service 104 | * 105 | * @return bool 106 | */ 107 | public static function shouldEnableOverride(Tenancy $tenancy, string $service): bool 108 | { 109 | return self::shouldEnableAllOverrides($tenancy) 110 | || in_array($service, self::enabledOverrides($tenancy) ?? [], true); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Overrides/Session/SproutFileSessionHandler.php: -------------------------------------------------------------------------------- 1 | hasTenant()) { 27 | return $this->path; 28 | } 29 | 30 | /** @var \Sprout\Contracts\Tenant&\Sprout\Contracts\TenantHasResources $tenant */ 31 | $tenant = $this->getTenant(); 32 | 33 | return rtrim($this->path, DIRECTORY_SEPARATOR) 34 | . DIRECTORY_SEPARATOR 35 | . $tenant->getTenantResourceKey(); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * @param $sessionId 42 | * 43 | * @return string|false 44 | * 45 | * @throws \Sprout\Exceptions\TenancyMissingException 46 | * @throws \Sprout\Exceptions\TenantMissingException 47 | */ 48 | public function read($sessionId): string|false 49 | { 50 | if ($this->files->isFile($path = $this->getPath() . '/' . $sessionId) && 51 | $this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) { 52 | return $this->files->sharedGet($path); 53 | } 54 | 55 | return ''; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | * 61 | * @return bool 62 | * 63 | * @throws \Sprout\Exceptions\TenancyMissingException 64 | * @throws \Sprout\Exceptions\TenantMissingException 65 | */ 66 | public function write($sessionId, $data): bool 67 | { 68 | $this->files->put($this->getPath() . '/' . $sessionId, $data, true); 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | * 76 | * @return bool 77 | */ 78 | public function destroy($sessionId): bool 79 | { 80 | $this->files->delete($this->getPath() . '/' . $sessionId); 81 | 82 | return true; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | * 88 | * @return int 89 | */ 90 | public function gc($lifetime): int 91 | { 92 | // @codeCoverageIgnoreStart 93 | $files = Finder::create() 94 | ->in($this->getPath()) 95 | ->files() 96 | ->ignoreDotFiles(true) 97 | ->date('<= now - ' . $lifetime . ' seconds'); 98 | 99 | $deletedSessions = 0; 100 | 101 | foreach ($files as $file) { 102 | $this->files->delete($file->getRealPath()); 103 | $deletedSessions++; 104 | } 105 | 106 | return $deletedSessions; 107 | // @codeCoverageIgnoreEnd 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Exceptions/MisconfigurationException.php: -------------------------------------------------------------------------------- 1 | name . '] is not supported'); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Overrides/Session/SproutDatabaseSessionHandler.php: -------------------------------------------------------------------------------- 1 | hasTenant()) { 37 | return parent::getQuery(); 38 | } 39 | 40 | $tenancy = $this->getTenancy(); 41 | $tenant = $this->getTenant(); 42 | 43 | /** 44 | * @var \Sprout\Contracts\Tenancy<*> $tenancy 45 | * @var \Sprout\Contracts\Tenant $tenant 46 | */ 47 | 48 | $query = parent::getQuery(); 49 | 50 | if ($write === false) { 51 | return $query->where('tenancy', '=', $tenancy->getName()) 52 | ->where('tenant_id', '=', $tenant->getTenantKey()); 53 | } 54 | 55 | return $query; 56 | } 57 | 58 | /** 59 | * Perform an insert operation on the session ID. 60 | * 61 | * @param string $sessionId 62 | * @param array $payload 63 | * 64 | * @return bool|null 65 | */ 66 | protected function performInsert($sessionId, $payload): ?bool 67 | { 68 | if ($this->hasTenant()) { 69 | $tenancy = $this->getTenancy(); 70 | $tenant = $this->getTenant(); 71 | 72 | /** 73 | * @var \Sprout\Contracts\Tenancy<*> $tenancy 74 | * @var \Sprout\Contracts\Tenant $tenant 75 | */ 76 | 77 | $payload['tenancy'] = $tenancy->getName(); 78 | $payload['tenant_id'] = $tenant->getTenantKey(); 79 | } 80 | 81 | try { 82 | return $this->getQuery(true)->insert(Arr::set($payload, 'id', $sessionId)); 83 | } catch (QueryException) { // @codeCoverageIgnore 84 | return $this->performUpdate($sessionId, $payload) > 0; // @codeCoverageIgnore 85 | } 86 | } 87 | 88 | /** 89 | * Perform an update operation on the session ID. 90 | * 91 | * @param string $sessionId 92 | * @param array $payload 93 | * 94 | * @return int 95 | * 96 | * @throws \Sprout\Exceptions\TenancyMissingException 97 | * @throws \Sprout\Exceptions\TenantMissingException 98 | */ 99 | protected function performUpdate($sessionId, $payload): int 100 | { 101 | return $this->getQuery(true)->where('id', $sessionId)->update($payload); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Overrides/CacheOverride.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $drivers = []; 22 | 23 | /** 24 | * Boot a service override 25 | * 26 | * This method should perform any initial steps required for the service 27 | * override that take place during the booting of the framework. 28 | * 29 | * @param \Illuminate\Contracts\Foundation\Application $app 30 | * @param \Sprout\Sprout $sprout 31 | * 32 | * @return void 33 | */ 34 | public function boot(Application $app, Sprout $sprout): void 35 | { 36 | $this->setApp($app)->setSprout($sprout); 37 | 38 | $tracker = fn (string $store) => $this->drivers[] = $store; 39 | 40 | // If the cache manager has been resolved, we can add the driver 41 | if ($app->resolved('cache')) { 42 | $this->addDriver($app->make('cache'), $sprout, $tracker); 43 | } else { 44 | // But if it hasn't, we'll add it once it is 45 | $app->afterResolving('cache', function (CacheManager $manager) use ($sprout, $tracker) { 46 | $this->addDriver($manager, $sprout, $tracker); 47 | }); 48 | } 49 | } 50 | 51 | protected function addDriver(CacheManager $manager, Sprout $sprout, Closure $tracker): void 52 | { 53 | $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout, $tracker): Repository { 54 | // The cache manager adds the store name to the config, so we'll 55 | // _STORE_ that ;) 56 | $tracker($config['store']); 57 | 58 | return (new SproutCacheDriverCreator($app, $manager, $config, $sprout))(); 59 | }); 60 | } 61 | 62 | /** 63 | * Get the drivers that have been resolved 64 | * 65 | * @return array 66 | */ 67 | public function getDrivers(): array 68 | { 69 | return $this->drivers; 70 | } 71 | 72 | /** 73 | * Clean up the service override 74 | * 75 | * This method should perform any necessary setup actions for the service 76 | * override. 77 | * It is called when the current tenant is unset, either to be replaced 78 | * by another tenant, or none. 79 | * 80 | * It will be called before {@see self::setup()}, but only if the previous 81 | * tenant was not null. 82 | * 83 | * @template TenantClass of \Sprout\Contracts\Tenant 84 | * 85 | * @param \Sprout\Contracts\Tenancy $tenancy 86 | * @param \Sprout\Contracts\Tenant $tenant 87 | * 88 | * @phpstan-param TenantClass $tenant 89 | * 90 | * @return void 91 | */ 92 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void 93 | { 94 | if (! empty($this->drivers)) { 95 | $this->getApp()->make('cache')->forgetDriver($this->drivers); 96 | 97 | $this->drivers = []; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Listeners/IdentifyTenantOnRouting.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 34 | } 35 | 36 | /** 37 | * Handle the event 38 | * 39 | * @param \Illuminate\Routing\Events\RouteMatched $event 40 | * 41 | * @return void 42 | * 43 | * @throws \Sprout\Exceptions\NoTenantFoundException 44 | */ 45 | public function handle(RouteMatched $event): void 46 | { 47 | $options = $this->parseTenantMiddleware($event->route); 48 | 49 | if ($options === null) { 50 | return; 51 | } 52 | 53 | [$resolverName, $tenancyName] = $options; 54 | 55 | ResolutionHelper::handleResolution( 56 | $event->request, 57 | ResolutionHook::Routing, 58 | $this->sprout, 59 | $resolverName, 60 | $tenancyName, 61 | false 62 | ); 63 | } 64 | 65 | /** 66 | * Parse the route middleware stack to find the marker middleware 67 | * 68 | * @param \Illuminate\Routing\Route $route 69 | * 70 | * @return array|null 71 | * 72 | * @codeCoverageIgnore 73 | */ 74 | private function parseTenantMiddleware(Route $route): ?array 75 | { 76 | $middleware = null; 77 | $found = false; 78 | 79 | foreach (Arr::wrap($route->middleware()) as $item) { 80 | // If it's the normal middleware, we'll get that 81 | if ( 82 | $item === SproutTenantContextMiddleware::ALIAS 83 | || Str::startsWith($item, SproutTenantContextMiddleware::ALIAS . ':') 84 | ) { 85 | $middleware = Str::trim(Str::after($item, SproutTenantContextMiddleware::ALIAS), ':'); 86 | $found = true; 87 | break; 88 | } 89 | 90 | // If it's the optional middleware, we'll get that 91 | if ( 92 | $item === SproutOptionalTenantContextMiddleware::ALIAS 93 | || Str::startsWith($item, SproutOptionalTenantContextMiddleware::ALIAS . ':') 94 | ) { 95 | $middleware = Str::trim(Str::after($item, SproutOptionalTenantContextMiddleware::ALIAS), ':'); 96 | $found = true; 97 | break; 98 | } 99 | } 100 | 101 | if ($found === true) { 102 | if (empty($middleware)) { 103 | return [null, null]; 104 | } 105 | 106 | if (Str::contains($middleware, ',')) { 107 | return explode(',', Str::after($middleware, ':'), 2); 108 | } 109 | 110 | return [ 111 | $middleware, 112 | null, 113 | ]; 114 | } 115 | 116 | return null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Support/GenericTenant.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected array $attributes; 27 | 28 | /** 29 | * Create a new generic User object. 30 | * 31 | * @param array $attributes 32 | * 33 | * @return void 34 | */ 35 | public function __construct(array $attributes = []) 36 | { 37 | $this->attributes = $attributes; 38 | } 39 | 40 | /** 41 | * Get the tenant identifier 42 | * 43 | * Retrieve the identifier used to publicly identify the tenant. 44 | * 45 | * @return string 46 | */ 47 | public function getTenantIdentifier(): string 48 | { 49 | /** @phpstan-ignore-next-line */ 50 | return $this->attributes[$this->getTenantIdentifierName()]; 51 | } 52 | 53 | /** 54 | * Get the name of the tenant identifier 55 | * 56 | * Retrieve the storage name for the tenant identifier, whether that's an 57 | * attribute, column name, array key or something else. 58 | * Used primarily by {@see \Sprout\Contracts\TenantProvider}. 59 | * 60 | * @return string 61 | */ 62 | public function getTenantIdentifierName(): string 63 | { 64 | return 'identifier'; 65 | } 66 | 67 | /** 68 | * Get the tenant key 69 | * 70 | * Retrieve the key used to identify a tenant internally. 71 | * 72 | * @return int|string 73 | */ 74 | public function getTenantKey(): int|string 75 | { 76 | /** @phpstan-ignore-next-line */ 77 | return $this->attributes[$this->getTenantKeyName()]; 78 | } 79 | 80 | /** 81 | * Get the name of the tenant key 82 | * 83 | * Retrieve the storage name for the tenant key, whether that's an 84 | * attribute, column name, array key or something else. 85 | * Used primarily by {@see \Sprout\Contracts\TenantProvider}. 86 | * 87 | * @return string 88 | */ 89 | public function getTenantKeyName(): string 90 | { 91 | return 'id'; 92 | } 93 | 94 | /** 95 | * Dynamically access the tenant's attributes. 96 | * 97 | * @param string $key 98 | * 99 | * @return mixed 100 | */ 101 | public function __get(string $key): mixed 102 | { 103 | return $this->attributes[$key]; 104 | } 105 | 106 | /** 107 | * Dynamically set an attribute on the tenant. 108 | * 109 | * @param string $key 110 | * @param mixed $value 111 | * 112 | * @return void 113 | */ 114 | public function __set(string $key, mixed $value): void 115 | { 116 | $this->attributes[$key] = $value; 117 | } 118 | 119 | /** 120 | * Dynamically check if a value is set on the tenant. 121 | * 122 | * @param string $key 123 | * 124 | * @return bool 125 | */ 126 | public function __isset(string $key): bool 127 | { 128 | return isset($this->attributes[$key]); 129 | } 130 | 131 | /** 132 | * Dynamically unset a value on the tenant. 133 | * 134 | * @param string $key 135 | * 136 | * @return void 137 | */ 138 | public function __unset(string $key): void 139 | { 140 | unset($this->attributes[$key]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Overrides/Auth/SproutAuthPasswordBrokerManager.php: -------------------------------------------------------------------------------- 1 | sprout = $sprout; 35 | } 36 | 37 | /** 38 | * Create a token repository instance based on the current configuration. 39 | * 40 | * @param array $config 41 | * 42 | * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface 43 | * 44 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 45 | */ 46 | protected function createTokenRepository(array $config): TokenRepositoryInterface 47 | { 48 | /** @var string $key */ 49 | $key = $this->app->make('config')->get('app.key'); 50 | 51 | // @codeCoverageIgnoreStart 52 | if (str_starts_with($key, 'base64:')) { // @infection-ignore-all 53 | $key = base64_decode(substr($key, 7)); // @infection-ignore-all 54 | } 55 | // @codeCoverageIgnoreEnd 56 | 57 | if (isset($config['driver']) && $config['driver'] === 'cache') { 58 | return new SproutAuthCacheTokenRepository( 59 | $this->sprout, 60 | $this->app->make('cache')->store($config['store'] ?? null), // @phpstan-ignore-line 61 | $this->app->make('hash'), 62 | $key, 63 | ($config['expire'] ?? 60) * 60, 64 | $config['throttle'] ?? 0, // @phpstan-ignore-line 65 | $config['prefix'] ?? '', // @phpstan-ignore-line 66 | ); 67 | } 68 | 69 | $connection = $config['connection'] ?? null; 70 | 71 | return new SproutAuthDatabaseTokenRepository( 72 | $this->sprout, 73 | $this->app->make('db')->connection($connection), // @phpstan-ignore-line 74 | $this->app->make('hash'), 75 | $config['table'], // @phpstan-ignore-line 76 | $key, 77 | $this->laravelVersionedExpiry($config['expire']),// @phpstan-ignore-line 78 | $config['throttle'] ?? 0// @phpstan-ignore-line 79 | ); 80 | } 81 | 82 | /** 83 | * Create the token expiry based on the Laravel version 84 | * 85 | * @param int|null $expiry 86 | * 87 | * @return int|null 88 | */ 89 | private function laravelVersionedExpiry(?int $expiry): ?int 90 | { 91 | if (! Str::startsWith($this->app->version(), '11.')) { 92 | return ($expiry ?? 60) * 60; 93 | } 94 | 95 | return $expiry; 96 | } 97 | 98 | /** 99 | * Check if a broker has been resolved 100 | * 101 | * @param string|null $name 102 | * 103 | * @return bool 104 | */ 105 | public function isResolved(?string $name = null): bool 106 | { 107 | return isset($this->brokers[$name ?? $this->getDefaultDriver()]); 108 | } 109 | 110 | /** 111 | * Flush the resolved brokers 112 | * 113 | * @return $this 114 | */ 115 | public function flush(): self 116 | { 117 | $this->brokers = []; 118 | 119 | return $this; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Support/ResolutionHelper.php: -------------------------------------------------------------------------------- 1 | $options 16 | * 17 | * @return array 18 | */ 19 | public static function parseOptions(array $options): array 20 | { 21 | if (count($options) === 2) { 22 | [$resolverName, $tenancyName] = $options; 23 | } else if (count($options) === 1) { 24 | [$resolverName] = $options; 25 | $tenancyName = null; 26 | } else { 27 | $resolverName = $tenancyName = null; 28 | } 29 | 30 | return [$resolverName, $tenancyName]; 31 | } 32 | 33 | /** 34 | * @param \Illuminate\Http\Request $request 35 | * @param \Sprout\Support\ResolutionHook $hook 36 | * @param string|null $resolverName 37 | * @param string|null $tenancyName 38 | * @param bool $throw 39 | * 40 | * @return bool 41 | * 42 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 43 | * @throws \Sprout\Exceptions\MisconfigurationException 44 | * @throws \Sprout\Exceptions\NoTenantFoundException 45 | */ 46 | public static function handleResolution( 47 | Request $request, 48 | ResolutionHook $hook, 49 | Sprout $sprout, 50 | ?string $resolverName = null, 51 | ?string $tenancyName = null, 52 | bool $throw = true, 53 | bool $optional = false 54 | ): bool 55 | { 56 | // Set the current hook 57 | $sprout->setCurrentHook($hook); 58 | 59 | // If the resolution hook is disabled, throw an exception 60 | if (! $sprout->supportsHook($hook)) { 61 | throw MisconfigurationException::unsupportedHook($hook); 62 | } 63 | 64 | $resolver = $sprout->resolvers()->get($resolverName); 65 | $tenancy = $sprout->tenancies()->get($tenancyName); 66 | 67 | /** 68 | * @var \Sprout\Contracts\IdentityResolver $resolver 69 | * @var \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy 70 | */ 71 | 72 | if ($tenancy->check() || ! $resolver->canResolve($request, $tenancy, $hook)) { 73 | return false; 74 | } 75 | 76 | $sprout->setCurrentTenancy($tenancy); 77 | 78 | /** @var \Illuminate\Routing\Route|null $route */ 79 | $route = $request->route(); 80 | 81 | // Is the resolver using a parameter, and is the parameter present? 82 | if ( 83 | $resolver instanceof IdentityResolverUsesParameters 84 | && $route !== null 85 | && $route->hasParameter($resolver->getRouteParameterName($tenancy)) 86 | ) { 87 | // Use the route to resolve the identity from the parameter 88 | $identity = $resolver->resolveFromRoute($route, $tenancy, $request); 89 | $route->forgetParameter($resolver->getRouteParameterName($tenancy)); 90 | } else { 91 | // If we reach here, either the resolver doesn't use parameters, or 92 | // the parameter isn't present in the URL, so we'll default to 93 | // using the request 94 | $identity = $resolver->resolveFromRequest($request, $tenancy); 95 | } 96 | 97 | // If there's no identity, and this is an optional resolution, we will 98 | // just return early 99 | if ($identity === null && $optional) { 100 | return false; 101 | } 102 | 103 | // Make sure the tenancy is aware of the resolver that was used to 104 | // resolve its tenant 105 | $tenancy->resolvedVia($resolver)->resolvedAt($hook); 106 | 107 | if ($identity === null || $tenancy->identify($identity) === false) { 108 | if ($throw) { 109 | throw NoTenantFoundException::make($resolver->getName(), $tenancy->getName()); 110 | } 111 | 112 | return false; 113 | } 114 | 115 | return true; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Overrides/AuthPasswordOverride.php: -------------------------------------------------------------------------------- 1 | removeDeferredServices(['auth.password']); 41 | 42 | // I'm intentionally not removing the deferred service 43 | // 'auth.password.broker' as it will proxy to our new 'auth.password'. 44 | 45 | // This is the actual thing we need. 46 | $app->singleton('auth.password', function ($app) use ($sprout) { 47 | return new SproutAuthPasswordBrokerManager($app, $sprout); 48 | }); 49 | 50 | // While it's unlikely that the password broker has been resolved, 51 | // it's possible, and as it's shared, we'll make the container forget it. 52 | if ($app->resolved('auth.password')) { 53 | $app->forgetInstance('auth.password'); 54 | } 55 | 56 | // I would ideally also like to mark the password reset service provider 57 | // as loaded here, but that method is protected. 58 | } 59 | 60 | /** 61 | * Set up the service override 62 | * 63 | * This method should perform any necessary setup actions for the service 64 | * override. 65 | * It is called when a new tenant is marked as the current tenant. 66 | * 67 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 68 | * @param \Sprout\Contracts\Tenant $tenant 69 | * 70 | * @return void 71 | */ 72 | public function setup(Tenancy $tenancy, Tenant $tenant): void 73 | { 74 | $this->flushPasswordBrokers(); 75 | } 76 | 77 | /** 78 | * Clean up the service override 79 | * 80 | * This method should perform any necessary setup actions for the service 81 | * override. 82 | * It is called when the current tenant is unset, either to be replaced 83 | * by another tenant, or none. 84 | * 85 | * It will be called before {@see self::setup()}, but only if the previous 86 | * tenant was not null. 87 | * 88 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 89 | * @param \Sprout\Contracts\Tenant $tenant 90 | * 91 | * @return void 92 | */ 93 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void 94 | { 95 | $this->flushPasswordBrokers(); 96 | } 97 | 98 | /** 99 | * Flush all password brokers 100 | * 101 | * @return void 102 | */ 103 | protected function flushPasswordBrokers(): void 104 | { 105 | // Same as with 'auth' above, we only want to run this code if the 106 | // password broker has been resolved already. 107 | if ($this->getApp()->resolved('auth.password')) { 108 | /** @var \Illuminate\Auth\Passwords\PasswordBrokerManager $passwordBroker */ 109 | $passwordBroker = $this->getApp()->make('auth.password'); 110 | 111 | // The flush method only exists on our custom implementation 112 | if ($passwordBroker instanceof SproutAuthPasswordBrokerManager) { 113 | $passwordBroker->flush(); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Overrides/FilesystemOverride.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $drivers = []; 22 | 23 | /** 24 | * Get the drivers that have been resolved 25 | * 26 | * @return array 27 | */ 28 | public function getDrivers(): array 29 | { 30 | return $this->drivers; 31 | } 32 | 33 | /** 34 | * Boot a service override 35 | * 36 | * This method should perform any initial steps required for the service 37 | * override that take place during the booting of the framework. 38 | * 39 | * @param \Illuminate\Contracts\Foundation\Application $app 40 | * @param \Sprout\Sprout $sprout 41 | * 42 | * @return void 43 | * 44 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 45 | */ 46 | public function boot(Application $app, Sprout $sprout): void 47 | { 48 | $tracker = fn (string $store) => $this->drivers[] = $store; 49 | 50 | // If the filesystem manager has been resolved, we can add the driver 51 | if ($app->resolved('filesystem')) { 52 | $this->addDriver($app->make('filesystem'), $sprout, $tracker); 53 | } else { 54 | // But if it hasn't, we'll add it once it is 55 | $app->afterResolving('filesystem', function (FilesystemManager $manager) use ($sprout, $tracker) { 56 | $this->addDriver($manager, $sprout, $tracker); 57 | }); 58 | } 59 | } 60 | 61 | protected function addDriver(FilesystemManager $manager, Sprout $sprout, Closure $tracker): void 62 | { 63 | $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout, $tracker): Filesystem { 64 | // If the config contains the disk name 65 | if (isset($config['name'])) { 66 | // Track it 67 | $tracker($config['name']); 68 | } 69 | 70 | return (new SproutFilesystemDriverCreator($app, $manager, $config, $sprout))(); 71 | }); 72 | } 73 | 74 | /** 75 | * Clean up the service override 76 | * 77 | * This method should perform any necessary setup actions for the service 78 | * override. 79 | * It is called when the current tenant is unset, either to be replaced 80 | * by another tenant, or none. 81 | * 82 | * It will be called before {@see self::setup()}, but only if the previous 83 | * tenant was not null. 84 | * 85 | * @template TenantClass of \Sprout\Contracts\Tenant 86 | * 87 | * @param \Sprout\Contracts\Tenancy $tenancy 88 | * @param \Sprout\Contracts\Tenant $tenant 89 | * 90 | * @phpstan-param TenantClass $tenant 91 | * 92 | * @return void 93 | */ 94 | public function cleanup(Tenancy $tenancy, Tenant $tenant): void 95 | { 96 | if ($this->getApp()->resolved('filesystem')) { 97 | /** @var \Illuminate\Filesystem\FilesystemManager $filesystemManager */ 98 | $filesystemManager = $this->getApp()->make(FilesystemManager::class); 99 | 100 | // If we're tracking some drivers we can simply forget those 101 | if (! empty($this->getDrivers())) { 102 | $filesystemManager->forgetDisk($this->getDrivers()); 103 | $this->drivers = []; 104 | } 105 | 106 | /** @var array> $diskConfig */ 107 | $diskConfig = $this->getApp()->make('config')->get('filesystems.disks', []); 108 | 109 | // But if we don't, we have to cycle through the config and pick out 110 | // any that have the 'sprout' driver 111 | foreach ($diskConfig as $disk => $config) { 112 | if (($config['driver'] ?? null) === 'sprout') { 113 | $filesystemManager->forgetDisk($disk); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Scopes/BelongsToTenantScope.php: -------------------------------------------------------------------------------- 1 | $builder 33 | * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 34 | * 35 | * @phpstan-param ModelClass $model 36 | * 37 | * @return void 38 | * 39 | * @throws \Sprout\Exceptions\TenantMissingException 40 | */ 41 | public function apply(Builder $builder, Model $model): void 42 | { 43 | /** 44 | * This has to be here because it errors if it's in the method docblock, 45 | * though I've no idea why. 46 | * 47 | * @var ModelClass&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 48 | */ 49 | 50 | /** 51 | * If the model has opted to ignore tenant restrictions, or we're outside 52 | * multitenanted context, we can exit early. 53 | */ 54 | if ($model::shouldIgnoreTenantRestrictions() || ! sprout()->withinContext()) { 55 | return; 56 | } 57 | 58 | $tenancy = $model->getTenancy(); 59 | 60 | // If there's no current tenant 61 | if (! $tenancy->check()) { 62 | // We can exit early because the tenant is optional! 63 | if ($model::isTenantOptional()) { 64 | return; 65 | } 66 | 67 | // We should throw an exception because the tenant is missing 68 | throw TenantMissingException::make($tenancy->getName()); 69 | } 70 | 71 | // Finally, add the clause so that all queries are scoped to the 72 | // current tenant. 73 | if ($model::isTenantOptional()) { 74 | // If the tenant is optional, we wrap the clause with an OR for those 75 | // that have no tenant 76 | $builder->where(function (Builder $query) use ($tenancy, $model) { 77 | $this->applyTenantClause($query, $model, $tenancy); 78 | $query->orWhereNull($model->getTenantRelation()->getForeignKeyName()); 79 | }); 80 | } else { 81 | // And if not, we just add the clause 82 | $this->applyTenantClause($builder, $model, $tenancy); 83 | } 84 | } 85 | 86 | /** 87 | * Add the actual tenant clause to the query 88 | * 89 | * This is abstracted out to avoid duplication in the above apply method. 90 | * 91 | * @template ModelClass of \Illuminate\Database\Eloquent\Model 92 | * 93 | * @param \Illuminate\Database\Eloquent\Builder $builder 94 | * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 95 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 96 | * 97 | * @phpstan-param ModelClass $model 98 | * 99 | * @return void 100 | */ 101 | protected function applyTenantClause(Builder $builder, Model $model, Tenancy $tenancy): void 102 | { 103 | /** @phpstan-ignore-next-line */ 104 | /** 105 | * This has to be here because it errors if it's in the method docblock, 106 | * though I've no idea why. 107 | * 108 | * @var ModelClass&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 109 | */ 110 | 111 | $builder->where( 112 | $model->getTenantRelation()->getForeignKeyName(), 113 | '=', 114 | $tenancy->key() 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Overrides/Cache/SproutCacheDriverCreator.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | private array $config; 33 | 34 | /** 35 | * @var \Sprout\Sprout 36 | */ 37 | private Sprout $sprout; 38 | 39 | /** 40 | * Create a new instance 41 | * 42 | * @param \Illuminate\Contracts\Foundation\Application $app 43 | * @param \Illuminate\Cache\CacheManager $manager 44 | * @param array $config 45 | * @param \Sprout\Sprout $sprout 46 | */ 47 | public function __construct(Application $app, CacheManager $manager, array $config, Sprout $sprout) 48 | { 49 | $this->app = $app; 50 | $this->config = $config; 51 | $this->manager = $manager; 52 | $this->sprout = $sprout; 53 | } 54 | 55 | /** 56 | * Create the Sprout cache driver 57 | * 58 | * @return \Illuminate\Contracts\Cache\Repository 59 | * 60 | * @throws \Sprout\Exceptions\MisconfigurationException 61 | * @throws \Sprout\Exceptions\TenancyMissingException 62 | * @throws \Sprout\Exceptions\TenantMissingException 63 | */ 64 | public function __invoke(): Repository 65 | { 66 | // If we're not within a multitenanted context, we need to error 67 | // out, as this driver shouldn't be hit without one 68 | if (! $this->sprout->withinContext()) { 69 | // TODO: Create a better exception 70 | throw TenancyMissingException::make(); 71 | } 72 | 73 | // Get the current active tenancy 74 | $tenancy = $this->sprout->getCurrentTenancy(); 75 | 76 | // If there isn't one, that's an issue as we need a tenancy 77 | if ($tenancy === null) { 78 | throw TenancyMissingException::make(); 79 | } 80 | 81 | // If there is a tenancy, but it doesn't have a tenant, that's also 82 | // an issue 83 | if ($tenancy->check() === false) { 84 | throw TenantMissingException::make($tenancy->getName()); 85 | } 86 | 87 | /** @var \Sprout\Contracts\Tenant $tenant */ 88 | $tenant = $tenancy->tenant(); 89 | 90 | // We need to know which store we're overriding to make tenanted 91 | if (! isset($this->config['override'])) { 92 | throw MisconfigurationException::missingConfig('override', 'service override', 'cache'); 93 | } 94 | 95 | // We need to get the config for that store 96 | /** @var array $storeConfig */ 97 | $storeConfig = $this->app->make('config')->get('cache.stores.' . $this->config['override']); 98 | 99 | if (empty($storeConfig)) { 100 | throw new InvalidArgumentException('Cache store [' . $this->config['override'] . '] is not defined'); 101 | } 102 | 103 | // Get the prefix for the tenanted store based on the store config, 104 | // the tenancy and its current tenant 105 | $storeConfig['prefix'] = $this->getStorePrefix($storeConfig, $tenancy, $tenant); 106 | 107 | return $this->manager->build($storeConfig); 108 | } 109 | 110 | /** 111 | * Get the prefix for the store 112 | * 113 | * @template TenantClass of \Sprout\Contracts\Tenant 114 | * 115 | * @param array $config 116 | * @param \Sprout\Contracts\Tenancy $tenancy 117 | * @param \Sprout\Contracts\Tenant $tenant 118 | * 119 | * @phpstan-param TenantClass $tenant 120 | * 121 | * @return string 122 | */ 123 | protected function getStorePrefix(array $config, Tenancy $tenancy, Tenant $tenant): string 124 | { 125 | return (isset($config['prefix']) ? $config['prefix'] . '_' : '') 126 | . $tenancy->getName() 127 | . '_' 128 | . $tenant->getTenantKey(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /resources/config/multitenancy.php: -------------------------------------------------------------------------------- 1 | [ 20 | 21 | 'tenancy' => 'tenants', 22 | 'provider' => 'tenants', 23 | 'resolver' => 'subdomain', 24 | 25 | ], 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Tenancies 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Next, you may define every tenancy type for your application. 33 | | If you only have one type of tenancy within your application, which will 34 | | be the case for most people, you can leave it at one. 35 | | 36 | | All tenancies have a tenant provider, which defines how the 37 | | tenants are actually retrieved out of your database or other storage 38 | | system used by the application. 39 | | 40 | | Tenancies can also have options, which is an array of options provided 41 | | by the TenancyOptions class that lets you fine tune the tenancies 42 | | behaviour. 43 | | 44 | */ 45 | 46 | 'tenancies' => [ 47 | 48 | 'tenants' => [ 49 | 'provider' => 'tenants', 50 | 'options' => [ 51 | TenancyOptions::hydrateTenantRelation(), 52 | TenancyOptions::throwIfNotRelated(), 53 | TenancyOptions::allOverrides(), 54 | ], 55 | ], 56 | 57 | ], 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Tenant Providers 62 | |-------------------------------------------------------------------------- 63 | | 64 | | All tenancies have a tenant provider, which defines how the 65 | | tenants are actually retrieved out of your database or other storage 66 | | system used by the application. 67 | | 68 | | If you have multiple tenant tables or models, you can configure multiple 69 | | providers to represent each. 70 | | These providers may then be assigned to any extra tenancies you have defined. 71 | | 72 | | Supported: "database", "eloquent" 73 | | 74 | */ 75 | 76 | 'providers' => [ 77 | 78 | 'tenants' => [ 79 | 'driver' => 'eloquent', 80 | 'model' => \Sprout\Database\Eloquent\Tenant::class, 81 | ], 82 | 83 | // 'backup' => [ 84 | // 'driver' => 'database', 85 | // 'table' => 'tenants', 86 | // ], 87 | 88 | ], 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Identity Resolvers 93 | |-------------------------------------------------------------------------- 94 | | 95 | | Where Laravel's auth would have a separate guard for each way that a 96 | | user can be authenticated in a request (think session, header, etc.), 97 | | Sprout abstracts this out into identity resolvers. 98 | | This means that all tenancies within the application can use all 99 | | configured resolvers. 100 | | 101 | | If you have multiple ways that tenant can be identified, say, through 102 | | subdomain and then a HTTP header for APIs, you can define one for each. 103 | | 104 | | There are sensible defaults for each supported driver, though it is 105 | | recommended that you remove any that you don't need, for simplicity 106 | | sake. 107 | | 108 | | Supported: "subdomain", "header", "path", "cookie" and "session" 109 | | 110 | */ 111 | 112 | 'resolvers' => [ 113 | 114 | 'subdomain' => [ 115 | 'driver' => 'subdomain', 116 | 'domain' => env('TENANTED_DOMAIN'), 117 | 'pattern' => '.*', 118 | ], 119 | 120 | 'header' => [ 121 | 'driver' => 'header', 122 | 'header' => '{Tenancy}-Identifier', 123 | ], 124 | 125 | 'path' => [ 126 | 'driver' => 'path', 127 | 'segment' => 1, 128 | ], 129 | 130 | 'cookie' => [ 131 | 'driver' => 'cookie', 132 | 'cookie' => '{Tenancy}-Identifier', 133 | ], 134 | 135 | 'session' => [ 136 | 'driver' => 'session', 137 | 'session' => 'multitenancy.{tenancy}', 138 | ], 139 | 140 | ], 141 | 142 | ]; 143 | -------------------------------------------------------------------------------- /src/Http/Resolvers/HeaderIdentityResolver.php: -------------------------------------------------------------------------------- 1 | $hooks 39 | */ 40 | public function __construct(string $name, ?string $header = null, array $hooks = []) 41 | { 42 | parent::__construct($name, $hooks); 43 | 44 | $this->header = $header ?? '{Tenancy}-Identifier'; 45 | } 46 | 47 | /** 48 | * Get the name of the header 49 | * 50 | * @return string 51 | */ 52 | public function getHeaderName(): string 53 | { 54 | return $this->header; 55 | } 56 | 57 | /** 58 | * Get the header name with replacements 59 | * 60 | * This method returns the name of the header returned by 61 | * {@see self::getHeaderName()}, except it replaces {tenancy} 62 | * and {resolver} with the name of the tenancy, and resolver, 63 | * respectively. 64 | * 65 | * You can use an uppercase character for the first character, {Tenancy} 66 | * and {Resolver}, and it'll be run through {@see \ucfirst()}. 67 | * 68 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 69 | * 70 | * @return string 71 | */ 72 | public function getRequestHeaderName(Tenancy $tenancy): string 73 | { 74 | return PlaceholderHelper::replace( 75 | $this->getHeaderName(), 76 | [ 77 | 'tenancy' => $tenancy->getName(), 78 | 'resolver' => $this->getName(), 79 | ] 80 | ); 81 | } 82 | 83 | /** 84 | * Get an identifier from the request 85 | * 86 | * Locates a tenant identifier within the provided request and returns it. 87 | * 88 | * @template TenantClass of \Sprout\Contracts\Tenant 89 | * 90 | * @param \Illuminate\Http\Request $request 91 | * @param \Sprout\Contracts\Tenancy $tenancy 92 | * 93 | * @return string|null 94 | */ 95 | public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string 96 | { 97 | return $request->header($this->getRequestHeaderName($tenancy)); 98 | } 99 | 100 | /** 101 | * Create a route group for the resolver 102 | * 103 | * Creates and configures a route group with the necessary settings to 104 | * support identity resolution. 105 | * 106 | * @template TenantClass of \Sprout\Contracts\Tenant 107 | * 108 | * @param \Illuminate\Routing\Router $router 109 | * @param \Closure $groupRoutes 110 | * @param \Sprout\Contracts\Tenancy $tenancy 111 | * 112 | * @return \Illuminate\Routing\RouteRegistrar 113 | * 114 | * @deprecated Use {@see self::configureRoute()} instead 115 | */ 116 | public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar 117 | { 118 | return $router->middleware([ 119 | SproutTenantContextMiddleware::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName(), 120 | AddTenantHeaderToResponse::class . ':' . $this->getName() . ',' . $tenancy->getName(), 121 | ])->group($groupRoutes); 122 | } 123 | 124 | /** 125 | * Configure the provided route for the resolver 126 | * 127 | * Configures a provided route to work with itself, adding parameters, 128 | * middleware, and anything else required, besides the default middleware. 129 | * 130 | * @param \Illuminate\Routing\RouteRegistrar $route 131 | * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy 132 | * 133 | * @return void 134 | */ 135 | public function configureRoute(RouteRegistrar $route, Tenancy $tenancy): void 136 | { 137 | $route->middleware([ 138 | AddTenantHeaderToResponse::class . ':' . $this->getName() . ',' . $tenancy->getName(), 139 | ]); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Contracts/IdentityResolver.php: -------------------------------------------------------------------------------- 1 | $tenancy 38 | * 39 | * @return string|null 40 | */ 41 | public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string; 42 | 43 | /** 44 | * Create a route group for the resolver 45 | * 46 | * Creates and configures a route group with the necessary settings to 47 | * support identity resolution. 48 | * 49 | * @template TenantClass of \Sprout\Contracts\Tenant 50 | * 51 | * @param \Illuminate\Routing\Router $router 52 | * @param \Closure $groupRoutes 53 | * @param \Sprout\Contracts\Tenancy $tenancy 54 | * 55 | * @return \Illuminate\Routing\RouteRegistrar 56 | * 57 | * @deprecated Use {@see self::configureRoute()} instead 58 | */ 59 | public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar; 60 | 61 | /** 62 | * Configure the provided route for the resolver 63 | * 64 | * Configures a provided route to work with itself, adding parameters, 65 | * middleware, and anything else required, besides the default middleware. 66 | * 67 | * @param \Illuminate\Routing\RouteRegistrar $route 68 | * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy 69 | * 70 | * @return void 71 | */ 72 | public function configureRoute(RouteRegistrar $route, Tenancy $tenancy): void; 73 | 74 | /** 75 | * Perform setup actions for the tenant 76 | * 77 | * When a tenant is marked as the current tenant within a tenancy, this 78 | * method will be called to perform any necessary setup actions. 79 | * This method is also called if there is no current tenant, as there may 80 | * be actions needed. 81 | * 82 | * @template TenantClass of \Sprout\Contracts\Tenant 83 | * 84 | * @param \Sprout\Contracts\Tenancy $tenancy 85 | * @param \Sprout\Contracts\Tenant|null $tenant 86 | * 87 | * @phpstan-param TenantClass|null $tenant 88 | * 89 | * @return void 90 | */ 91 | public function setup(Tenancy $tenancy, ?Tenant $tenant): void; 92 | 93 | /** 94 | * Can the resolver run on the request 95 | * 96 | * This method allows a resolver to prevent resolution with the request in 97 | * its current state, whether that means it's too early, or too late. 98 | * 99 | * @template TenantClass of \Sprout\Contracts\Tenant 100 | * 101 | * @param \Illuminate\Http\Request $request 102 | * @param \Sprout\Contracts\Tenancy $tenancy 103 | * @param \Sprout\Support\ResolutionHook $hook 104 | * 105 | * @return bool 106 | */ 107 | public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $hook): bool; 108 | 109 | /** 110 | * Generate a URL for a tenanted route 111 | * 112 | * This method wraps Laravel's {@see \route()} helper to allow for 113 | * identity resolvers that use route parameters. 114 | * Route parameter names are dynamic and configurable, so hard-coding them 115 | * is less than ideal. 116 | * 117 | * This method is only really useful for identity resolvers that use route 118 | * parameters, but, it's here for backwards compatibility. 119 | * 120 | * @template TenantClass of \Sprout\Contracts\Tenant 121 | * 122 | * @param string $name 123 | * @param \Sprout\Contracts\Tenancy $tenancy 124 | * @param \Sprout\Contracts\Tenant $tenant 125 | * @param array $parameters 126 | * @param bool $absolute 127 | * 128 | * @phpstan-param TenantClass $tenant 129 | * 130 | * @return string 131 | */ 132 | public function route(string $name, Tenancy $tenancy, Tenant $tenant, array $parameters = [], bool $absolute = true): string; 133 | } 134 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php: -------------------------------------------------------------------------------- 1 | $builder 33 | * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 34 | * 35 | * @phpstan-param ModelClass $model 36 | * 37 | * @return void 38 | * 39 | * @throws \Sprout\Exceptions\TenantMissingException 40 | * @throws \Sprout\Exceptions\TenantRelationException 41 | */ 42 | public function apply(Builder $builder, Model $model): void 43 | { 44 | /** 45 | * This has to be here otherwise the 'BelongsToTenant' 46 | * trait referenced in the below docblock causes PHPStan to error. 47 | * HILARIOUSLY, if I remove that trait from the docblock, PHPStan 48 | * will add a bunch more errors because a lot of the following code 49 | * relies on methods added by that trait. 50 | * 51 | * @phpstan-ignore-next-line 52 | */ 53 | /** 54 | * This has to be here because it errors if it's in the method docblock, 55 | * though I've no idea why. 56 | * 57 | * @var ModelClass&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 58 | */ 59 | if ($model::shouldIgnoreTenantRestrictions() || ! sprout()->withinContext()) { 60 | return; 61 | } 62 | 63 | $tenancy = $model->getTenancy(); 64 | 65 | // If there's no current tenant 66 | if (! $tenancy->check()) { 67 | // We can exit early because the tenant is optional! 68 | if ($model::isTenantOptional()) { 69 | return; 70 | } 71 | 72 | // We should throw an exception because the tenant is missing 73 | throw TenantMissingException::make($tenancy->getName()); 74 | } 75 | 76 | // Finally, add the clause so that all queries are scoped to the 77 | // current tenant. 78 | if ($model::isTenantOptional()) { 79 | // If the tenant is optional, we wrap the clause with an OR for those 80 | // that have no tenant 81 | $builder->where(function (Builder $query) use ($tenancy, $model) { 82 | $this->applyTenantClause($query, $model, $tenancy); 83 | $query->orDoesntHave($model->getTenantRelationName()); 84 | }); 85 | } else { 86 | // And if not, we just add the clause 87 | $this->applyTenantClause($builder, $model, $tenancy); 88 | } 89 | } 90 | 91 | /** 92 | * Add the actual tenant clause to the query 93 | * 94 | * This is abstracted out to avoid duplication in the above apply method. 95 | * 96 | * @template ModelClass of \Illuminate\Database\Eloquent\Model 97 | * 98 | * @param \Illuminate\Database\Eloquent\Builder $builder 99 | * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 100 | * @param \Sprout\Contracts\Tenancy<*> $tenancy 101 | * 102 | * @phpstan-param ModelClass $model 103 | * 104 | * @return void 105 | * @throws \Sprout\Exceptions\TenantRelationException 106 | */ 107 | protected function applyTenantClause(Builder $builder, Model $model, Tenancy $tenancy): void 108 | { 109 | /** @phpstan-ignore-next-line */ 110 | /** 111 | * This has to be here because it errors if it's in the method docblock, 112 | * though I've no idea why. 113 | * 114 | * @var ModelClass&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model 115 | */ 116 | 117 | $builder->whereHas( 118 | $model->getTenantRelationName(), 119 | function (Builder $builder) use ($tenancy) { 120 | $builder->whereKey($tenancy->key()); 121 | }, 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Support/BaseIdentityResolver.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | private array $hooks; 35 | 36 | /** 37 | * Create a new instance 38 | * 39 | * @param string $name 40 | * @param array<\Sprout\Support\ResolutionHook> $hooks 41 | */ 42 | public function __construct(string $name, array $hooks = []) 43 | { 44 | $this->name = $name; 45 | $this->hooks = empty($hooks) ? [ResolutionHook::Routing] : $hooks; 46 | } 47 | 48 | /** 49 | * Get the registered name of the resolver 50 | * 51 | * @return string 52 | */ 53 | public function getName(): string 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * Get the hooks this resolver uses 60 | * 61 | * @return array<\Sprout\Support\ResolutionHook> 62 | */ 63 | public function getHooks(): array 64 | { 65 | return $this->hooks; 66 | } 67 | 68 | /** 69 | * Perform setup actions for the tenant 70 | * 71 | * When a tenant is marked as the current tenant within a tenancy, this 72 | * method will be called to perform any necessary setup actions. 73 | * This method is also called if there is no current tenant, as there may 74 | * be actions needed. 75 | * 76 | * @template TenantClass of \Sprout\Contracts\Tenant 77 | * 78 | * @param \Sprout\Contracts\Tenancy $tenancy 79 | * @param \Sprout\Contracts\Tenant|null $tenant 80 | * 81 | * @phpstan-param Tenant|null $tenant 82 | * 83 | * @return void 84 | */ 85 | public function setup(Tenancy $tenancy, ?Tenant $tenant): void 86 | { 87 | // This is intentionally empty 88 | } 89 | 90 | /** 91 | * Can the resolver run on the request 92 | * 93 | * This method allows a resolver to prevent resolution with the request in 94 | * its current state, whether that means it's too early, or too late. 95 | * 96 | * @template TenantClass of \Sprout\Contracts\Tenant 97 | * 98 | * @param \Illuminate\Http\Request $request 99 | * @param \Sprout\Contracts\Tenancy $tenancy 100 | * @param \Sprout\Support\ResolutionHook $hook 101 | * 102 | * @return bool 103 | */ 104 | public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $hook): bool 105 | { 106 | return ! $tenancy->wasResolved() && in_array($hook, $this->hooks, true); 107 | } 108 | 109 | /** 110 | * Generate a URL for a tenanted route 111 | * 112 | * This method wraps Laravel's {@see \route()} helper to allow for 113 | * identity resolvers that use route parameters. 114 | * Route parameter names are dynamic and configurable, so hard-coding them 115 | * is less than ideal. 116 | * 117 | * This method is only really useful for identity resolvers that use route 118 | * parameters, but, it's here for backwards compatibility. 119 | * 120 | * @template TenantClass of \Sprout\Contracts\Tenant 121 | * 122 | * @param string $name 123 | * @param \Sprout\Contracts\Tenancy $tenancy 124 | * @param \Sprout\Contracts\Tenant $tenant 125 | * @param array $parameters 126 | * @param bool $absolute 127 | * 128 | * @phpstan-param TenantClass $tenant 129 | * 130 | * @return string 131 | */ 132 | public function route(string $name, Tenancy $tenancy, Tenant $tenant, array $parameters = [], bool $absolute = true): string 133 | { 134 | return route($name, $parameters, $absolute); 135 | } 136 | 137 | /** 138 | * Configure the provided route for the resolver 139 | * 140 | * Configures a provided route to work with itself, adding parameters, 141 | * middleware, and anything else required, besides the default middleware. 142 | * 143 | * @param \Illuminate\Routing\RouteRegistrar $route 144 | * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy 145 | * 146 | * @return void 147 | */ 148 | public function configureRoute(RouteRegistrar $route, Tenancy $tenancy): void 149 | { 150 | // This is intentionally empty 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![Packagist Version](https://img.shields.io/packagist/v/sprout/sprout) 4 | ![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/sprout/sprout) 5 | 6 | ![GitHub](https://img.shields.io/github/license/sprout-laravel/sprout) 7 | ![Laravel](https://img.shields.io/badge/laravel-11.x-red.svg) 8 | [![codecov](https://codecov.io/gh/sprout-laravel/sprout/branch/1.x/graph/badge.svg?token=FHJ41NQMTA)](https://codecov.io/gh/sprout-laravel/sprout) 9 | 10 | ![Unit Tests](https://github.com/sprout-laravel/sprout/actions/workflows/tests.yml/badge.svg) 11 | ![Static Analysis](https://github.com/sprout-laravel/sprout/actions/workflows/static-analysis.yml/badge.svg) 12 | 13 | # Sprout for Laravel 14 | A flexible, seamless, and easy to use multitenancy solution for Laravel 15 | 16 | Sprout is a multitenancy package for Laravel that fits seamlessly into your application. 17 | It provides a whole host of features, with the flexibility to allow you to add your own. 18 | You can read all about Sprouts features and how to get started in 19 | the [documentation](https://sprout.ollieread.com/docs/1.x). 20 | 21 | ## Features 22 | 23 | Sprout comes out of the box with the following features: 24 | 25 | - **Tenant Identification**: 26 | Sprout can identify tenants by _subdomain_, _path_, _header_, _cookie_ or session. 27 | It can do this immediately once a route is matched, or during the middleware stack. 28 | - **Multiple Tenancies**: 29 | Sprout can handle multiple tenancies, as in multiple different models that are tenants. 30 | - **Tenant Storage Disks**: 31 | Sprout comes with a service override that allows you to create a storage disk that 32 | always points to the current tenant's storage directory. 33 | - **Tenant Sessions & Cookies**: 34 | If you're identifying tenants via a method that uses the URL (subdomain or path), 35 | cookies, and therefore the session cookie, will be automatically scoped to the current tenant. 36 | - **Tenant Cache Stores**: 37 | Just like with storage disks, Sprout allows you to create a cache store that always 38 | returns the current tenants cache. 39 | - **Tenant Aware Jobs**: 40 | When a job is dispatched, Sprout will make sure that any tenancies that are active are 41 | recreated when the job is processing, along with their current tenants. 42 | - **Tenant Password Resets**: 43 | If you're following a model where users belong to a single tenant, you'll also want to 44 | make sure that password resets are scoped to the tenant. 45 | Sprout can do this for you. 46 | - **Automatic Scoping**: 47 | As well as all the automated scoping of storage disks, cache stores, jobs, password resets 48 | and so on, Sprout also comes with a set of functionality for automatically scoping models, during creation and 49 | querying. 50 | 51 | There are also three upcoming first-party addons for Sprout: 52 | 53 | - [**Sprout Bud**](https://github.com/sprout-laravel/bud): 54 | Bud allows you to manage tenant-specific 55 | configuration, with built-in support for 56 | dynamically configuring a whole of Laravels core connections and driver-based services. 57 | - [**Sprout Seedling**](https://github.com/sprout-laravel/seedling): 58 | Seedling builds on-top of the functionality 59 | provided by Sprout Bud, to bring multi-tenant-specific database support to your Laravel application. 60 | As well as enabling the dynamic configuration of connections, it comes with a batch of supporting functionality to 61 | make managing tenant-specific databases easier. 62 | - [**Sprout Terra**](https://github.com/sprout-laravel/terra): 63 | Terra brings _domain_-based identification to 64 | Sprout, allowing you to identify tenants based on the domain they are accessing your application from. 65 | Just like with Seedling, it also comes with a bunch of supporting functionality for dealing with tenant domains. 66 | 67 | ## FAQ 68 | 69 | ### Does Sprout support tenant-specific databases? 70 | It will do through [Sprout Seedling](https://github.com/sprout-laravel/seedling), which is currently in development. 71 | 72 | ### Why are tenant-specific databases handled by an addon? 73 | I didn't want to just provide a barebones implementation of tenant-specific database handling, as there's a lot more 74 | to think about than just changing the connection on a model. 75 | So, I wanted to provide the feature with a bunch of opt-in supporting functionality, which is why it's an addon. 76 | 77 | ### Does sprout support domain-based identification? 78 | It will do through [Sprout Terra](https://github.com/sprout-laravel/terra), which is currently in development. 79 | 80 | ### Why are domain-based tenants handled by an addon? 81 | The same as with Seedling, I wanted to provide a bunch of supporting functionality to make managing domain-based tenants 82 | easier, which is why it's an addon. 83 | 84 | ### Why should I use Sprout over other multitenancy packages? 85 | It will mostly come down to preference, but I've tried to make Sprout as flexible as possible, without compromising on 86 | any features and functionality. 87 | It is built to be as seamless as possible, avoiding hacky workarounds and providing a clean API. 88 | No magic to dynamically modify things, or artificial limitations, whether side effects or not. 89 | 90 | ### Why did you build Sprout? 91 | I found that the existing multitenancy packages for Laravel were either too opinionated, not flexible enough, or 92 | lacking in features. 93 | I wanted to build something that was flexible, feature-rich, and easy to use. 94 | -------------------------------------------------------------------------------- /src/Managers/TenantProviderManager.php: -------------------------------------------------------------------------------- 1 | 21 | * 22 | * @package Core 23 | */ 24 | final class TenantProviderManager extends BaseFactory 25 | { 26 | /** 27 | * Get the name used by this factory 28 | * 29 | * @return string 30 | */ 31 | public function getFactoryName(): string 32 | { 33 | return 'provider'; 34 | } 35 | 36 | /** 37 | * Get the config key for the given name 38 | * 39 | * @param string $name 40 | * 41 | * @return string 42 | */ 43 | public function getConfigKey(string $name): string 44 | { 45 | return 'multitenancy.providers.' . $name; 46 | } 47 | 48 | /** 49 | * Create the eloquent tenant provider 50 | * 51 | * @param array $config 52 | * @param string $name 53 | * 54 | * @return \Sprout\Providers\EloquentTenantProvider 55 | * 56 | * @template TenantModel of \Illuminate\Database\Eloquent\Model&\Sprout\Contracts\Tenant 57 | * 58 | * @phpstan-param array{model?: class-string} $config 59 | * 60 | * @phpstan-return \Sprout\Providers\EloquentTenantProvider 61 | * 62 | * @throws \Sprout\Exceptions\MisconfigurationException 63 | */ 64 | protected function createEloquentProvider(array $config, string $name): EloquentTenantProvider 65 | { 66 | if (! isset($config['model'])) { 67 | throw MisconfigurationException::missingConfig('model', 'provider', $name); 68 | } 69 | 70 | if ( 71 | ! class_exists($config['model']) 72 | || ! is_subclass_of($config['model'], Model::class) 73 | || ! is_subclass_of($config['model'], Tenant::class) 74 | ) { 75 | throw MisconfigurationException::invalidConfig('model', 'provider', $name, $config['model']); 76 | } 77 | 78 | return new EloquentTenantProvider($name, $config['model']); 79 | } 80 | 81 | /** 82 | * Create the database tenant provider 83 | * 84 | * @param array $config 85 | * @param string $name 86 | * 87 | * @return \Sprout\Providers\DatabaseTenantProvider 88 | * 89 | * @template TenantEntity of \Sprout\Contracts\Tenant 90 | * 91 | * @phpstan-param array{entity?: class-string, table?: string|class-string<\Illuminate\Database\Eloquent\Model>, connection?: string} $config 92 | * 93 | * @phpstan-return \Sprout\Providers\DatabaseTenantProvider 94 | * 95 | * @throws \Sprout\Exceptions\MisconfigurationException 96 | */ 97 | protected function createDatabaseProvider(array $config, string $name): DatabaseTenantProvider 98 | { 99 | if ( 100 | isset($config['entity']) 101 | && ( 102 | ! class_exists($config['entity']) 103 | || ! is_subclass_of($config['entity'], Tenant::class) 104 | ) 105 | ) { 106 | throw MisconfigurationException::invalidConfig('entity', 'provider', $name, $config['entity']); 107 | } 108 | 109 | if (! isset($config['table'])) { 110 | throw MisconfigurationException::missingConfig('table', 'provider', $name); 111 | } 112 | 113 | // This allows users to provide a model name for retrieval of table and 114 | // connection name, in case they need a backup without Eloquent 115 | if (class_exists($config['table'])) { 116 | // It's worth checking that the provided value is in fact a model, 117 | // otherwise things are going to get awkward 118 | if (! is_subclass_of($config['table'], Model::class)) { 119 | throw MisconfigurationException::invalidConfig('table', 'provider', $name, $config['table']); 120 | } 121 | 122 | $model = new $config['table'](); 123 | $table = $model->getTable(); 124 | $connection = $model->getConnectionName(); 125 | } else { 126 | $table = $config['table']; 127 | $connection = $config['connection'] ?? null; 128 | } 129 | 130 | /** 131 | * @var \Illuminate\Database\ConnectionInterface $connection 132 | * @phpstan-ignore-next-line 133 | */ 134 | $connection = $this->app['db']->connection($connection); 135 | 136 | return new DatabaseTenantProvider( 137 | $name, 138 | $connection, 139 | $table, 140 | $config['entity'] ?? GenericTenant::class 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Providers/EloquentTenantProvider.php: -------------------------------------------------------------------------------- 1 | 22 | * 23 | * @package Providers 24 | * 25 | * @internal New instances are created with {@see \Sprout\Managers\TenantProviderManager::createEloquentProvider()}, and shouldn't be created manually 26 | */ 27 | final class EloquentTenantProvider extends BaseTenantProvider 28 | { 29 | /** 30 | * The model class 31 | * 32 | * @var class-string 33 | * 34 | * @phpstan-var class-string 35 | */ 36 | private string $modelClass; 37 | 38 | /** 39 | * A model instance to work from 40 | * 41 | * @var \Illuminate\Database\Eloquent\Model 42 | * 43 | * @phpstan-var TenantModel 44 | */ 45 | private Model $model; 46 | 47 | /** 48 | * Create a new instance of the eloquent tenant provider 49 | * 50 | * @param string $name 51 | * @param class-string $modelClass 52 | * 53 | * @phpstan-param class-string $modelClass 54 | */ 55 | public function __construct(string $name, string $modelClass) 56 | { 57 | parent::__construct($name); 58 | 59 | $this->modelClass = $modelClass; 60 | } 61 | 62 | /** 63 | * Get the model class 64 | * 65 | * @return string 66 | * 67 | * @phpstan-return class-string 68 | */ 69 | public function getModelClass(): string 70 | { 71 | return $this->modelClass; 72 | } 73 | 74 | /** 75 | * Get an instance of the tenant model 76 | * 77 | * @return \Illuminate\Database\Eloquent\Model&\Sprout\Contracts\Tenant 78 | * 79 | * @phpstan-return TenantModel 80 | */ 81 | private function getModel(): Model&Tenant 82 | { 83 | if (! isset($this->model)) { 84 | $this->model = new $this->modelClass(); 85 | } 86 | 87 | return $this->model; 88 | } 89 | 90 | /** 91 | * Retrieve a tenant by its identifier 92 | * 93 | * Gets an instance of the tenant implementation the provider represents, 94 | * using an identifier. 95 | * 96 | * @param string $identifier 97 | * 98 | * @return \Sprout\Contracts\Tenant|null 99 | * 100 | * @see \Sprout\Contracts\Tenant::getTenantIdentifier() 101 | * @see \Sprout\Contracts\Tenant::getTenantIdentifierName() 102 | * 103 | * @phpstan-return TenantModel|null 104 | */ 105 | public function retrieveByIdentifier(string $identifier): ?Tenant 106 | { 107 | $model = $this->getModel(); 108 | 109 | return $model->newModelQuery() 110 | ->where($model->getTenantIdentifierName(), $identifier) 111 | ->first(); 112 | } 113 | 114 | /** 115 | * Retrieve a tenant by its key 116 | * 117 | * Gets an instance of the tenant implementation the provider represents, 118 | * using a key. 119 | * 120 | * @param int|string $key 121 | * 122 | * @return \Sprout\Contracts\Tenant|null 123 | * 124 | * @see \Sprout\Contracts\Tenant::getTenantKey() 125 | * @see \Sprout\Contracts\Tenant::getTenantKeyName() 126 | * 127 | * @phpstan-return TenantModel|null 128 | */ 129 | public function retrieveByKey(int|string $key): ?Tenant 130 | { 131 | $model = $this->getModel(); 132 | 133 | return $model->newModelQuery() 134 | ->where($model->getTenantKeyName(), $key) 135 | ->first(); 136 | } 137 | 138 | /** 139 | * Retrieve a tenant by its resource key 140 | * 141 | * Gets an instance of the tenant implementation the provider represents, 142 | * using a resource key. 143 | * The tenant class must implement the {@see \Sprout\Contracts\TenantHasResources} 144 | * interface for this method to work. 145 | * 146 | * @param string $resourceKey 147 | * 148 | * @return (\Sprout\Contracts\Tenant&\Sprout\Contracts\TenantHasResources)|null 149 | * 150 | * @throws \Sprout\Exceptions\MisconfigurationException 151 | * 152 | * @phpstan-return (TenantModel&\Sprout\Contracts\TenantHasResources)|null 153 | * 154 | * @see \Sprout\Contracts\TenantHasResources::getTenantResourceKeyName() 155 | * @see \Sprout\Contracts\TenantHasResources::getTenantResourceKey() 156 | */ 157 | public function retrieveByResourceKey(string $resourceKey): (Tenant&TenantHasResources)|null 158 | { 159 | $model = $this->getModel(); 160 | 161 | if (! ($model instanceof TenantHasResources)) { 162 | throw MisconfigurationException::misconfigured('tenant', $model::class, 'resources'); 163 | } 164 | 165 | return $model->newModelQuery() 166 | ->where($model->getTenantResourceKeyName(), $resourceKey) 167 | ->first(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Database/Eloquent/Concerns/IsTenantChild.php: -------------------------------------------------------------------------------- 1 | getMethods(ReflectionMethod::IS_PUBLIC)) 122 | ->filter(function (ReflectionMethod $method) { 123 | return ! $method->isStatic() && $method->getAttributes(TenantRelation::class); 124 | }) 125 | ->map(fn (ReflectionMethod $method) => $method->getName()); 126 | 127 | if ($methods->isEmpty()) { 128 | throw TenantRelationException::missing(static::class); 129 | } 130 | 131 | if ($methods->count() > 1) { 132 | throw TenantRelationException::tooMany(static::class, $methods->count()); 133 | } 134 | 135 | return $methods->first(); 136 | } catch (ReflectionException $exception) { 137 | throw TenantRelationException::missing(static::class, previous: $exception); // @codeCoverageIgnore 138 | } 139 | } 140 | 141 | /** 142 | * Get the name of the tenant relation 143 | * 144 | * @return string|null 145 | * 146 | * @throws \Sprout\Exceptions\TenantRelationException 147 | */ 148 | public function getTenantRelationName(): ?string 149 | { 150 | if (! isset(static::$tenantRelationName)) { 151 | static::$tenantRelationName = $this->findTenantRelationName(); 152 | } 153 | 154 | return static::$tenantRelationName ?? null; 155 | } 156 | 157 | /** 158 | * Get the name of the tenancy this model relates to a tenant of 159 | * 160 | * @return string|null 161 | */ 162 | public function getTenancyName(): ?string 163 | { 164 | return null; 165 | } 166 | 167 | /** 168 | * Get the tenancy this model relates to a tenant of 169 | * 170 | * @return \Sprout\Contracts\Tenancy 171 | */ 172 | public function getTenancy(): Tenancy 173 | { 174 | /** @var \Sprout\Managers\TenancyManager $tenancyManager */ 175 | $tenancyManager = app(TenancyManager::class); 176 | 177 | return $tenancyManager->get($this->getTenancyName()); 178 | } 179 | 180 | /** 181 | * Get the tenant relation 182 | * 183 | * @return \Illuminate\Database\Eloquent\Relations\Relation 184 | */ 185 | public function getTenantRelation(): Relation 186 | { 187 | return $this->{$this->getTenantRelationName()}(); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Contracts/Tenancy.php: -------------------------------------------------------------------------------- 1 | tenant() 37 | * @phpstan-assert-if-true string $this->identifier() 38 | * @phpstan-assert-if-true string|int $this->key() 39 | * @phpstan-assert-if-false null $this->tenant() 40 | * @phpstan-assert-if-false null $this->identifier() 41 | * @phpstan-assert-if-false null $this->key() 42 | */ 43 | public function check(): bool; 44 | 45 | /** 46 | * Get the current tenant 47 | * 48 | * Gets the current set tenant if one is present. 49 | * Implementations may attempt to load a tenant if one isn't present, though 50 | * this is not required. 51 | * 52 | * @return \Sprout\Contracts\Tenant|null 53 | * 54 | * @phpstan-return TenantClass|null 55 | */ 56 | public function tenant(): ?Tenant; 57 | 58 | /** 59 | * Get the tenants key 60 | * 61 | * Get the tenant key for the current tenant if there is one. 62 | * 63 | * @return int|string|null 64 | * @see \Sprout\Contracts\Tenant::getTenantKey() 65 | * 66 | */ 67 | public function key(): int|string|null; 68 | 69 | /** 70 | * Get the tenants' identifier 71 | * 72 | * Get the tenant identifier for the current tenant if there is one. 73 | * 74 | * @return string|null 75 | * @see \Sprout\Contracts\Tenant::getTenantIdentifier() 76 | * 77 | */ 78 | public function identifier(): ?string; 79 | 80 | /** 81 | * Identity a tenant 82 | * 83 | * Retrieve and set the current tenant based on an identifier. 84 | * 85 | * @param string $identifier 86 | * 87 | * @return bool 88 | */ 89 | public function identify(string $identifier): bool; 90 | 91 | /** 92 | * Load a tenant 93 | * 94 | * Retrieve and set the current tenant based on a key. 95 | * 96 | * @param int|string $key 97 | * 98 | * @return bool 99 | */ 100 | public function load(int|string $key): bool; 101 | 102 | /** 103 | * Get the tenant provider 104 | * 105 | * Get the tenant provider used by this tenancy. 106 | * 107 | * @return \Sprout\Contracts\TenantProvider 108 | */ 109 | public function provider(): TenantProvider; 110 | 111 | /** 112 | * Set the identity resolved used 113 | * 114 | * @param \Sprout\Contracts\IdentityResolver $resolver 115 | * 116 | * @return static 117 | */ 118 | public function resolvedVia(IdentityResolver $resolver): static; 119 | 120 | /** 121 | * Set the hook where the tenant was resolved 122 | * 123 | * @param \Sprout\Support\ResolutionHook $hook 124 | * 125 | * @return $this 126 | */ 127 | public function resolvedAt(ResolutionHook $hook): static; 128 | 129 | /** 130 | * Get the used identity resolver 131 | * 132 | * @return \Sprout\Contracts\IdentityResolver|null 133 | */ 134 | public function resolver(): ?IdentityResolver; 135 | 136 | /** 137 | * Get the hook where the tenant was resolved 138 | * 139 | * @return \Sprout\Support\ResolutionHook|null 140 | */ 141 | public function hook(): ?ResolutionHook; 142 | 143 | /** 144 | * Check if the current tenant was resolved 145 | * 146 | * @return bool 147 | * 148 | * @phpstan-assert-if-true \Sprout\Contracts\IdentityResolver $this->resolver() 149 | * @phpstan-assert-if-false null $this->resolver() 150 | */ 151 | public function wasResolved(): bool; 152 | 153 | /** 154 | * Set the current tenant 155 | * 156 | * @param \Sprout\Contracts\Tenant|null $tenant 157 | * 158 | * @phpstan-param TenantClass|null $tenant 159 | * 160 | * @return static 161 | */ 162 | public function setTenant(?Tenant $tenant): static; 163 | 164 | /** 165 | * Get all tenant options 166 | * 167 | * @return list 168 | */ 169 | public function options(): array; 170 | 171 | /** 172 | * Check if a tenancy has an option 173 | * 174 | * @param string $option 175 | * 176 | * @return bool 177 | */ 178 | public function hasOption(string $option): bool; 179 | 180 | /** 181 | * Check if a tenancy has an option with config 182 | * 183 | * @param string $option 184 | * 185 | * @return bool 186 | */ 187 | public function hasOptionConfig(string $option): bool; 188 | 189 | /** 190 | * Add an option to the tenancy 191 | * 192 | * @param string $option 193 | * 194 | * @return static 195 | */ 196 | public function addOption(string $option): static; 197 | 198 | /** 199 | * Remove an option from the tenancy 200 | * 201 | * @param string $option 202 | * 203 | * @return static 204 | */ 205 | public function removeOption(string $option): static; 206 | 207 | /** 208 | * Get a tenancy options config 209 | * 210 | * @param string $option 211 | * 212 | * @return array|null 213 | */ 214 | public function optionConfig(string $option): ?array; 215 | } 216 | --------------------------------------------------------------------------------