├── Pipeline ├── Events │ ├── Fired.php │ ├── Resolved.php │ ├── Processing.php │ ├── Resolving.php │ └── Event.php ├── Step.php ├── Contracts │ └── Step.php ├── Steps.php └── Pipeline.php ├── Tenant └── Events │ ├── Created.php │ ├── Deleted.php │ ├── Updated.php │ └── Event.php ├── readme.md ├── Contracts ├── AffectsApp.php └── LifecycleHook.php ├── Support ├── HooksProvider.php ├── AffectsProvider.php ├── Contracts │ └── ProvidesPassword.php ├── PasswordGenerator.php ├── Concerns │ └── PublishesConfigs.php ├── Provider.php └── DriverProvider.php ├── Concerns ├── DispatchesEvents.php └── ResolvesTenants.php ├── Lifecycle ├── Contracts │ └── ResolvesHooks.php ├── Hook.php ├── ConfigurableHook.php └── HookResolver.php ├── Affects ├── Contracts │ └── ResolvesAffects.php ├── Affect.php └── AffectResolver.php ├── Providers ├── Provides │ ├── ProvidesBindings.php │ ├── ProvidesListeners.php │ ├── ProvidesHooks.php │ ├── ProvidesAffects.php │ └── ProvidesConfigs.php ├── TenantProvider.php └── TenancyProvider.php ├── Identification ├── Events │ ├── Identified.php │ ├── Resolved.php │ ├── Switched.php │ ├── NothingIdentified.php │ ├── Configuring.php │ └── Resolving.php ├── Contracts │ ├── Tenant.php │ └── ResolvesTenants.php ├── Support │ └── TenantModelCollection.php ├── Concerns │ └── AllowsTenantIdentification.php └── TenantResolver.php ├── license.md ├── Facades └── Tenancy.php ├── composer.json └── Environment.php /Pipeline/Events/Fired.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Affects/Contracts/ResolvesAffects.php: -------------------------------------------------------------------------------- 1 | singletons as $contract => $singleton) { 24 | $this->app->singleton($contract, $singleton); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Affects/Affect.php: -------------------------------------------------------------------------------- 1 | event instanceof Switched; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Identification/Events/Identified.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identification/Events/Resolved.php: -------------------------------------------------------------------------------- 1 | tenant = &$tenant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identification/Events/Switched.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identification/Events/NothingIdentified.php: -------------------------------------------------------------------------------- 1 | tenant = &$tenant; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identification/Events/Configuring.php: -------------------------------------------------------------------------------- 1 | resolver = &$resolver; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identification/Events/Resolving.php: -------------------------------------------------------------------------------- 1 | models = $models; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Providers/Provides/ProvidesListeners.php: -------------------------------------------------------------------------------- 1 | listen as $event => $listeners) { 26 | foreach ($listeners as $listener) { 27 | Event::listen($event, $listener); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Pipeline/Events/Resolved.php: -------------------------------------------------------------------------------- 1 | steps = &$steps; 28 | 29 | return $this; 30 | } 31 | 32 | public function __call($name, $arguments) 33 | { 34 | return $this->steps->{$name}($arguments); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pipeline/Events/Processing.php: -------------------------------------------------------------------------------- 1 | steps = &$steps; 28 | 29 | return $this; 30 | } 31 | 32 | public function __call($name, $arguments) 33 | { 34 | return $this->steps->{$name}($arguments); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Providers/Provides/ProvidesHooks.php: -------------------------------------------------------------------------------- 1 | app->resolving(ResolvesHooks::class, function (ResolvesHooks $resolver) { 26 | foreach ($this->hooks as $hook) { 27 | $resolver->addHook($hook); 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Providers/Provides/ProvidesAffects.php: -------------------------------------------------------------------------------- 1 | app->resolving(ResolvesAffects::class, function (ResolvesAffects $resolver) { 26 | foreach ($this->affects as $affect) { 27 | $resolver->addAffect($affect); 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Support/PasswordGenerator.php: -------------------------------------------------------------------------------- 1 | getTenantIdentifier(), 29 | $tenant->getTenantKey(), 30 | config('tenancy.key') ?? config('app.key') 31 | )); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Pipeline/Events/Resolving.php: -------------------------------------------------------------------------------- 1 | step = &$step; 28 | 29 | return $this; 30 | } 31 | 32 | public function replace(Step $with) 33 | { 34 | $this->step = $with; 35 | 36 | return $this; 37 | } 38 | 39 | public function remove() 40 | { 41 | $this->step = null; 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Providers/Provides/ProvidesConfigs.php: -------------------------------------------------------------------------------- 1 | configs as $config) { 24 | $configPath = basename($config); 25 | $configName = basename($config, '.php'); 26 | 27 | $this->publishes([$config => config_path('tenancy'.DIRECTORY_SEPARATOR.$configPath)], [$configName, 'tenancy']); 28 | 29 | $this->mergeConfigFrom($config, 'tenancy.'.$configName); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Pipeline/Step.php: -------------------------------------------------------------------------------- 1 | event = $event; 32 | 33 | return $this; 34 | } 35 | 36 | public function priority(): int 37 | { 38 | return $this->priority; 39 | } 40 | 41 | public function fires(): bool 42 | { 43 | return $this->fires; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Lifecycle/Hook.php: -------------------------------------------------------------------------------- 1 | queued; 31 | } 32 | 33 | public function queue(): ?string 34 | { 35 | return $this->queue; 36 | } 37 | 38 | public function fires(): bool 39 | { 40 | return $this->event instanceof Event && parent::fires(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Pipeline/Events/Event.php: -------------------------------------------------------------------------------- 1 | event = $event; 33 | $this->pipeline = $pipeline; 34 | } 35 | 36 | public function isForPipeline($pipeline): bool 37 | { 38 | $pipeline = is_string($pipeline) ? $pipeline : get_class($pipeline); 39 | 40 | return get_class($this->pipeline) === $pipeline; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Identification/Contracts/Tenant.php: -------------------------------------------------------------------------------- 1 | filter(function (string $item) use ($contracts) { 34 | $implements = class_implements($item); 35 | 36 | return count(array_intersect($implements, $contracts)); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Support/Concerns/PublishesConfigs.php: -------------------------------------------------------------------------------- 1 | configs as $config) { 31 | $configPath = basename($config); 32 | $configName = basename($config, '.php'); 33 | 34 | $this->publishes([$config => config_path('tenancy'.DIRECTORY_SEPARATOR.$configPath)], [$configName, 'tenancy']); 35 | 36 | $this->mergeConfigFrom($config, 'tenancy.'.$configName); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Support/Provider.php: -------------------------------------------------------------------------------- 1 | runTrait('register'); 26 | } 27 | 28 | public function boot() 29 | { 30 | $this->runTrait('boot'); 31 | } 32 | 33 | protected function runTrait(string $runtime) 34 | { 35 | $class = static::class; 36 | 37 | foreach (class_uses_recursive($class) as $trait) { 38 | if (method_exists($class, $method = $runtime.class_basename($trait))) { 39 | call_user_func([$this, $method]); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Pipeline/Contracts/Step.php: -------------------------------------------------------------------------------- 1 | app->resolving(ResolvesTenants::class, function (ResolvesTenants $resolver) { 37 | foreach ($this->drivers as $contract) { 38 | $resolver->registerDriver($contract); 39 | } 40 | }); 41 | 42 | $this->publishConfigs(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Lifecycle/ConfigurableHook.php: -------------------------------------------------------------------------------- 1 | queue !== false; 35 | } 36 | 37 | public function queue(): ?string 38 | { 39 | return $this->queue; 40 | } 41 | 42 | public function priority(): int 43 | { 44 | return $this->priority; 45 | } 46 | 47 | public function fires(): bool 48 | { 49 | return $this->fires; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Tenancy for Laravel & Arlon Antonius 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 | -------------------------------------------------------------------------------- /Providers/TenantProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(Tenant::class, function () { 29 | /** @var Environment $env */ 30 | $env = resolve(Environment::class); 31 | 32 | if ($env->isIdentified()) { 33 | return $env->getTenant(); 34 | } 35 | 36 | return $env->identifyTenant(); 37 | }); 38 | } 39 | 40 | public function provides() 41 | { 42 | return [ 43 | Tenant::class, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Affects/AffectResolver.php: -------------------------------------------------------------------------------- 1 | steps->add($affect); 33 | 34 | return $this; 35 | } 36 | 37 | public function getAffects(): array 38 | { 39 | return $this->getSteps()->toArray(); 40 | } 41 | 42 | public function setAffects(array $affects) 43 | { 44 | $this->setSteps($affects); 45 | 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Facades/Tenancy.php: -------------------------------------------------------------------------------- 1 | map(function ($step) use ($event, $pipeline) { 27 | /** @var Step $hook */ 28 | $step = is_string($step) ? resolve($step) : $step; 29 | 30 | $step = $step->for($event); 31 | 32 | event((new Events\Resolving($event, $pipeline))->step($step)); 33 | 34 | return $step; 35 | }) 36 | ->filter(); 37 | } 38 | 39 | public function prioritized() 40 | { 41 | return $this->sortBy(function (Step $step) { 42 | return $step->priority(); 43 | }); 44 | } 45 | 46 | public function fires() 47 | { 48 | return $this->filter(function (Step $step) { 49 | return $step->fires(); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Identification/Concerns/AllowsTenantIdentification.php: -------------------------------------------------------------------------------- 1 | getKeyName(); 29 | } 30 | 31 | /** 32 | * The actual value of the key for the tenant Model. 33 | * 34 | * @return string|int 35 | */ 36 | public function getTenantKey() 37 | { 38 | return $this->getKey(); 39 | } 40 | 41 | /** 42 | * A unique identifier, eg class or table to distinguish this tenant Model. 43 | * 44 | * @return string 45 | */ 46 | public function getTenantIdentifier(): string 47 | { 48 | $identifier = $this->getTable(); 49 | $connection = $this->getConnectionName() ?? config('database.default'); 50 | 51 | return "$connection.$identifier"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Identification/Contracts/ResolvesTenants.php: -------------------------------------------------------------------------------- 1 | steps = $steps ?? new Steps(); 28 | } 29 | 30 | public function getSteps(): Steps 31 | { 32 | return $this->steps; 33 | } 34 | 35 | /** 36 | * @param array|Step[] $steps 37 | * 38 | * @return Pipeline 39 | */ 40 | public function setSteps(array $steps): self 41 | { 42 | $this->steps = new Steps($steps); 43 | 44 | return $this; 45 | } 46 | 47 | public function handle($event, callable $fire = null) 48 | { 49 | $steps = $this->steps; 50 | 51 | event((new Events\Processing($event, $this))->steps($steps)); 52 | 53 | $steps = $steps 54 | ->resolve($event, $this) 55 | ->prioritized() 56 | ->fires(); 57 | 58 | event((new Events\Resolved($event, $this))->steps($steps)); 59 | 60 | if ($fire) { 61 | $fire($steps); 62 | } else { 63 | $steps->each(function (Step $step) { 64 | $step->fire(); 65 | }); 66 | } 67 | 68 | event(new Events\Fired($event, $this)); 69 | 70 | return $steps; 71 | } 72 | 73 | public function __call($name, $arguments) 74 | { 75 | return $this->steps->{$name}($arguments); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Lifecycle/HookResolver.php: -------------------------------------------------------------------------------- 1 | steps->add($hook); 33 | 34 | return $this; 35 | } 36 | 37 | public function getHooks(): array 38 | { 39 | return $this->getSteps()->toArray(); 40 | } 41 | 42 | public function setHooks(array $hooks) 43 | { 44 | $this->setSteps($hooks); 45 | 46 | return $this; 47 | } 48 | 49 | public function handle($event, callable $fire = null) 50 | { 51 | parent::handle($event, function ($hooks) { 52 | $hooks->each(function (LifecycleHook $hook) { 53 | if ($hook->queued()) { 54 | dispatch(function () use ($hook) { 55 | // @codeCoverageIgnoreStart 56 | $hook->fire(); 57 | // @codeCoverageIgnoreEnd 58 | })->onQueue($hook->queue()); 59 | } else { 60 | $hook->fire(); 61 | } 62 | }); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Environment.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 44 | 45 | $this->events()->dispatch(new Switched($tenant)); 46 | 47 | if (!$this->identified) { 48 | $this->identified = true; 49 | } 50 | 51 | return $this; 52 | } 53 | 54 | public function getTenant(): ?Tenant 55 | { 56 | return $this->tenant; 57 | } 58 | 59 | public function identifyTenant(bool $refresh = false, string $contract = null): ?Tenant 60 | { 61 | if (!$this->identified || $refresh) { 62 | $resolver = $this->tenantResolver(); 63 | 64 | $this->setTenant($resolver($contract)); 65 | } 66 | 67 | return $this->getTenant(); 68 | } 69 | 70 | public function isIdentified(): bool 71 | { 72 | return $this->identified; 73 | } 74 | 75 | public function setIdentified(bool $identified) 76 | { 77 | $this->identified = $identified; 78 | 79 | return $this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Providers/TenancyProvider.php: -------------------------------------------------------------------------------- 1 | Environment::class, 39 | ResolvesHooks::class => HookResolver::class, 40 | ResolvesAffects::class => AffectResolver::class, 41 | ResolvesTenants::class => TenantResolver::class, 42 | ProvidesPassword::class => PasswordGenerator::class, 43 | ]; 44 | 45 | protected $listen = [ 46 | Event\Created::class => [ 47 | ResolvesHooks::class, 48 | ], 49 | Event\Updated::class => [ 50 | ResolvesHooks::class, 51 | ], 52 | Event\Deleted::class => [ 53 | ResolvesHooks::class, 54 | ], 55 | Switched::class => [ 56 | ResolvesAffects::class, 57 | ], 58 | ]; 59 | 60 | public function register() 61 | { 62 | $this->runTrait('register'); 63 | 64 | $this->app->register(TenantProvider::class); 65 | } 66 | 67 | public function boot() 68 | { 69 | $this->runTrait('boot'); 70 | } 71 | 72 | protected function runTrait(string $runtime) 73 | { 74 | $class = static::class; 75 | 76 | foreach (class_uses_recursive($class) as $trait) { 77 | if (method_exists($class, $method = $runtime.class_basename($trait))) { 78 | call_user_func([$this, $method]); 79 | } 80 | } 81 | } 82 | 83 | public function provides() 84 | { 85 | return [ 86 | Environment::class, 87 | ResolvesTenants::class, 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Identification/TenantResolver.php: -------------------------------------------------------------------------------- 1 | models = new TenantModelCollection(); 46 | 47 | $this->configure(); 48 | } 49 | 50 | public function __invoke(string $contract = null): ?Tenant 51 | { 52 | /** @var Tenant|null $tenant */ 53 | $tenant = $this->events()->until(new Events\Resolving($models = $this->getModels())); 54 | 55 | if (!$tenant && !is_null($contract)) { 56 | return $this->identifyByContract($contract); 57 | } 58 | 59 | if (!$tenant && count($this->drivers) > 0) { 60 | $tenant = $this->resolveFromDrivers($models); 61 | } 62 | 63 | if ($tenant) { 64 | $this->events()->dispatch(new Events\Identified($tenant)); 65 | } 66 | 67 | // Provide a debug log entry when no tenant was identified, possibly because no identification driver is active. 68 | if (!$tenant && count($this->drivers) === 0) { 69 | logger('No tenant was identified, a possible cause being that no identification drivers are available.'); 70 | } 71 | 72 | if (!$tenant) { 73 | $this->events()->dispatch(new Events\NothingIdentified($tenant)); 74 | } 75 | 76 | $this->events()->dispatch(new Events\Resolved($tenant)); 77 | 78 | return $tenant; 79 | } 80 | 81 | protected function configure() 82 | { 83 | $this->events()->dispatch(new Events\Configuring($this)); 84 | } 85 | 86 | public function addModel(string $class) 87 | { 88 | if (!in_array(Tenant::class, class_implements($class))) { 89 | throw new InvalidArgumentException("$class has to implement ".Tenant::class); 90 | } 91 | 92 | $this->models->push($class); 93 | 94 | return $this; 95 | } 96 | 97 | public function getModels(): TenantModelCollection 98 | { 99 | return $this->models; 100 | } 101 | 102 | public function findModel(string $identifier, $key = null) 103 | { 104 | $model = $this->getModels()->map(function (string $model) { 105 | return new $model(); 106 | })->first(function (Tenant $model) use ($identifier) { 107 | return $model->getTenantIdentifier() === $identifier; 108 | }); 109 | 110 | if ($key !== null && $model) { 111 | return $model->where($model->getTenantKeyName(), $key)->first(); 112 | } 113 | 114 | return $model; 115 | } 116 | 117 | /** 118 | * Updates the tenant model collection. 119 | * 120 | * @param TenantModelCollection $collection 121 | * 122 | * @return $this 123 | */ 124 | public function setModels(TenantModelCollection $collection) 125 | { 126 | $this->models = $collection; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @param string $contract 133 | * 134 | * @return $this 135 | */ 136 | public function registerDriver(string $contract) 137 | { 138 | $this->drivers[] = $contract; 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * @param TenantModelCollection $models 145 | * 146 | * @return Tenant 147 | */ 148 | protected function resolveFromDrivers(TenantModelCollection $models): ?Tenant 149 | { 150 | $tenant = null; 151 | 152 | $models 153 | ->filterByContract($this->drivers) 154 | ->each(function (string $item) use (&$tenant) { 155 | $implements = class_implements($item); 156 | $drivers = array_intersect($implements, $this->drivers); 157 | 158 | foreach ($drivers as $driver) { 159 | /** @var ReflectionMethod $method */ 160 | foreach ($this->retrieveDriverMethods($driver) as $method) { 161 | try { 162 | $tenant = app()->call("$item@{$method->getName()}"); 163 | // @codeCoverageIgnoreStart 164 | } catch (BindingResolutionException $e) { 165 | // @codeCoverageIgnoreEnd 166 | // Prevent trying to find a tenant when bindings aren't working for them. 167 | } 168 | 169 | if ($tenant) { 170 | return false; 171 | } 172 | } 173 | } 174 | }); 175 | 176 | return $tenant; 177 | } 178 | 179 | /** 180 | * @param string $driver 181 | * 182 | * @return array|ReflectionMethod[] 183 | */ 184 | protected function retrieveDriverMethods(string $driver): array 185 | { 186 | return (new ReflectionClass($driver))->getMethods(ReflectionMethod::IS_PUBLIC); 187 | } 188 | 189 | protected function identifyByContract(string $contract) 190 | { 191 | // Provide a debug log entry when no the specific identification driver has not been installed. 192 | if (!in_array($contract, $this->drivers)) { 193 | logger('Identification driver '.$contract.' was not available'); 194 | 195 | return; 196 | } 197 | 198 | $tenant = $this->resolveFromDriver($this->getModels(), $contract); 199 | 200 | if ($tenant) { 201 | $this->events()->dispatch(new Events\Identified($tenant)); 202 | } 203 | 204 | if (!$tenant) { 205 | $this->events()->dispatch(new Events\NothingIdentified($tenant)); 206 | } 207 | 208 | $this->events()->dispatch(new Events\Resolved($tenant)); 209 | 210 | return $tenant; 211 | } 212 | 213 | /** 214 | * @param TenantModelCollection $models 215 | * @param string|array $contract 216 | * 217 | * @return Tenant|null 218 | */ 219 | protected function resolveFromDriver(TenantModelCollection $models, string $contract): ?Tenant 220 | { 221 | $tenant = null; 222 | 223 | $methods = $this->retrieveDriverMethods($contract); 224 | 225 | $models 226 | ->filterByContract($contract) 227 | ->each(function (string $item) use (&$tenant, $methods) { 228 | foreach ($methods as $method) { 229 | try { 230 | $tenant = app()->call("$item@{$method->getName()}"); 231 | // @codeCoverageIgnoreStart 232 | } catch (BindingResolutionException $e) { 233 | // @codeCoverageIgnoreEnd 234 | // Prevent trying to find a tenant when bindings aren't working for them. 235 | } 236 | 237 | if ($tenant) { 238 | return false; 239 | } 240 | } 241 | }); 242 | 243 | return $tenant; 244 | } 245 | } 246 | --------------------------------------------------------------------------------