├── .github └── workflows │ └── tests.yml ├── LICENSE ├── composer.json ├── config └── tenancy.php └── src ├── Console ├── AbstractTenantCommand.php ├── TenantListCommand.php ├── TenantRouteCacheCommand.php ├── TenantRouteClearCommand.php ├── TenantRouteListCommand.php └── stubs │ └── routes.stub ├── Contracts ├── BelongsToTenant.php ├── BelongsToTenantParticipant.php ├── BelongsToTenantParticipants.php ├── DomainAwareTenantParticipant.php ├── DomainAwareTenantParticipantRepository.php ├── SecurityModel.php ├── Tenant.php ├── TenantAware.php ├── TenantParticipant.php └── TenantParticipantRepository.php ├── Entities ├── Concerns │ ├── BelongsToTenant.php │ ├── DomainAwareTenantParticipant.php │ ├── TenantAware.php │ └── TenantParticipant.php ├── NullTenant.php ├── NullUser.php ├── SecurityModel.php └── Tenant.php ├── Foundation └── TenantAwareApplication.php ├── Http ├── Controller │ └── TenantController.php ├── Middleware │ ├── AuthenticateTenant.php │ ├── EnsureTenantType.php │ ├── TenantRouteResolver.php │ └── TenantSiteResolver.php └── TenantRedirectorService.php ├── Repositories ├── DomainAwareTenantParticipantRepository.php ├── TenantAwareRepository.php └── TenantParticipantRepository.php ├── Services └── TenantTypeResolver.php ├── TenancyServiceProvider.php └── Twig └── TenantExtension.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | 6 | create: 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate 19 | 20 | - name: Install dependencies 21 | run: composer install --prefer-dist --no-progress --no-suggest 22 | 23 | - name: Run test suite 24 | run: vendor/bin/phpunit 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Redfern 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. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somnambulist/laravel-doctrine-tenancy", 3 | "type": "library", 4 | "description": "A multi-tenancy implementation that uses Laravel and Doctrine.", 5 | "keywords": [ 6 | "doctrine", 7 | "laravel", 8 | "laravel-doctrine", 9 | "tenancy", 10 | "multi-tenancy" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Dave Redfern", 16 | "email": "info@somnambulist.tech" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "Somnambulist\\Tenancy\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Somnambulist\\Tenancy\\Tests\\": "tests/" 27 | } 28 | }, 29 | "require": { 30 | "php": ">=7.3", 31 | "eloquent/enumeration": "^6.0", 32 | "illuminate/routing": "^7.0|^8.0" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "^9.5", 36 | "doctrine/orm": "^2.7", 37 | "laravel/framework": "^7.0|^8.0", 38 | "symfony/var-dumper": "^5.1", 39 | "rcrowe/twigbridge": "^0.12" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/tenancy.php: -------------------------------------------------------------------------------- 1 | /app/route/here. A tenant can have multiple 10 | * sub-accounts if desired. The participant must be configured and must implement 11 | * the interfaces: 12 | * 13 | * * TenantParticipant 14 | * * TenantParticipantRepository 15 | * 16 | * Eloquent models should work, provided a repository class is created first. 17 | */ 18 | 'multi_account' => [ 19 | 'enabled' => false, 20 | 21 | /* 22 | * Participant class and repository must be a valid Entity + Repository class. 23 | * The Doctrine EntityRepository class should be fine if using Doctrine. 24 | * 25 | * It is recommended to use the ::class property to allow auto-updating the class 26 | * names when using IDE refactoring tools. 27 | * 28 | * Mappings 29 | * 30 | * Mappings allow a short alias to be used for the participant class e.g. for use 31 | * with EnsureTenantType route middleware in routes. 32 | * 33 | * If you are using table inheritance, then you must provide mappings for 34 | * each class type - NOT the abstract base class. The aliases can be 35 | * whatever you like. It is suggested to use: 36 | * 37 | * short name (for routing) 38 | * doctrine discriminator name (for consistency with Doctrine) 39 | * class name (for class look ups) 40 | * 41 | * Format is: 42 | * // alias => fully qualified class name 43 | * 'alias' => \App\Entity\MyClass::class, 44 | */ 45 | 'participant' => [ 46 | 'class' => '', 47 | 'repository' => '', 48 | 'mappings' => [ 49 | // 'my_alias' => Some\Class\Name::class, 50 | ], 51 | ], 52 | ], 53 | 54 | /* 55 | * Multi-Site Tenancy 56 | * 57 | * Multi-Site has a separate configuration as it can run at the same time as 58 | * multi-account. It has a similar set of options to multi-account and requires 59 | * that the tenant participant implement: 60 | * 61 | * * DomainAwareTenantParticipant 62 | * * DomainAwareTenantParticipantRepository 63 | */ 64 | 'multi_site' => [ 65 | 'enabled' => false, 66 | 67 | /* 68 | * For multi-site tenancy i.e. by domain name, specify the 69 | * DomainAwareTenantParticipantRepository here. This will be used to find tenants 70 | * by the domain name. The entity class is also required. 71 | * 72 | * Note: that the interface extends TenantParticipant so the same details can be 73 | * used here as in the previous participant settings. 74 | */ 75 | 'participant' => [ 76 | 'class' => '', 77 | 'repository' => '', 78 | 'mappings' => [ 79 | // 'my_alias' => Some\Class\Name::class, 80 | ], 81 | ], 82 | 83 | /* 84 | * An array of domain chunks that should be removed from the host name before 85 | * performing a domain aware tenant participant look-up. 86 | * 87 | * For example: your main domain is www.example.com but in testing and dev it 88 | * is prefixed with dev. and test. this array can contain these (and www.) and 89 | * the host will always be resolved to example.com. 90 | * 91 | * Leave this empty to have the domain look up always check the full domain. 92 | */ 93 | 'ignorable_domain_components' => [ 94 | 'dev.', 95 | 'test.', 96 | ], 97 | 98 | /* 99 | * Register your router settings for multi-site. The namespace and patterns will 100 | * be added to the router when the TenantRouteResolver middleware is triggered. 101 | */ 102 | 'router' => [ 103 | 'namespace' => 'App\Http\Controllers', 104 | 'patterns' => [ 105 | // 'id' => '[0-9]+', 106 | ], 107 | ], 108 | ], 109 | 110 | /* 111 | * Register Doctrine repositories 112 | * 113 | * Add any repositories that need to be added to the container with the 114 | * tenant bound. This should be the Tenant version, the base repository 115 | * being wrapped, and then an alias / any tags (optional). 116 | * 117 | * Each Tenanted repository should extend TenantAwareRepository and ensure 118 | * that any custom security models have been defined in the repository 119 | * methods. 120 | * 121 | * Generally you should extend the TenantAwareRepository to a base class that 122 | * can add / override the security model methods. 123 | * 124 | * Format is: 125 | * 126 | * [ 127 | * 'repository' => '\App\Repo\Tenant\MyRepo', 128 | * 'base' => '\App\Repo\MyRepo', 129 | * 'alias' => 'tenant.my_repo', 130 | * 'tags' => ['tenant_aware'] 131 | * ] 132 | */ 133 | 'doctrine' => [ 134 | 'repositories' => [], 135 | ], 136 | ]; 137 | -------------------------------------------------------------------------------- /src/Console/AbstractTenantCommand.php: -------------------------------------------------------------------------------- 1 | router = $router; 54 | $this->repository = $repository; 55 | $this->resolver = $resolver; 56 | } 57 | 58 | /** 59 | * @param string $domain 60 | * 61 | * @return RouteCollection|null 62 | */ 63 | protected function resolveTenantRoutes($domain) 64 | { 65 | if (null === $dt = $this->repository->findOneByDomain($domain)) { 66 | $this->error( 67 | sprintf('No tenant found for "%s", are you sure you entered the correct domain?', $domain) 68 | ); 69 | 70 | return null; 71 | } 72 | 73 | /** @var Tenant $tenant */ 74 | $tenant = app('auth.tenant'); 75 | $tenant->updateTenancy(new NullUser(), $dt->getTenantOwner(), $dt); 76 | 77 | $this->resolver->boot(); 78 | 79 | return $this->routes = $this->router->getRoutes(); 80 | } 81 | 82 | /** 83 | * Get the console command arguments 84 | * 85 | * @return array 86 | */ 87 | protected function getArguments() 88 | { 89 | return [ 90 | ['domain', InputArgument::REQUIRED, 'The tenant domain to display route information for.'], 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Console/TenantListCommand.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 49 | } 50 | 51 | public function handle() 52 | { 53 | $data = []; 54 | 55 | /** @var DomainAwareTenantParticipant $tenant */ 56 | foreach ($this->repository->findBy([], ['name' => 'ASC']) as $tenant) { 57 | $data[] = [ 58 | 'id' => $tenant->getId(), 59 | 'name' => $tenant->getName(), 60 | 'domain' => $tenant->getDomain(), 61 | 'owner' => $tenant->getTenantOwner()->getName(), 62 | 'model' => $tenant->getSecurityModel(), 63 | ]; 64 | } 65 | 66 | $this->table($this->headers, $data); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Console/TenantRouteCacheCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 49 | } 50 | 51 | public function handle() 52 | { 53 | $domain = $this->argument('domain'); 54 | $this->call('tenant:route:clear', ['domain' => $domain]); 55 | 56 | $routes = $this->getFreshApplicationRoutes($domain); 57 | 58 | if (count($routes) == 0) { 59 | $this->error("The specified tenant does not have any routes."); 60 | 61 | return; 62 | } 63 | 64 | foreach ($routes as $route) { 65 | $route->prepareForSerialization(); 66 | } 67 | 68 | $this->files->put( 69 | $this->laravel->getCachedRoutesPath(), 70 | $this->buildRouteCacheFile($routes) 71 | ); 72 | 73 | $this->info('Tenant routes cached successfully!'); 74 | } 75 | 76 | protected function getFreshApplicationRoutes($domain): AbstractRouteCollection 77 | { 78 | return $this->resolveTenantRoutes($domain); 79 | } 80 | 81 | protected function buildRouteCacheFile(AbstractRouteCollection $routes): string 82 | { 83 | $stub = $this->files->get(__DIR__ . '/stubs/routes.stub'); 84 | 85 | return str_replace('{{routes}}', var_export($routes->compile(), true), $stub); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Console/TenantRouteClearCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | * 46 | * @return void 47 | */ 48 | public function handle() 49 | { 50 | $this->resolveTenantRoutes($this->argument('domain')); 51 | 52 | $this->files->delete($this->laravel->getCachedRoutesPath()); 53 | 54 | $this->info('Tenant route cache cleared!'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/TenantRouteListCommand.php: -------------------------------------------------------------------------------- 1 | resolveTenantRoutes($this->argument('domain')))) { 57 | return; 58 | } 59 | 60 | if (!$this->routes instanceof AbstractRouteCollection) { 61 | $this->error("The specified tenant does not have any routes."); 62 | 63 | return; 64 | } 65 | 66 | $this->displayRoutes($this->getRoutes()); 67 | } 68 | 69 | /** 70 | * Compile the routes into a displayable format. 71 | * 72 | * @return array 73 | */ 74 | protected function getRoutes() 75 | { 76 | $routes = collect($this->routes)->map(function ($route) { 77 | return $this->getRouteInformation($route); 78 | })->all(); 79 | 80 | if ($sort = $this->option('sort')) { 81 | $routes = $this->sortRoutes($sort, $routes); 82 | } 83 | 84 | if ($this->option('reverse')) { 85 | $routes = array_reverse($routes); 86 | } 87 | 88 | return array_filter($routes); 89 | } 90 | 91 | /** 92 | * Get the route information for a given route. 93 | * 94 | * @param Route $route 95 | * 96 | * @return array 97 | */ 98 | protected function getRouteInformation(Route $route) 99 | { 100 | return $this->filterRoute([ 101 | 'host' => $route->domain(), 102 | 'method' => implode('|', $route->methods()), 103 | 'uri' => $route->uri(), 104 | 'name' => $route->getName(), 105 | 'action' => $route->getActionName(), 106 | 'middleware' => $this->getMiddleware($route), 107 | ]); 108 | } 109 | 110 | /** 111 | * Sort the routes by a given element. 112 | * 113 | * @param string $sort 114 | * @param array $routes 115 | * 116 | * @return array 117 | */ 118 | protected function sortRoutes($sort, $routes) 119 | { 120 | return Arr::sort($routes, function ($route) use ($sort) { 121 | return $route[$sort]; 122 | }); 123 | } 124 | 125 | /** 126 | * Display the route information on the console. 127 | * 128 | * @param array $routes 129 | * 130 | * @return void 131 | */ 132 | protected function displayRoutes(array $routes) 133 | { 134 | $this->table($this->headers, $routes); 135 | } 136 | 137 | /** 138 | * Get before filters. 139 | * 140 | * @param Route $route 141 | * 142 | * @return string 143 | */ 144 | protected function getMiddleware($route) 145 | { 146 | return collect($route->gatherMiddleware()) 147 | ->map(function ($middleware) { 148 | return $middleware instanceof Closure ? 'Closure' : $middleware; 149 | }) 150 | ->implode(',') 151 | ; 152 | } 153 | 154 | /** 155 | * Filter the route by URI and / or name. 156 | * 157 | * @param array $route 158 | * 159 | * @return array|null 160 | */ 161 | protected function filterRoute(array $route) 162 | { 163 | if ( 164 | ($this->option('name') && !Str::contains($route['name'], $this->option('name'))) || 165 | $this->option('path') && !Str::contains($route['uri'], $this->option('path')) || 166 | $this->option('method') && !Str::contains($route['method'], $this->option('method')) 167 | ) { 168 | return null; 169 | } 170 | 171 | return $route; 172 | } 173 | 174 | /** 175 | * Get the console command options. 176 | * 177 | * @return array 178 | */ 179 | protected function getOptions() 180 | { 181 | return [ 182 | ['method', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by method.'], 183 | ['name', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by name.'], 184 | ['path', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by path.'], 185 | ['reverse', 'r', InputOption::VALUE_NONE, 'Reverse the ordering of the routes.'], 186 | ['sort', null, InputOption::VALUE_OPTIONAL, 'The column (host, method, uri, name, action, middleware) to sort by.', 'uri',], 187 | ]; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Console/stubs/routes.stub: -------------------------------------------------------------------------------- 1 | setCompiledRoutes( 15 | {{routes}} 16 | ); 17 | -------------------------------------------------------------------------------- /src/Contracts/BelongsToTenant.php: -------------------------------------------------------------------------------- 1 | belongsToTenant($t); 29 | 30 | if ($hasTenant && !$requireAll) { 31 | return true; 32 | } elseif (!$hasTenant && $requireAll) { 33 | return false; 34 | } 35 | } 36 | 37 | return $requireAll; 38 | } else { 39 | if ($this instanceof BelongsToTenantParticipant) { 40 | if ( 41 | !is_null($this->getTenantParticipant()) && 42 | $this->getTenantName($tenant) === $this->getTenantParticipant()->getName() 43 | ) { 44 | return true; 45 | } 46 | } 47 | if ($this instanceof BelongsToTenantParticipants) { 48 | foreach ($this->getTenantParticipants() as $t) { 49 | if ($this->getTenantName($tenant) === $t->getName()) { 50 | return true; 51 | } 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | } 58 | 59 | /** 60 | * @param TenantParticipantContract|string $tenant 61 | * 62 | * @return string 63 | */ 64 | protected function getTenantName($tenant) 65 | { 66 | return $tenant instanceof TenantParticipantContract ? $tenant->getName() : $tenant; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Entities/Concerns/DomainAwareTenantParticipant.php: -------------------------------------------------------------------------------- 1 | domain; 27 | } 28 | 29 | /** 30 | * @param string $domain 31 | * 32 | * @return $this 33 | */ 34 | public function setDomain($domain) 35 | { 36 | $this->domain = $domain; 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Entities/Concerns/TenantAware.php: -------------------------------------------------------------------------------- 1 | setTenantOwnerId($tenant->getTenantOwnerId()); 34 | $this->setTenantCreatorId($tenant->getTenantCreatorId()); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @return integer 41 | */ 42 | public function getTenantOwnerId() 43 | { 44 | return $this->tenantOwnerId; 45 | } 46 | 47 | /** 48 | * @param integer $id 49 | * 50 | * @return $this 51 | */ 52 | public function setTenantOwnerId($id) 53 | { 54 | $this->tenantOwnerId = $id; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return integer 61 | */ 62 | public function getTenantCreatorId() 63 | { 64 | return $this->tenantCreatorId; 65 | } 66 | 67 | /** 68 | * @param integer $id 69 | * 70 | * @return $this 71 | */ 72 | public function setTenantCreatorId($id) 73 | { 74 | $this->tenantCreatorId = $id; 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Entities/Concerns/TenantParticipant.php: -------------------------------------------------------------------------------- 1 | value(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Entities/Tenant.php: -------------------------------------------------------------------------------- 1 | updateTenancy($user, $owner, $creator); 36 | } 37 | 38 | /** 39 | * Update the tenant details 40 | * 41 | * @param Authenticatable $user 42 | * @param TenantParticipantContract $owner 43 | * @param TenantParticipantContract $creator 44 | * 45 | * @return $this 46 | * @internal Should not be called normally 47 | */ 48 | public function updateTenancy(Authenticatable $user, TenantParticipantContract $owner, TenantParticipantContract $creator) 49 | { 50 | $this->user = $user; 51 | $this->tenantOwner = $owner; 52 | $this->tenantCreator = $creator; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return integer 59 | */ 60 | public function getTenantOwnerId() 61 | { 62 | return $this->tenantOwner->getId(); 63 | } 64 | 65 | /** 66 | * @return integer 67 | */ 68 | public function getTenantCreatorId() 69 | { 70 | return $this->tenantCreator->getId(); 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | public function getTenantSecurityModel() 77 | { 78 | return $this->tenantOwner->getSecurityModel(); 79 | } 80 | 81 | /** 82 | * @return TenantParticipantContract 83 | */ 84 | public function getTenantOwner() 85 | { 86 | return $this->tenantOwner; 87 | } 88 | 89 | /** 90 | * @return TenantParticipantContract 91 | */ 92 | public function getTenantCreator() 93 | { 94 | return $this->tenantCreator; 95 | } 96 | 97 | /** 98 | * @return Authenticatable 99 | */ 100 | public function getUser() 101 | { 102 | return $this->user; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Foundation/TenantAwareApplication.php: -------------------------------------------------------------------------------- 1 | registerBaseTenantBindings(); 38 | } 39 | 40 | /** 41 | * Register the root Tenant instance 42 | * 43 | * @return void 44 | */ 45 | protected function registerBaseTenantBindings() 46 | { 47 | $this->singleton(TenantContract::class, function ($app) { 48 | return new Tenant(new NullUser(), new NullTenant(), new NullTenant()); 49 | }); 50 | 51 | $this->alias(TenantContract::class, 'auth.tenant'); 52 | } 53 | 54 | /** 55 | * @return boolean 56 | */ 57 | public function isMultiSiteTenant() 58 | { 59 | return ($this['auth.tenant']->getTenantOwner() instanceof DomainAwareTenantParticipant); 60 | } 61 | 62 | /** 63 | * @param string $default 64 | * 65 | * @return string 66 | */ 67 | protected function getTenantCacheName($default) 68 | { 69 | if ($this->isMultiSiteTenant()) { 70 | $creator = $this['auth.tenant']->getTenantCreator()->getDomain(); 71 | $owner = $this['auth.tenant']->getTenantOwner()->getDomain(); 72 | 73 | if ($creator && $creator != $owner) { 74 | return $creator; 75 | } else { 76 | return $owner; 77 | } 78 | } 79 | 80 | return $default; 81 | } 82 | 83 | public function getCachedConfigPath() 84 | { 85 | return $this->basePath() . '/bootstrap/cache/' . $this->getTenantCacheName('config') . '.php'; 86 | } 87 | 88 | public function getCachedRoutesPath() 89 | { 90 | return $this->basePath() . '/bootstrap/cache/' . $this->getTenantCacheName('routes') . '.php'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Http/Controller/TenantController.php: -------------------------------------------------------------------------------- 1 | getSelectTenantView(), ['user' => auth()->user()]); 39 | } 40 | 41 | /** 42 | * Route: tenant.no_tenants 43 | * 44 | * @return Factory|View 45 | */ 46 | public function noTenantsAvailableAction() 47 | { 48 | return view($this->getNoTenantsAvailableView(), ['user' => auth()->user()]); 49 | } 50 | 51 | /** 52 | * Route: tenant.access_denied 53 | * 54 | * @return Factory|View 55 | */ 56 | public function accessDeniedAction() 57 | { 58 | return view($this->getAccessDeniedView(), ['user' => auth()->user()]); 59 | } 60 | 61 | /** 62 | * Route: tenant.invalid_tenant_hierarchy 63 | * 64 | * @return Factory|View 65 | */ 66 | public function invalidHierarchyAction() 67 | { 68 | return view($this->getInvalidHierarchyView(), ['user' => auth()->user()]); 69 | } 70 | 71 | /** 72 | * Route: tenant.tenant_type_not_supported 73 | * 74 | * @param Request $request 75 | * 76 | * @return Factory|View 77 | */ 78 | public function tenantTypeNotSupportedAction(Request $request) 79 | { 80 | return view($this->getTenantTypeNotSupportedView(), [ 81 | 'user' => auth()->user(), 82 | 'type' => $request->get('type', 'Not Defined'), 83 | ]); 84 | } 85 | 86 | 87 | 88 | /** 89 | * View to render when selecting a tenant 90 | * 91 | * @return string 92 | */ 93 | protected function getSelectTenantView() 94 | { 95 | return 'tenant/select_tenant'; 96 | } 97 | 98 | /** 99 | * View to render when the current user has no tenants available 100 | * 101 | * @return string 102 | */ 103 | protected function getNoTenantsAvailableView() 104 | { 105 | return 'tenant/error/no_tenants'; 106 | } 107 | 108 | /** 109 | * View to render when the current user does not have access to the tenant 110 | * 111 | * @return string 112 | */ 113 | protected function getAccessDeniedView() 114 | { 115 | return 'tenant/error/access_denied'; 116 | } 117 | 118 | /** 119 | * View to render when the type of the tenant has been rejected by EnsureTenantType 120 | * 121 | * @return string 122 | */ 123 | protected function getTenantTypeNotSupportedView() 124 | { 125 | return 'tenant/error/type_not_supported'; 126 | } 127 | 128 | /** 129 | * View to render when the User tries to access a tenant with an invalid owner 130 | * 131 | * @return string 132 | */ 133 | protected function getInvalidHierarchyView() 134 | { 135 | return 'tenant/error/invalid_hierarchy'; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Http/Middleware/AuthenticateTenant.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 38 | $this->repository = $repository; 39 | } 40 | 41 | /** 42 | * Handle an incoming request. 43 | * 44 | * @param Request $request 45 | * @param Closure $next 46 | * 47 | * @return mixed 48 | */ 49 | public function handle($request, Closure $next) 50 | { 51 | $owner = $creator = null; 52 | /** @var TenantContract $tenant */ 53 | $tenant = app('auth.tenant'); 54 | 55 | /** @var TenantParticipantContract $owner */ 56 | if (null !== $tenantOwnerId = $request->route('tenant_owner_id')) { 57 | if ($tenant->getTenantOwnerId() && $tenantOwnerId != $tenant->getTenantOwnerId()) { 58 | abort(500, sprintf( 59 | 'Selected tenant_owner_id "%s" in route parameters does not match the resolved owner "%s: %s"', 60 | $tenantOwnerId, $tenant->getTenantOwnerId(), $tenant->getTenantOwner()->getName() 61 | )); 62 | } 63 | 64 | $owner = $this->repository->find($tenantOwnerId); 65 | } 66 | 67 | /** @var TenantParticipantContract $creator */ 68 | if (null !== $tenantCreatorId = $request->route('tenant_creator_id')) { 69 | $creator = $this->repository->find($tenantCreatorId); 70 | } 71 | 72 | /** @var BelongsToTenantContract $user */ 73 | $user = $this->auth->user(); 74 | 75 | if (!$user instanceof BelongsToTenantContract) { 76 | abort(500, sprintf('The Authenticatable User entity does not implement BelongsToTenant contract.')); 77 | } 78 | 79 | if (!$creator || !$user->belongsToTenant($creator)) { 80 | return redirect()->route('tenant.access_denied'); 81 | } 82 | if ($owner && $creator->getTenantOwner() !== $owner) { 83 | return redirect()->route('tenant.invalid_tenant_hierarchy'); 84 | } 85 | 86 | // remove the tenant parameters, TenantAware URL generator has access to Tenant 87 | $request->route()->forgetParameter('tenant_owner_id'); 88 | $request->route()->forgetParameter('tenant_creator_id'); 89 | 90 | $tenant->updateTenancy($user, $creator->getTenantOwner(), $creator); 91 | 92 | // replace route tenant info with the resolved, authenticated users tenant info 93 | app('url')->defaults([ 94 | 'tenant_owner_id' => $tenant->getTenantOwnerId(), 95 | 'tenant_creator_id' => $tenant->getTenantCreatorId(), 96 | ]); 97 | 98 | return $next($request); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Http/Middleware/EnsureTenantType.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 32 | $this->typeResolver = $typeResolver; 33 | } 34 | 35 | /** 36 | * Handle an incoming request. 37 | * 38 | * @param Request $request 39 | * @param Closure $next 40 | * @param string $type 41 | * 42 | * @return mixed 43 | */ 44 | public function handle($request, Closure $next, $type) 45 | { 46 | if (!$this->typeResolver->hasType($this->tenant->getTenantCreator(), $type)) { 47 | return redirect()->route('tenant.tenant_type_not_supported', ['type' => $type]); 48 | } 49 | 50 | return $next($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Http/Middleware/TenantRouteResolver.php: -------------------------------------------------------------------------------- 1 | patterns. 33 | * 34 | * 35 | * [ 40 | * 'router' => [ 41 | * 'namespace' => 'App\Http\Controllers', 42 | * 'patterns' => [ 43 | * 'id' => '[0-9]+, 44 | * ], 45 | * ], 46 | * ], 47 | * 48 | * // other stuff omitted 49 | * ] 50 | * 51 | * 52 | * If the tenant is not a DomainAware participant, the standard routes.php file will be used 53 | * instead of a site specific file. 54 | * 55 | * @package Somnambulist\Tenancy\Http\Middleware 56 | * @subpackage Somnambulist\Tenancy\Http\Middleware\TenantRouteResolver 57 | */ 58 | class TenantRouteResolver extends ServiceProvider 59 | { 60 | 61 | /** 62 | * This namespace is applied to the controller routes in your routes file. 63 | * 64 | * In addition, it is set as the URL generator's root namespace. 65 | * 66 | * @var string 67 | */ 68 | protected $namespace; 69 | 70 | public function __construct(Application $app) 71 | { 72 | parent::__construct($app); 73 | 74 | $this->namespace = $app->make('config')->get('tenancy.multi_site.router.namespace', 'App\Http\Controllers'); 75 | } 76 | 77 | /** 78 | * Handle an incoming request. 79 | * 80 | * @param Request $request 81 | * @param Closure $next 82 | * 83 | * @return mixed 84 | */ 85 | public function handle($request, Closure $next) 86 | { 87 | $this->boot(); 88 | 89 | return $next($request); 90 | } 91 | 92 | public function register() 93 | { 94 | // override the parent register as we need to run on kernel boot instead 95 | } 96 | 97 | public function boot() 98 | { 99 | foreach ($this->app->make('config')->get('tenancy.multi_site.router.patterns', []) as $name => $pattern) { 100 | $this->app->make('router')->pattern($name, $pattern); 101 | } 102 | 103 | $this->setRootControllerNamespace(); 104 | 105 | if ($this->routesAreCached()) { 106 | $this->loadCachedRoutes(); 107 | } else { 108 | $this->loadRoutes(); 109 | 110 | $this->app->booted(function () { 111 | $this->app['router']->getRoutes()->refreshNameLookups(); 112 | $this->app['router']->getRoutes()->refreshActionLookups(); 113 | }); 114 | } 115 | } 116 | 117 | /** 118 | * Ensures that routes are checked in order of creator -> owner -> default. Will check for web and api. 119 | * 120 | * @param Router $router 121 | * 122 | * @return void 123 | */ 124 | public function map(Router $router) 125 | { 126 | /** @var Tenant $tenant */ 127 | $tenant = $this->app->make('auth.tenant'); 128 | $router->group( 129 | ['namespace' => $this->namespace], 130 | function ($router) use ($tenant) { 131 | $tries = ['routes', 'web', 'api',]; 132 | $failures = []; 133 | 134 | if ($tenant->getTenantOwner() instanceof DomainAwareTenantParticipant) { 135 | array_unshift($tries, $tenant->getTenantOwner()->getDomain()); 136 | } 137 | if ($tenant->getTenantCreator() instanceof DomainAwareTenantParticipant) { 138 | array_unshift($tries, $tenant->getTenantCreator()->getDomain()); 139 | } 140 | 141 | $tries = array_unique($tries); 142 | 143 | foreach ($tries as $file) { 144 | foreach (['routes', 'app/Http',] as $folder) { 145 | $path = base_path(sprintf('%s/%s.php', $folder, $file)); 146 | 147 | if (file_exists($path)) { 148 | require $path; 149 | 150 | return; 151 | } 152 | 153 | $failures[] = $path; 154 | } 155 | } 156 | 157 | throw new RuntimeException('No routes found, tried: ' . implode(', ', $failures)); 158 | } 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Http/Middleware/TenantSiteResolver.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 37 | } 38 | 39 | /** 40 | * Handle an incoming request. 41 | * 42 | * @param Request $request 43 | * @param Closure $next 44 | * 45 | * @return mixed 46 | */ 47 | public function handle($request, Closure $next) 48 | { 49 | $config = app('config'); 50 | $view = app('view'); 51 | $domain = str_replace((array)$config->get('tenancy.multi_site.ignorable_domain_components'), '', $request->getHost()); 52 | $tenant = $this->repository->findOneByDomain($domain); 53 | 54 | if (!$tenant instanceof TenantParticipant) { 55 | throw new RuntimeException( 56 | sprintf('Unable to resolve host "%s" to valid TenantParticipant.', $domain) 57 | ); 58 | } 59 | 60 | // update app config 61 | $config->set('app.url', $request->getHost()); 62 | $config->set('view.paths', array_merge((array)$config->get('view.paths'), $this->registerPathsInFinder($view, $tenant))); 63 | 64 | // bind resolved tenant data to container & set route defaults 65 | app('auth.tenant')->updateTenancy(new NullUser(), $tenant->getTenantOwner(), $tenant); 66 | app('url')->defaults([ 67 | 'tenant_owner_id' => app('auth.tenant')->getTenantOwnerId(), 68 | 'tenant_creator_id' => app('auth.tenant')->getTenantCreatorId(), 69 | ]); 70 | 71 | return $next($request); 72 | } 73 | 74 | /** 75 | * Registers the view paths by creating a new array and injecting a new FileViewFinder 76 | * 77 | * @param Factory $view 78 | * @param TenantParticipant $tenant 79 | * 80 | * @return array 81 | */ 82 | protected function registerPathsInFinder(Factory $view, TenantParticipant $tenant) 83 | { 84 | $tenantViewPaths = [ 85 | realpath(base_path('resources/views/' . $tenant->getDomain())), 86 | realpath(base_path('resources/views/' . $tenant->getTenantOwner()->getDomain())), 87 | ]; 88 | 89 | $paths = array_values(array_unique(array_merge(array_filter($tenantViewPaths), $view->getFinder()->getPaths()))); 90 | 91 | $view->getFinder()->setPaths($paths); 92 | 93 | return $paths; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/TenantRedirectorService.php: -------------------------------------------------------------------------------- 1 | route('tenant.index', [ 32 | 'tenant_owner_id' => $user->getTenantParticipant()->getTenantOwner()->getId(), 33 | 'tenant_creator_id' => $user->getTenantParticipant()->getId(), 34 | ]); 35 | } 36 | 37 | if ($user instanceof ParticipantsContract) { 38 | switch ($user->getTenantParticipants()->count()) { 39 | case 0: 40 | $response = redirect()->route('tenant.no_tenants'); 41 | break; 42 | 43 | case 1: 44 | $response = redirect()->route('tenant.index', [ 45 | 'tenant_owner_id' => $user->getTenantParticipants()->first()->getTenantOwner()->getId(), 46 | 'tenant_creator_id' => $user->getTenantParticipants()->first()->getId(), 47 | ]); 48 | break; 49 | 50 | default: 51 | $response = redirect()->route('tenant.select_tenant'); 52 | } 53 | 54 | return $response; 55 | } 56 | 57 | throw new InvalidArgumentException( 58 | sprintf('Supplied Authenticatable User instance does not implement tenancy') 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Repositories/DomainAwareTenantParticipantRepository.php: -------------------------------------------------------------------------------- 1 | repository->findOneByDomain($domain); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Repositories/TenantAwareRepository.php: -------------------------------------------------------------------------------- 1 | em = $em; 62 | $this->repository = $repository; 63 | $this->tenant = $tenant; 64 | $repoClass = $this->repository->getClassName(); 65 | 66 | if (!in_array(TenantAwareContract::class, class_implements($repoClass))) { 67 | throw new RuntimeException( 68 | sprintf('Class "%s" does not implement "%s"', $repoClass, TenantAwareContract::class) 69 | ); 70 | } 71 | } 72 | 73 | /** 74 | * For consistency, allow standard calls, but fail for everything 75 | * 76 | * @param $method 77 | * @param $arguments 78 | * 79 | * @throws ORMException 80 | */ 81 | public function __call($method, $arguments) 82 | { 83 | throw ORMException::invalidFindByCall($this->getClassName(), '*', $method); 84 | } 85 | 86 | /** 87 | * Creates a new QueryBuilder instance that is prepopulated for this entity name. 88 | * 89 | * @param string $alias 90 | * @param string $indexBy The index for the from. 91 | * 92 | * @return QueryBuilder 93 | */ 94 | public function createQueryBuilder($alias, $indexBy = null) 95 | { 96 | $qb = $this->repository->createQueryBuilder($alias, $indexBy); 97 | 98 | $this->applySecurityModel($qb, $alias); 99 | 100 | return $qb; 101 | } 102 | 103 | /** 104 | * Applies the rules for the selected Security Model 105 | * 106 | * The security model name is capitalised, and then turned into a method prefixed with 107 | * apply and suffixed with SecurityModel e.g.: shared -> applySharedSecurityModel. 108 | * 109 | * @param QueryBuilder $qb 110 | * @param string $alias 111 | */ 112 | protected function applySecurityModel(QueryBuilder $qb, $alias) 113 | { 114 | $model = $this->tenant->getTenantSecurityModel(); 115 | $method = 'apply' . ucfirst($model) . 'SecurityModel'; 116 | 117 | if (method_exists($this, $method)) { 118 | $this->$method($qb, $alias); 119 | } else { 120 | throw new RuntimeException( 121 | sprintf('Security model "%s" has not been implemented by "%s"', $model, $method) 122 | ); 123 | } 124 | } 125 | 126 | /** 127 | * @param QueryBuilder $qb 128 | * @param string $alias 129 | */ 130 | protected function applySharedSecurityModel(QueryBuilder $qb, $alias) 131 | { 132 | $qb 133 | ->where("{$alias}.tenantOwnerId = :tenantOwnerId") 134 | ->setParameters([ 135 | ':tenantOwnerId' => $this->tenant->getTenantOwnerId(), 136 | ]) 137 | ; 138 | } 139 | 140 | /** 141 | * @param QueryBuilder $qb 142 | * @param string $alias 143 | */ 144 | protected function applyUserSecurityModel(QueryBuilder $qb, $alias) 145 | { 146 | $user = $this->tenant->getUser(); 147 | if ($user instanceof BelongsToTenantParticipants) { 148 | $tenants = $user->getTenantParticipants(); 149 | } else { 150 | $tenants = new ArrayCollection([$this->tenant->getTenantCreator()]); 151 | } 152 | 153 | $qb 154 | ->where("{$alias}.tenantOwnerId = :tenantOwnerId") 155 | ->andWhere("{$alias}.tenantCreatorId IN (:tenantCreators)") 156 | ->setParameters([ 157 | ':tenantOwnerId' => $this->tenant->getTenantOwnerId(), 158 | ':tenantCreators' => $tenants, 159 | ]) 160 | ; 161 | } 162 | 163 | /** 164 | * @param QueryBuilder $qb 165 | * @param string $alias 166 | */ 167 | protected function applyClosedSecurityModel(QueryBuilder $qb, $alias) 168 | { 169 | $qb 170 | ->where("{$alias}.tenantOwnerId = :tenantOwnerId") 171 | ->andWhere("{$alias}.tenantCreatorId = :tenantCreatorId") 172 | ->setParameters([ 173 | ':tenantOwnerId' => $this->tenant->getTenantOwnerId(), 174 | ':tenantCreatorId' => $this->tenant->getTenantCreatorId(), 175 | ]) 176 | ; 177 | } 178 | 179 | /** 180 | * Finds an object by its primary key / identifier. 181 | * 182 | * @param mixed $id The identifier. 183 | * 184 | * @return object|null The object. 185 | * @todo Might have performance issues since we are accessing EM to get class 186 | * meta data to reference the correct primary identifier. 187 | * 188 | * @todo Allow composite primary key look up? 189 | * 190 | */ 191 | public function find($id) 192 | { 193 | $qb = $this->createQueryBuilder('o'); 194 | $meta = $this->em->getClassMetadata($this->repository->getClassName()); 195 | 196 | $qb->andWhere("o.{$meta->getSingleIdentifierFieldName()} = :id")->setParameter(':id', $id); 197 | 198 | return $qb->getQuery()->getOneOrNullResult(); 199 | } 200 | 201 | /** 202 | * Finds all objects in the repository. 203 | * 204 | * @return array The objects. 205 | */ 206 | public function findAll() 207 | { 208 | return $this->findBy([]); 209 | } 210 | 211 | /** 212 | * Finds objects by a set of criteria. 213 | * 214 | * Optionally sorting and limiting details can be passed. An implementation may throw 215 | * an UnexpectedValueException if certain values of the sorting or limiting details are 216 | * not supported. 217 | * 218 | * @param array $criteria 219 | * @param array|null $orderBy ['field' => 'ASC|DESC'] 220 | * @param int|null $limit 221 | * @param int|null $offset 222 | * 223 | * @return array The objects. 224 | * 225 | * @throws UnexpectedValueException 226 | */ 227 | public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 228 | { 229 | $qb = $this->createQueryBuilder('o'); 230 | if (null !== $offset) { 231 | $qb->setFirstResult($offset); 232 | } 233 | if (null !== $limit) { 234 | $qb->setMaxResults($limit); 235 | } 236 | 237 | $this->applyCriteriaAndOrderByToQueryBuilder($qb, $criteria, $orderBy); 238 | 239 | return $qb->getQuery()->getResult(); 240 | } 241 | 242 | /** 243 | * Finds a single entity by a set of criteria. 244 | * 245 | * @param array $criteria 246 | * @param array|null $orderBy ['field' => 'ASC|DESC'] 247 | * 248 | * @return object|null The entity instance or NULL if the entity can not be found. 249 | */ 250 | public function findOneBy(array $criteria, array $orderBy = null) 251 | { 252 | $qb = $this->createQueryBuilder('o'); 253 | 254 | $this->applyCriteriaAndOrderByToQueryBuilder($qb, $criteria, $orderBy); 255 | 256 | return $qb->getQuery()->getOneOrNullResult(); 257 | } 258 | 259 | /** 260 | * Returns the class name of the object managed by the repository. 261 | * 262 | * @return string 263 | */ 264 | public function getClassName() 265 | { 266 | return $this->repository->getClassName(); 267 | } 268 | 269 | /** 270 | * @param QueryBuilder $qb 271 | * @param array $criteria 272 | * @param array $orderBy 273 | */ 274 | protected function applyCriteriaAndOrderByToQueryBuilder(QueryBuilder $qb, array $criteria, array $orderBy = null) 275 | { 276 | if (null !== $orderBy) { 277 | foreach ($orderBy as $field => $direction) { 278 | $qb->addOrderBy('o.' . $field, $direction); 279 | } 280 | } 281 | 282 | foreach ($criteria as $key => $value) { 283 | if (is_array($value)) { 284 | $qb->andWhere($qb->expr()->in("o.{$key}", $value)); 285 | } else { 286 | $qb->andWhere("o.{$key} = :{$key}_value")->setParameter(":{$key}_value", $value); 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Repositories/TenantParticipantRepository.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 32 | } 33 | 34 | /** 35 | * Finds an object by its primary key / identifier. 36 | * 37 | * @param mixed $id The identifier. 38 | * 39 | * @return TenantParticipantContract|null The object. 40 | */ 41 | public function find($id) 42 | { 43 | return $this->repository->find($id); 44 | } 45 | 46 | /** 47 | * Finds all objects in the repository. 48 | * 49 | * @return array|TenantParticipantContract[] The objects. 50 | */ 51 | public function findAll() 52 | { 53 | return $this->repository->findAll(); 54 | } 55 | 56 | /** 57 | * Finds objects by a set of criteria. 58 | * 59 | * Optionally sorting and limiting details can be passed. An implementation may throw 60 | * an UnexpectedValueException if certain values of the sorting or limiting details are 61 | * not supported. 62 | * 63 | * @param array $criteria 64 | * @param array|null $orderBy 65 | * @param int|null $limit 66 | * @param int|null $offset 67 | * 68 | * @return array|TenantParticipantContract[] The objects. 69 | * 70 | * @throws UnexpectedValueException 71 | */ 72 | public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) 73 | { 74 | return $this->repository->findBy($criteria, $orderBy, $limit, $offset); 75 | } 76 | 77 | /** 78 | * Finds a single object by a set of criteria. 79 | * 80 | * @param array $criteria The criteria. 81 | * @param array|null $orderBy 82 | * 83 | * @return TenantParticipantContract The object. 84 | */ 85 | public function findOneBy(array $criteria, array $orderBy = null) 86 | { 87 | return $this->repository->findOneBy($criteria, $orderBy); 88 | } 89 | 90 | /** 91 | * Returns the class name of the object managed by the repository. 92 | * 93 | * @return string 94 | */ 95 | public function getClassName() 96 | { 97 | return $this->repository->getClassName(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Services/TenantTypeResolver.php: -------------------------------------------------------------------------------- 1 | mappings = new Collection(); 31 | } 32 | 33 | /** 34 | * @param TenantParticipantContract $tenant 35 | * @param string $type 36 | * 37 | * @return boolean 38 | */ 39 | public function hasType(TenantParticipantContract $tenant, $type) 40 | { 41 | if (null === $class = $this->getMapping($type)) { 42 | throw new InvalidArgumentException( 43 | sprintf('Type "%s" is not mapped to the TenantParticipant class', $type) 44 | ); 45 | } 46 | 47 | if ($tenant instanceof $class) { 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * @return Collection 56 | */ 57 | public function getMappings() 58 | { 59 | return $this->mappings; 60 | } 61 | 62 | /** 63 | * @param array $mappings 64 | * 65 | * @return $this 66 | */ 67 | public function setMappings(array $mappings) 68 | { 69 | $this->mappings = new Collection($mappings); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * @param string $class 76 | * 77 | * @return array 78 | */ 79 | public function getMappingsForClass($class) 80 | { 81 | $return = []; 82 | 83 | foreach ($this->mappings as $key => $value) { 84 | if ($value == $class) { 85 | $return[] = $key; 86 | } 87 | } 88 | 89 | return $return; 90 | } 91 | 92 | /** 93 | * @param string $type 94 | * 95 | * @return boolean 96 | */ 97 | public function hasMapping($type) 98 | { 99 | return $this->mappings->has($type); 100 | } 101 | 102 | /** 103 | * @param string $type 104 | * 105 | * @return string|null 106 | */ 107 | public function getMapping($type) 108 | { 109 | return $this->mappings->get($type); 110 | } 111 | 112 | /** 113 | * @param string $type 114 | * @param string $class 115 | * 116 | * @return $this 117 | */ 118 | public function addMapping($type, $class) 119 | { 120 | $this->mappings[$type] = $class; 121 | $this->mappings[$class] = $class; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @param string $type 128 | * 129 | * @return $this 130 | */ 131 | public function removeMapping($type) 132 | { 133 | $this->mappings->forget($type); 134 | $mappings = $this->getMappingsForClass($type); 135 | 136 | foreach ($mappings as $key) { 137 | $this->mappings->forget($key); 138 | } 139 | 140 | return $this; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/TenancyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([$this->getConfigPath() => config_path('tenancy.php'),], 'config'); 34 | } 35 | 36 | public function register() 37 | { 38 | $this->mergeConfig(); 39 | 40 | /** @var Repository $config */ 41 | $config = $this->app->make('config'); 42 | 43 | if ( 44 | !$config->get('tenancy.multi_account.enabled', false) && 45 | !$config->get('tenancy.multi_site.enabled', false) 46 | ) { 47 | return; 48 | } 49 | 50 | $this->registerTenantCoreServices($config); 51 | $this->registerMultiAccountTenancy($config); 52 | $this->registerMultiSiteTenancy($config); 53 | $this->registerTenantAwareRepositories($config); 54 | } 55 | 56 | protected function mergeConfig() 57 | { 58 | $this->mergeConfigFrom($this->getConfigPath(), 'tenancy'); 59 | } 60 | 61 | protected function registerTenantCoreServices(Repository $config) 62 | { 63 | if (!$this->app->resolved(TenantContract::class)) { 64 | // might be registered by TenantAwareApplication already 65 | $this->app->singleton(TenantContract::class, function ($app) { 66 | return new Tenant(new NullUser(), new NullTenant(), new NullTenant()); 67 | }); 68 | 69 | $this->app->alias(TenantContract::class, 'auth.tenant'); 70 | } 71 | 72 | $this->app->singleton(TenantRedirectorService::class, function ($app) { 73 | return new TenantRedirectorService(); 74 | }); 75 | $this->app->singleton(TenantTypeResolver::class, function ($app) { 76 | return new TenantTypeResolver(); 77 | }); 78 | 79 | /* Aliases */ 80 | $this->app->alias(TenantRedirectorService::class, 'auth.tenant.redirector'); 81 | $this->app->alias(TenantTypeResolver::class, 'auth.tenant.type_resolver'); 82 | } 83 | 84 | protected function registerMultiAccountTenancy(Repository $config) 85 | { 86 | if (!$config->get('tenancy.multi_account.enabled', false)) { 87 | return; 88 | } 89 | 90 | $this->registerMultiAccountParticipantRepository($config); 91 | $this->registerTenantParticipantMappings($config->get('tenancy.multi_account.participant.mappings')); 92 | } 93 | 94 | protected function registerMultiSiteTenancy(Repository $config) 95 | { 96 | if (!$config->get('tenancy.multi_site.enabled', false)) { 97 | return; 98 | } 99 | 100 | if (!$this->app instanceof TenantAwareApplication) { 101 | throw new RuntimeException( 102 | 'Multi-site requires updating your bootstrap/app.php to use TenantAwareApplication' 103 | ); 104 | } 105 | 106 | /* 107 | * @todo Need a way to detect if an app RouteServiceProvider is registered as it needs replacing. 108 | */ 109 | 110 | $this->registerMultiSiteParticipantRepository($config); 111 | $this->registerMultiSiteConsoleCommands(); 112 | $this->registerTenantParticipantMappings($config->get('tenancy.multi_site.participant.mappings')); 113 | } 114 | 115 | protected function registerTenantParticipantMappings(array $mappings = []) 116 | { 117 | $resolver = $this->app->make('auth.tenant.type_resolver'); 118 | foreach ($mappings as $alias => $class) { 119 | $resolver->addMapping($alias, $class); 120 | } 121 | } 122 | 123 | protected function registerMultiAccountParticipantRepository(Repository $config) 124 | { 125 | $repository = $config->get('tenancy.multi_account.participant.repository'); 126 | $entity = $config->get('tenancy.multi_account.participant.class'); 127 | 128 | $this->app->singleton(TenantRepositoryContract::class, function ($app) use ($repository, $entity) { 129 | return new TenantParticipantRepository( 130 | new $repository($app['em'], $app['em']->getClassMetaData($entity)) 131 | ); 132 | }); 133 | 134 | $this->app->alias(TenantRepositoryContract::class, 'auth.tenant.account_repository'); 135 | } 136 | 137 | protected function registerMultiSiteParticipantRepository(Repository $config) 138 | { 139 | $repository = $config->get('tenancy.multi_site.participant.repository'); 140 | $entity = $config->get('tenancy.multi_site.participant.class'); 141 | 142 | $this->app->singleton(DomainTenantRepositoryContract::class, 143 | function ($app) use ($repository, $entity) { 144 | return new DomainAwareTenantParticipantRepository( 145 | new $repository($app['em'], $app['em']->getClassMetaData($entity)) 146 | ); 147 | } 148 | ); 149 | 150 | $this->app->alias(DomainTenantRepositoryContract::class, 'auth.tenant.site_repository'); 151 | } 152 | 153 | protected function registerMultiSiteConsoleCommands() 154 | { 155 | $this->commands([ 156 | Console\TenantListCommand::class, 157 | Console\TenantRouteListCommand::class, 158 | Console\TenantRouteCacheCommand::class, 159 | Console\TenantRouteClearCommand::class, 160 | ]); 161 | } 162 | 163 | protected function registerTenantAwareRepositories(Repository $config) 164 | { 165 | foreach ($config->get('tenancy.doctrine.repositories', []) as $details) { 166 | if (!isset($details['repository']) && !isset($details['base'])) { 167 | throw new InvalidArgumentException( 168 | sprintf('Failed to process tenant repository data: missing repository/base from definition') 169 | ); 170 | } 171 | 172 | $this->app->singleton($details['repository'], function ($app) use ($details) { 173 | $class = $details['repository']; 174 | 175 | return new $class($app['em'], $app[$details['base']], $app['auth.tenant']); 176 | }); 177 | 178 | if (isset($details['alias'])) { 179 | $this->app->alias($details['repository'], $details['alias']); 180 | } 181 | if (isset($details['tags'])) { 182 | $this->app->tag($details['repository'], $details['tags']); 183 | } 184 | } 185 | } 186 | 187 | protected function getConfigPath() 188 | { 189 | return __DIR__ . '/../config/tenancy.php'; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Twig/TenantExtension.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 29 | } 30 | 31 | public function getFunctions() 32 | { 33 | return [ 34 | new TwigFunction('current_tenant_owner_id', [$this->tenant, 'getTenantOwnerId']), 35 | new TwigFunction('current_tenant_creator_id', [$this->tenant, 'getTenantCreatorId']), 36 | new TwigFunction('current_tenant_owner', [$this->tenant, 'getTenantOwner']), 37 | new TwigFunction('current_tenant_creator', [$this->tenant, 'getTenantCreator']), 38 | new TwigFunction('current_tenant_security_model', [$this->tenant, 'getSecurityModel']), 39 | ]; 40 | } 41 | } 42 | --------------------------------------------------------------------------------