├── phpstan.neon.dist ├── src ├── Exceptions │ ├── AbstractTenantException.php │ ├── InvalidDomainTenantException.php │ ├── InvalidSubdomainTenantException.php │ └── ModelNotFoundForTenantException.php ├── Contracts │ └── TenantResolver.php ├── Traits │ ├── TenantableChild.php │ └── Tenantable.php ├── Resolvers │ ├── SubdomainOrDomainTenantResolver.php │ ├── DomainTenantResolver.php │ └── SubdomainTenantResolver.php ├── Scopes │ ├── TenantableChildScope.php │ └── TenantableScope.php ├── Console │ └── Commands │ │ ├── PublishCommand.php │ │ ├── RollbackCommand.php │ │ └── MigrateCommand.php ├── Providers │ └── TenantsServiceProvider.php └── Models │ └── Tenant.php ├── LICENSE ├── config └── config.php ├── database └── migrations │ ├── 2020_01_01_000002_create_tenantables_table.php │ └── 2020_01_01_000001_create_tenants_table.php ├── CONTRIBUTING.md ├── composer.json ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | parameters: 4 | level: 5 5 | paths: 6 | - src 7 | -------------------------------------------------------------------------------- /src/Exceptions/AbstractTenantException.php: -------------------------------------------------------------------------------- 1 | model = $model; 20 | $this->message = "No query results for model [{$model}] when scoped by tenant."; 21 | 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Resolvers/SubdomainOrDomainTenantResolver.php: -------------------------------------------------------------------------------- 1 | getHost(), array_keys(config('app.domains')))) { 23 | return SubdomainTenantResolver::resolve(); 24 | } 25 | 26 | return DomainTenantResolver::resolve(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Resolvers/DomainTenantResolver.php: -------------------------------------------------------------------------------- 1 | where('is_active', true)->where('domain', $host = request()->getHost())->first(); 23 | 24 | throw_unless($tenant, InvalidDomainTenantException::class, $host); 25 | 26 | return $tenant; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021, Rinvex LLC, 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Scopes/TenantableChildScope.php: -------------------------------------------------------------------------------- 1 | whereHas($builder->getModel()->getRelationshipToTenantable()); 24 | } 25 | 26 | /** 27 | * Extend the query builder with the needed functions. 28 | * 29 | * @param \Illuminate\Database\Eloquent\Builder $builder 30 | * 31 | * @return void 32 | */ 33 | public function extend(Builder $builder) 34 | { 35 | $builder->macro('withoutTenantables', function (Builder $builder) { 36 | return $builder->withoutGlobalScope($this); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | alert($this->description); 35 | 36 | collect($this->option('resource') ?: ['config', 'migrations'])->each(function ($resource) { 37 | $this->call('vendor:publish', ['--tag' => "rinvex/tenants::{$resource}", '--force' => $this->option('force')]); 38 | }); 39 | 40 | $this->line(''); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | true, 9 | 10 | // Tenants Database Tables 11 | 'tables' => [ 12 | 13 | 'tenants' => 'tenants', 14 | 'tenantables' => 'tenantables', 15 | 16 | ], 17 | 18 | // Tenants Models 19 | 'models' => [ 20 | 'tenant' => \Rinvex\Tenants\Models\Tenant::class, 21 | ], 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Tenant Resolver Class 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This package resolve currently active tenant using Resolver Classes. 29 | | It comes with few default resolvers that you can use, or you can 30 | | build your own custom resolver to support additional functions. 31 | | 32 | | Default Resolvers: 33 | | - \Rinvex\Tenants\Http\Resolvers\DomainTenantResolver::class 34 | | - \Rinvex\Tenants\Http\Resolvers\SubdomainTenantResolver::class 35 | | - \Rinvex\Tenants\Http\Resolvers\SubdomainOrDomainTenantResolver::class 36 | | 37 | */ 38 | 39 | 'resolver' => \Rinvex\Tenants\Resolvers\SubdomainOrDomainTenantResolver::class, 40 | 41 | ]; 42 | -------------------------------------------------------------------------------- /database/migrations/2020_01_01_000002_create_tenantables_table.php: -------------------------------------------------------------------------------- 1 | integer('tenant_id')->unsigned(); 21 | $table->morphs('tenantable'); 22 | $table->timestamps(); 23 | 24 | // Indexes 25 | $table->unique(['tenant_id', 'tenantable_id', 'tenantable_type'], 'tenantables_ids_type_unique'); 26 | $table->foreign('tenant_id')->references('id')->on(config('rinvex.tenants.tables.tenants')) 27 | ->onDelete('cascade')->onUpdate('cascade'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down(): void 37 | { 38 | Schema::dropIfExists(config('rinvex.tenants.tables.tenantables')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/RollbackCommand.php: -------------------------------------------------------------------------------- 1 | alert($this->description); 35 | 36 | $path = config('rinvex.tenants.autoload_migrations') ? 37 | 'vendor/rinvex/laravel-tenants/database/migrations' : 38 | 'database/migrations/rinvex/laravel-tenants'; 39 | 40 | if (file_exists($path)) { 41 | $this->call('migrate:reset', [ 42 | '--path' => $path, 43 | '--force' => $this->option('force'), 44 | ]); 45 | } else { 46 | $this->warn('No migrations found! Consider publish them first: php artisan rinvex:publish:tenants'); 47 | } 48 | 49 | $this->line(''); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/Commands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | alert($this->description); 35 | 36 | $path = config('rinvex.tenants.autoload_migrations') ? 37 | 'vendor/rinvex/laravel-tenants/database/migrations' : 38 | 'database/migrations/rinvex/laravel-tenants'; 39 | 40 | if (file_exists($path)) { 41 | $this->call('migrate', [ 42 | '--step' => true, 43 | '--path' => $path, 44 | '--force' => $this->option('force'), 45 | ]); 46 | } else { 47 | $this->warn('No migrations found! Consider publish them first: php artisan rinvex:publish:tenants'); 48 | } 49 | 50 | $this->line(''); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Scopes/TenantableScope.php: -------------------------------------------------------------------------------- 1 | tenant = $tenant; 29 | } 30 | 31 | /** 32 | * Apply the scope to a given Eloquent query builder. 33 | * 34 | * @param \Illuminate\Database\Eloquent\Builder $builder 35 | * @param \Illuminate\Database\Eloquent\Model $model 36 | * 37 | * @return void 38 | */ 39 | public function apply(Builder $builder, Model $model) 40 | { 41 | $builder->whereHas('tenants', function (Builder $builder) { 42 | $builder->where($key = $this->tenant->getKeyName(), $this->tenant->{$key})->where('is_active', true); 43 | }); 44 | } 45 | 46 | /** 47 | * Extend the query builder with the needed functions. 48 | * 49 | * @param \Illuminate\Database\Eloquent\Builder $builder 50 | * 51 | * @return void 52 | */ 53 | public function extend(Builder $builder) 54 | { 55 | $builder->macro('withoutTenants', function (Builder $builder) { 56 | return $builder->withoutGlobalScope($this); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Resolvers/SubdomainTenantResolver.php: -------------------------------------------------------------------------------- 1 | getHost()); 26 | 27 | // Check host if it matches criteria 28 | $isLocalhost = count($segments) === 1; 29 | $isCentralDomain = in_array($host, $appDomains, true); 30 | $isNotCentralSubdomain = ! Str::endsWith($host, $appDomains); 31 | $isIpAddress = count(array_filter($segments, 'is_numeric')) === count($segments); 32 | 33 | // Throw an exception if the host is not a valid subdomain of central domains 34 | throw_if($isCentralDomain || $isLocalhost || $isIpAddress || $isNotCentralSubdomain, InvalidSubdomainTenantException::class, $host); 35 | 36 | $tenant = app('rinvex.tenants.tenant')->where('is_active', true)->where('slug', $tenantSlug = $segments[0])->first(); 37 | 38 | // Throw an exception if tenant not found 39 | throw_unless($tenant, InvalidDomainTenantException::class, $tenantSlug); 40 | 41 | return $tenant; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2020_01_01_000001_create_tenants_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 21 | $table->string('slug'); 22 | $table->string('domain')->nullable(); 23 | $table->json('name'); 24 | $table->json('description')->nullable(); 25 | $table->string('email'); 26 | $table->string('website')->nullable(); 27 | $table->string('phone')->nullable(); 28 | $table->string('language_code', 2); 29 | $table->string('country_code', 2); 30 | $table->string('state')->nullable(); 31 | $table->string('city')->nullable(); 32 | $table->string('address')->nullable(); 33 | $table->string('postal_code')->nullable(); 34 | $table->date('launch_date')->nullable(); 35 | $table->string('timezone')->nullable(); 36 | $table->string('currency')->nullable(); 37 | $table->boolean('is_active')->default(true); 38 | $table->timestamps(); 39 | $table->softDeletes(); 40 | 41 | // Indexes 42 | $table->unique('slug'); 43 | $table->unique('domain'); 44 | }); 45 | } 46 | 47 | /** 48 | * Reverse the migrations. 49 | * 50 | * @return void 51 | */ 52 | public function down(): void 53 | { 54 | Schema::dropIfExists(config('rinvex.tenants.tables.tenants')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This project adheres to the following standards and practices. 4 | 5 | 6 | ## Versioning 7 | 8 | This project is versioned under the [Semantic Versioning](http://semver.org/) guidelines as much as possible. 9 | 10 | Releases will be numbered with the following format: 11 | 12 | - `..` 13 | - `..` 14 | 15 | And constructed with the following guidelines: 16 | 17 | - Breaking backward compatibility bumps the major and resets the minor and patch. 18 | - New additions without breaking backward compatibility bump the minor and reset the patch. 19 | - Bug fixes and misc changes bump the patch. 20 | 21 | 22 | ## Pull Requests 23 | 24 | The pull request process differs for new features and bugs. 25 | 26 | Pull requests for bugs may be sent without creating any proposal issue. If you believe that you know of a solution for a bug that has been filed, please leave a comment detailing your proposed fix or create a pull request with the fix mentioning that issue id. 27 | 28 | 29 | ## Coding Standards 30 | 31 | This project follows the FIG PHP Standards Recommendations compliant with the [PSR-1: Basic Coding Standard](http://www.php-fig.org/psr/psr-1/), [PSR-2: Coding Style Guide](http://www.php-fig.org/psr/psr-2/) and [PSR-4: Autoloader](http://www.php-fig.org/psr/psr-4/) to ensure a high level of interoperability between shared PHP code. If you notice any compliance oversights, please send a patch via pull request. 32 | 33 | 34 | ## Feature Requests 35 | 36 | If you have a proposal or a feature request, you may create an issue with `[Proposal]` in the title. 37 | 38 | The proposal should also describe the new feature, as well as implementation ideas. The proposal will then be reviewed and either approved or denied. Once a proposal is approved, a pull request may be created implementing the new feature. 39 | 40 | 41 | ## Git Flow 42 | 43 | This project follows [Git-Flow](http://nvie.com/posts/a-successful-git-branching-model/), and as such has `master` (latest stable releases), `develop` (latest WIP development) and X.Y support branches (when there's multiple major versions). 44 | 45 | Accordingly all pull requests MUST be sent to the `develop` branch. 46 | 47 | > **Note:** Pull requests which do not follow these guidelines will be closed without any further notice. 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rinvex/laravel-tenants", 3 | "description": "Rinvex Tenants is a contextually intelligent polymorphic Laravel package, for single db multi-tenancy. You can completely isolate tenants data with ease using the same database, with full power and control over what data to be centrally shared, and what to be tenant related and therefore isolated from others.", 4 | "type": "library", 5 | "keywords": [ 6 | "model", 7 | "tenant", 8 | "laravel", 9 | "eloquent", 10 | "tenantable", 11 | "translatable", 12 | "polymorphic", 13 | "sluggable", 14 | "tenancy", 15 | "rinvex", 16 | "multi" 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://rinvex.com", 20 | "support": { 21 | "email": "help@rinvex.com", 22 | "issues": "https://github.com/rinvex/laravel-tenants/issues", 23 | "source": "https://github.com/rinvex/laravel-tenants", 24 | "docs": "https://github.com/rinvex/laravel-tenants/blob/master/README.md" 25 | }, 26 | "authors": [ 27 | { 28 | "name": "Rinvex LLC", 29 | "homepage": "https://rinvex.com", 30 | "email": "help@rinvex.com" 31 | }, 32 | { 33 | "name": "Abdelrahman Omran", 34 | "homepage": "https://omranic.com", 35 | "email": "me@omranic.com", 36 | "role": "Project Lead" 37 | }, 38 | { 39 | "name": "The Generous Laravel Community", 40 | "homepage": "https://github.com/rinvex/laravel-tenants/contributors" 41 | } 42 | ], 43 | "require": { 44 | "php": "^8.1.0", 45 | "illuminate/console": "^10.0.0 || ^11.0.0", 46 | "illuminate/database": "^10.0.0 || ^11.0.0", 47 | "illuminate/support": "^10.0.0 || ^11.0.0", 48 | "propaganistas/laravel-phone": "^5.0.0", 49 | "rinvex/countries": "^9.0.0", 50 | "rinvex/languages": "^7.0.0", 51 | "rinvex/laravel-support": "^7.0.0", 52 | "spatie/laravel-sluggable": "^3.4.0", 53 | "symfony/console": "^6.2.0" 54 | }, 55 | "require-dev": { 56 | "codedungeon/phpunit-result-printer": "^0.32.0", 57 | "illuminate/container": "^10.0.0 || ^11.0.0", 58 | "phpunit/phpunit": "^10.1.0" 59 | }, 60 | "autoload": { 61 | "psr-4": { 62 | "Rinvex\\Tenants\\": "src/" 63 | } 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "Rinvex\\Tenants\\Tests\\": "tests" 68 | } 69 | }, 70 | "scripts": { 71 | "test": "vendor/bin/phpunit" 72 | }, 73 | "config": { 74 | "sort-packages": true, 75 | "preferred-install": "dist", 76 | "optimize-autoloader": true 77 | }, 78 | "extra": { 79 | "laravel": { 80 | "providers": [ 81 | "Rinvex\\Tenants\\Providers\\TenantsServiceProvider" 82 | ] 83 | } 84 | }, 85 | "minimum-stability": "dev", 86 | "prefer-stable": true 87 | } 88 | -------------------------------------------------------------------------------- /src/Providers/TenantsServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(realpath(__DIR__.'/../../config/config.php'), 'rinvex.tenants'); 40 | 41 | // Bind eloquent models to IoC container 42 | $this->registerModels([ 43 | 'rinvex.tenants.tenant' => Tenant::class, 44 | ]); 45 | 46 | // Register console commands 47 | $this->commands($this->commands); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function boot() 54 | { 55 | // Register paths to be published by the publish command. 56 | $this->publishConfigFrom(realpath(__DIR__.'/../../config/config.php'), 'rinvex/tenants'); 57 | $this->publishMigrationsFrom(realpath(__DIR__.'/../../database/migrations'), 'rinvex/tenants'); 58 | 59 | ! $this->app['config']['rinvex.tenants.autoload_migrations'] || $this->loadMigrationsFrom(realpath(__DIR__.'/../../database/migrations')); 60 | 61 | // Resolve active tenant 62 | $this->resolveActiveTenant(); 63 | 64 | // Map relations 65 | Relation::morphMap([ 66 | 'tenant' => config('rinvex.tenants.models.tenant'), 67 | ]); 68 | } 69 | 70 | /** 71 | * Resolve active tenant. 72 | * 73 | * @return void 74 | */ 75 | protected function resolveActiveTenant() 76 | { 77 | $tenant = null; 78 | 79 | try { 80 | // Just check if we have DB connection! This is to avoid 81 | // exceptions on new projects before configuring database options 82 | DB::connection()->getPdo(); 83 | 84 | if (! array_key_exists($this->app['request']->getHost(), config('app.domains')) && Schema::hasTable(config('rinvex.tenants.tables.tenants'))) { 85 | $tenant = config('rinvex.tenants.resolver')::resolve(); 86 | } 87 | } catch (Exception $e) { 88 | // Be quiet! Do not do or say anything!! 89 | } 90 | 91 | // Resolve and register tenant into service container 92 | $this->app->singleton('request.tenant', fn () => $tenant); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [help@rinvex.com](mailto:help@rinvex.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/Models/Tenant.php: -------------------------------------------------------------------------------- 1 | 'string', 104 | 'domain' => 'string', 105 | 'name' => 'string', 106 | 'description' => 'string', 107 | 'email' => 'string', 108 | 'website' => 'string', 109 | 'phone' => 'string', 110 | 'country_code' => 'string', 111 | 'language_code' => 'string', 112 | 'state' => 'string', 113 | 'city' => 'string', 114 | 'address' => 'string', 115 | 'postal_code' => 'string', 116 | 'launch_date' => 'string', 117 | 'timezone' => 'string', 118 | 'currency' => 'string', 119 | 'is_active' => 'boolean', 120 | 'deleted_at' => 'datetime', 121 | ]; 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | protected $observables = [ 127 | 'validating', 128 | 'validated', 129 | ]; 130 | 131 | /** 132 | * The attributes that are translatable. 133 | * 134 | * @var array 135 | */ 136 | public $translatable = [ 137 | 'name', 138 | 'description', 139 | ]; 140 | 141 | /** 142 | * The default rules that the model will validate against. 143 | * 144 | * @var array 145 | */ 146 | protected $rules = []; 147 | 148 | /** 149 | * Whether the model should throw a 150 | * ValidationException if it fails validation. 151 | * 152 | * @var bool 153 | */ 154 | protected $throwValidationExceptions = true; 155 | 156 | /** 157 | * Create a new Eloquent model instance. 158 | * 159 | * @param array $attributes 160 | */ 161 | public function __construct(array $attributes = []) 162 | { 163 | $this->setTable(config('rinvex.tenants.tables.tenants')); 164 | $this->mergeRules([ 165 | 'slug' => 'required|alpha_dash|max:150|unique:'.config('rinvex.tenants.models.tenant').',slug', 166 | 'domain' => 'nullable|strip_tags|max:150|unique:'.config('rinvex.tenants.models.tenant').',domain', 167 | 'name' => 'required|string|strip_tags|max:150', 168 | 'description' => 'nullable|string|max:32768', 169 | 'email' => 'required|email:rfc,dns|min:3|max:128|unique:'.config('rinvex.tenants.models.tenant').',email', 170 | 'website' => 'nullable|url|max:1500', 171 | 'phone' => 'nullable|phone:AUTO', 172 | 'country_code' => 'required|alpha|size:2|country', 173 | 'language_code' => 'required|alpha|size:2|language', 174 | 'state' => 'nullable|string', 175 | 'city' => 'nullable|string', 176 | 'address' => 'nullable|string', 177 | 'postal_code' => 'nullable|string', 178 | 'launch_date' => 'nullable|date_format:Y-m-d', 179 | 'timezone' => 'nullable|string|max:64|timezone', 180 | 'currency' => 'nullable|alpha|size:3|currency', 181 | 'is_active' => 'sometimes|boolean', 182 | ]); 183 | 184 | parent::__construct($attributes); 185 | } 186 | 187 | /** 188 | * Get all attached models of the given class to the tenant. 189 | * 190 | * @param string $class 191 | * 192 | * @return \Illuminate\Database\Eloquent\Relations\MorphToMany 193 | */ 194 | public function entries(string $class): MorphToMany 195 | { 196 | return $this->morphedByMany($class, 'tenantable', config('rinvex.tenants.tables.tenantables'), 'tenant_id', 'tenantable_id', 'id', 'id'); 197 | } 198 | 199 | /** 200 | * Get the options for generating the slug. 201 | * 202 | * @return \Spatie\Sluggable\SlugOptions 203 | */ 204 | public function getSlugOptions(): SlugOptions 205 | { 206 | return SlugOptions::create() 207 | ->doNotGenerateSlugsOnUpdate() 208 | ->generateSlugsFrom('name') 209 | ->saveSlugsTo('slug'); 210 | } 211 | 212 | /** 213 | * Get the tenant's country. 214 | * 215 | * @return \Rinvex\Country\Country 216 | */ 217 | public function getCountryAttribute() 218 | { 219 | return country($this->country_code); 220 | } 221 | 222 | /** 223 | * Get the tenant's language. 224 | * 225 | * @return \Rinvex\Language\Language 226 | */ 227 | public function getLanguageAttribute() 228 | { 229 | return language($this->language_code); 230 | } 231 | 232 | /** 233 | * Determine if the given model is supermanager of the tenant. 234 | * 235 | * @param \Illuminate\Database\Eloquent\Model $model 236 | * 237 | * @return bool 238 | */ 239 | public function isSuperManager(Model $model): bool 240 | { 241 | return $this->isManager($model) && $model->isA('supermanager'); 242 | } 243 | 244 | /** 245 | * Determine if the given model is manager of the tenant. 246 | * 247 | * @param \Illuminate\Database\Eloquent\Model $model 248 | * 249 | * @return bool 250 | */ 251 | public function isManager(Model $model): bool 252 | { 253 | return $model->tenants->contains($this); 254 | } 255 | 256 | /** 257 | * Activate the tenant. 258 | * 259 | * @return $this 260 | */ 261 | public function activate() 262 | { 263 | $this->update(['is_active' => true]); 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Deactivate the tenant. 270 | * 271 | * @return $this 272 | */ 273 | public function deactivate() 274 | { 275 | $this->update(['is_active' => false]); 276 | 277 | return $this; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/Traits/Tenantable.php: -------------------------------------------------------------------------------- 1 | morphToMany(config('rinvex.tenants.models.tenant'), 'tenantable', config('rinvex.tenants.tables.tenantables'), 'tenantable_id', 'tenant_id') 70 | ->withTimestamps(); 71 | } 72 | 73 | /** 74 | * Attach the given tenant(s) to the model. 75 | * 76 | * @param mixed $tenants 77 | * 78 | * @return void 79 | */ 80 | public function setTenantsAttribute($tenants): void 81 | { 82 | static::saved(function (self $model) use ($tenants) { 83 | $model->syncTenants($tenants); 84 | }); 85 | } 86 | 87 | /** 88 | * Boot the tenantable trait for the model. 89 | * 90 | * @return void 91 | */ 92 | public static function bootTenantable() 93 | { 94 | if ($tenant = app('request.tenant')) { 95 | static::addGlobalScope('tenantable', new TenantableScope($tenant)); 96 | 97 | static::saved(function (self $model) use ($tenant) { 98 | $model->attachTenants($tenant); 99 | }); 100 | } 101 | 102 | static::deleted(function (self $model) { 103 | // Check if this is a soft delete or not by checking if `SoftDeletes::isForceDeleting` method exists 104 | (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) || $model->tenants()->detach(); 105 | }); 106 | } 107 | 108 | /** 109 | * Returns a new query builder without any of the tenant scopes applied. 110 | * 111 | * @return \Illuminate\Database\Eloquent\Builder 112 | */ 113 | public static function forAllTenants() 114 | { 115 | return (new static())->newQuery()->withoutGlobalScopes(['tenantable']); 116 | } 117 | 118 | /** 119 | * Override the default findOrFail method so that we can re-throw 120 | * a more useful exception. Otherwise it can be very confusing 121 | * why queries don't work because of tenant scoping issues. 122 | * 123 | * @param mixed $id 124 | * @param array $columns 125 | * 126 | * @throws \Rinvex\Tenants\Exceptions\ModelNotFoundForTenantException 127 | * 128 | * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection 129 | */ 130 | public static function findOrFail($id, $columns = ['*']) 131 | { 132 | try { 133 | return static::query()->findOrFail($id, $columns); 134 | } catch (ModelNotFoundException $exception) { 135 | // If it DOES exist, just not for this tenant, throw a nicer exception 136 | if (! is_null(static::forAllTenants()->find($id, $columns))) { 137 | throw (new ModelNotFoundForTenantException())->setModel(static::class, [$id]); 138 | } 139 | 140 | throw $exception; 141 | } 142 | } 143 | 144 | /** 145 | * Scope query with all the given tenants. 146 | * 147 | * @param \Illuminate\Database\Eloquent\Builder $builder 148 | * @param mixed $tenants 149 | * 150 | * @return \Illuminate\Database\Eloquent\Builder 151 | */ 152 | public function scopeWithAllTenants(Builder $builder, $tenants): Builder 153 | { 154 | $tenants = $this->prepareTenantIds($tenants); 155 | 156 | collect($tenants)->each(function ($tenant) use ($builder) { 157 | $builder->whereHas('tenants', function (Builder $builder) use ($tenant) { 158 | return $builder->where('id', $tenant); 159 | }); 160 | }); 161 | 162 | return $builder; 163 | } 164 | 165 | /** 166 | * Scope query with any of the given tenants. 167 | * 168 | * @param \Illuminate\Database\Eloquent\Builder $builder 169 | * @param mixed $tenants 170 | * 171 | * @return \Illuminate\Database\Eloquent\Builder 172 | */ 173 | public function scopeWithAnyTenants(Builder $builder, $tenants): Builder 174 | { 175 | $tenants = $this->prepareTenantIds($tenants); 176 | 177 | return $builder->whereHas('tenants', function (Builder $builder) use ($tenants) { 178 | $builder->whereIn('id', $tenants); 179 | }); 180 | } 181 | 182 | /** 183 | * Scope query with any of the given tenants. 184 | * 185 | * @param \Illuminate\Database\Eloquent\Builder $builder 186 | * @param mixed $tenants 187 | * 188 | * @return \Illuminate\Database\Eloquent\Builder 189 | */ 190 | public function scopeWithTenants(Builder $builder, $tenants): Builder 191 | { 192 | return static::scopeWithAnyTenants($builder, $tenants); 193 | } 194 | 195 | /** 196 | * Scope query without any of the given tenants. 197 | * 198 | * @param \Illuminate\Database\Eloquent\Builder $builder 199 | * @param mixed $tenants 200 | * 201 | * @return \Illuminate\Database\Eloquent\Builder 202 | */ 203 | public function scopeWithoutTenants(Builder $builder, $tenants): Builder 204 | { 205 | $tenants = $this->prepareTenantIds($tenants); 206 | 207 | return $builder->whereDoesntHave('tenants', function (Builder $builder) use ($tenants) { 208 | $builder->whereIn('id', $tenants); 209 | }); 210 | } 211 | 212 | /** 213 | * Scope query without any tenants. 214 | * 215 | * @param \Illuminate\Database\Eloquent\Builder $builder 216 | * 217 | * @return \Illuminate\Database\Eloquent\Builder 218 | */ 219 | public function scopeWithoutAnyTenants(Builder $builder): Builder 220 | { 221 | return $builder->doesntHave('tenants'); 222 | } 223 | 224 | /** 225 | * Determine if the model has any of the given tenants. 226 | * 227 | * @param mixed $tenants 228 | * 229 | * @return bool 230 | */ 231 | public function hasTenants($tenants): bool 232 | { 233 | $tenants = $this->prepareTenantIds($tenants); 234 | 235 | return ! $this->tenants->pluck('id')->intersect($tenants)->isEmpty(); 236 | } 237 | 238 | /** 239 | * Determine if the model has any the given tenants. 240 | * 241 | * @param mixed $tenants 242 | * 243 | * @return bool 244 | */ 245 | public function hasAnyTenants($tenants): bool 246 | { 247 | return static::hasTenants($tenants); 248 | } 249 | 250 | /** 251 | * Determine if the model has all of the given tenants. 252 | * 253 | * @param mixed $tenants 254 | * 255 | * @return bool 256 | */ 257 | public function hasAllTenants($tenants): bool 258 | { 259 | $tenants = $this->prepareTenantIds($tenants); 260 | 261 | return collect($tenants)->diff($this->tenants->pluck('id'))->isEmpty(); 262 | } 263 | 264 | /** 265 | * Sync model tenants. 266 | * 267 | * @param mixed $tenants 268 | * @param bool $detaching 269 | * 270 | * @return $this 271 | */ 272 | public function syncTenants($tenants, bool $detaching = true) 273 | { 274 | // Find tenants 275 | $tenants = $this->prepareTenantIds($tenants); 276 | 277 | // Sync model tenants 278 | $this->tenants()->sync($tenants, $detaching); 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * Attach model tenants. 285 | * 286 | * @param mixed $tenants 287 | * 288 | * @return $this 289 | */ 290 | public function attachTenants($tenants) 291 | { 292 | return $this->syncTenants($tenants, false); 293 | } 294 | 295 | /** 296 | * Detach model tenants. 297 | * 298 | * @param mixed $tenants 299 | * 300 | * @return $this 301 | */ 302 | public function detachTenants($tenants = null) 303 | { 304 | $tenants = ! is_null($tenants) ? $this->prepareTenantIds($tenants) : null; 305 | 306 | // Sync model tenants 307 | $this->tenants()->detach($tenants); 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * Prepare tenant IDs. 314 | * 315 | * @param mixed $tenants 316 | * 317 | * @return array 318 | */ 319 | protected function prepareTenantIds($tenants): array 320 | { 321 | // Convert collection to plain array 322 | if ($tenants instanceof BaseCollection && is_string($tenants->first())) { 323 | $tenants = $tenants->toArray(); 324 | } 325 | 326 | // Find tenants by their ids 327 | if (is_numeric($tenants) || (is_array($tenants) && is_numeric(Arr::first($tenants)))) { 328 | return array_map('intval', (array) $tenants); 329 | } 330 | 331 | // Find tenants by their slugs 332 | if (is_string($tenants) || (is_array($tenants) && is_string(Arr::first($tenants)))) { 333 | $tenants = app('rinvex.tenants.tenant')->whereIn('slug', $tenants)->get()->pluck('id'); 334 | } 335 | 336 | if ($tenants instanceof Model) { 337 | return [$tenants->getKey()]; 338 | } 339 | 340 | if ($tenants instanceof Collection) { 341 | return $tenants->modelKeys(); 342 | } 343 | 344 | if ($tenants instanceof BaseCollection) { 345 | return $tenants->toArray(); 346 | } 347 | 348 | return (array) $tenants; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Rinvex Tenants Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](CONTRIBUTING.md). 6 | 7 | 8 | ## [v8.1.2] - 2023-07-03 9 | - Update composer dependencies 10 | - Use canonicalized absolute pathnames for resources 11 | 12 | ## [v8.1.1] - 2023-06-29 13 | - Require missing composer dependency 14 | - Refactor resource loading and publishing 15 | 16 | ## [v8.1.0] - 2023-05-02 17 | - 306f2b4: Add support for Laravel v11, and drop support for Laravel v9 18 | - f8a8e0c: Upgrade spatie/laravel-translatable to v6.5 from v6.0 19 | - 0417634: Upgrade spatie/laravel-sluggable to v3.4 from v3.3 20 | - 348c050: Update propaganistas/laravel-phone to v5.0 from v4.4 21 | - b531808: Update phpunit to v10.1 from v9.5 22 | - 0497f95: Fix tenant attributes 23 | 24 | ## [v8.0.0] - 2023-01-09 25 | - Add Relation::morphMap 26 | - Tweak artisan commands registration 27 | - Drop PHP v8.0 support and update composer dependencies 28 | - Utilize PHP 8.1 attributes feature for artisan commands 29 | 30 | ## [v7.3.1] - 2022-06-20 31 | - Update composer dependencies spatie/laravel-translatable to ^6.0.0 from ^5.2.0 32 | - Add alias for tenantable global scopes 33 | - Require rfc & dns email validation rules 34 | 35 | ## [v7.3.0] - 2022-02-14 36 | - Update composer dependencies to Laravel v9 37 | - Add support for model HasFactory 38 | 39 | ## [v7.2.1] - 2021-10-22 40 | - Refactor resolveActiveTenant to check if there's a db connection first, otherwise skip silently 41 | - Update .styleci.yml fixers 42 | 43 | ## [v7.2.0] - 2021-10-10 44 | - Update documentation 45 | - Move app.url & session.domain runtime override to modules & application layer 46 | - Use the new app.domains config option 47 | - Remove config options and global helpers to modules and application layer 48 | - Check before detaching pages if deleted entity was soft deleted 49 | 50 | ## [v7.1.0] - 2021-08-22 51 | - Drop PHP v7 support, and upgrade rinvex package dependencies to next major version 52 | - Update composer dependencies 53 | 54 | ## [v7.0.1] - 2021-08-21 55 | - Introduce central subdomains functionality and improve tenant resolution logic 56 | 57 | ## [v7.0.0] - 2021-08-18 58 | - Breaking Change: Refactor of tenant resolving logic 59 | - Add domain and subdomain tenant resolvers and exceptions 60 | - Dynamically change session domain config on the fly, if current requested host is not a central domain or a central subdomain 61 | - Resolve and register currently active tenant into service container 62 | - Add central_domains() & tenant_domains() global helpers 63 | - Add central domains config options to support additional alias domains 64 | - Add TenantableChild trait and TenantableChildScope for child models 65 | - Remove useless TenantNotSetException 66 | - Move global scope to TenantableScope class 67 | - Scope queries to is_active tenants 68 | - Add support for withoutTenants 69 | - Add domain attribute to tenants 70 | - Update composer dependencies 71 | - Update documentation 72 | 73 | ## [v6.0.5] - 2021-05-24 74 | - Merge rules instead of resetting, to allow adequate model override 75 | - Update spatie/laravel-translatable composer package to v5.0.0 76 | - Update spatie/laravel-sluggable composer package to v3.0.0 77 | 78 | ## [v6.0.4] - 2021-05-11 79 | - Fix constructor initialization order (fill attributes should come next after merging fillables & rules) 80 | 81 | ## [v6.0.3] - 2021-05-07 82 | - Drop old MySQL versions support that doesn't support json columns 83 | - Upgrade to GitHub-native Dependabot 84 | - Use app() method alias `has` instead of `bound` for better readability 85 | - Utilize SoftDeletes 86 | 87 | ## [v6.0.2] - 2021-02-06 88 | - Simplify service provider model registration into IoC 89 | - Enable StyleCI risky mode 90 | 91 | ## [v6.0.1] - 2020-12-25 92 | - Add support for PHP v8 93 | 94 | ## [v6.0.0] - 2020-12-22 95 | - Upgrade to Laravel v8 96 | - Move custom eloquent model events to module layer from core package layer 97 | - Refactor and tweak Eloquent Events 98 | 99 | ## [v5.0.2] - 2020-08-04 100 | - Make sure `request.tenant` IoC service already bound before using 101 | - Update timezone validation rule 102 | 103 | ## [v5.0.1] - 2020-07-16 104 | - Update timezone validation rule 105 | - Update validation rules 106 | 107 | ## [v5.0.0] - 2020-06-19 108 | - Refactor active tenant container service binding 109 | 110 | ## [v4.1.0] - 2020-06-15 111 | - Fix attaching categories by their IDs where IDs are passed mistakenly as strings in some cases! 112 | - Fix phone validation rule 113 | - Drop using rinvex/laravel-cacheable from core packages for more flexibility 114 | - Caching should be handled on the application layer, not enforced from the core packages 115 | - Drop PHP 7.2 & 7.3 support from travis 116 | 117 | ## [v4.0.6] - 2020-05-30 118 | - Remove default indent size config 119 | - Add strip_tags validation rule to string fields 120 | - Specify events queue 121 | - Explicitly specify relationship attributes 122 | - Add strip_tags validation rule 123 | - Explicitly define relationship name 124 | 125 | ## [v4.0.5] - 2020-04-12 126 | - Fix ServiceProvider registerCommands method compatibility 127 | 128 | ## [v4.0.4] - 2020-04-09 129 | - Tweak artisan command registration 130 | - Reverse commit "Convert database int fields into bigInteger" 131 | - Refactor publish command and allow multiple resource values 132 | 133 | ## [v4.0.3] - 2020-04-04 134 | - Fix namespace issue 135 | 136 | ## [v4.0.2] - 2020-04-04 137 | - Enforce consistent artisan command tag namespacing 138 | - Enforce consistent package namespace 139 | - Drop laravel/helpers usage as it's no longer used 140 | 141 | ## [v4.0.1] - 2020-03-20 142 | - Convert into bigInteger database fields 143 | - Add shortcut -f (force) for artisan publish commands 144 | - Fix migrations path 145 | 146 | ## [v4.0.0] - 2020-03-15 147 | - Upgrade to Laravel v7.1.x & PHP v7.4.x 148 | 149 | ## [v3.0.3] - 2020-03-13 150 | - Tweak TravisCI config 151 | - Add migrations autoload option to the package 152 | - Tweak service provider `publishesResources` 153 | - Remove indirect composer dependency 154 | - Drop using global helpers 155 | - Update StyleCI config 156 | 157 | ## [v3.0.2] - 2019-12-18 158 | - Fix `migrate:reset` args as it doesn't accept --step 159 | - Create event classes and map them in the model 160 | 161 | ## [v3.0.1] - 2019-09-24 162 | - Add missing laravel/helpers composer package 163 | 164 | ## [v3.0.0] - 2019-09-23 165 | - Upgrade to Laravel v6 and update dependencies 166 | 167 | ## [v2.1.1] - 2019-06-03 168 | - Enforce latest composer package versions 169 | 170 | ## [v2.1.0] - 2019-06-02 171 | - Update composer deps 172 | - Drop PHP 7.1 travis test 173 | - Refactor migrations and artisan commands, and tweak service provider publishes functionality 174 | 175 | ## [v2.0.0] - 2019-03-03 176 | - Rename environment variable QUEUE_DRIVER to QUEUE_CONNECTION 177 | - Require PHP 7.2 & Laravel 5.8 178 | - Apply PHPUnit 8 updates 179 | - Replace get_called_class() with static::class (potentially deprecated in PHP 7.4) 180 | - Refactor isManager & isSupermanager methods 181 | - Drop ownership feature of tenants 182 | 183 | ## [v1.0.3] - 2018-12-23 184 | - Add missing countries & languages dependencies (fix #19) 185 | 186 | ## [v1.0.2] - 2018-12-22 187 | - Update composer dependencies 188 | - Add PHP 7.3 support to travis 189 | - Fix MySQL / PostgreSQL json column compatibility 190 | 191 | ## [v1.0.1] - 2018-10-05 192 | - Fix wrong composer package version constraints 193 | 194 | ## [v1.0.0] - 2018-10-01 195 | - Enforce Consistency 196 | - Support Laravel 5.7+ 197 | - Rename package to rinvex/laravel-tenants 198 | 199 | ## [v0.0.5] - 2018-09-21 200 | - Update travis php versions 201 | - Define polymorphic relationship parameters explicitly 202 | - Rename tenant "user" to "owner" 203 | - Add isOwner and isStaff model methods 204 | - Install composer package propaganistas/laravel-phone for phone verification 205 | - Require composer package rinvex/languages 206 | - Loose strongly typed return value of owner relationship for flexible override on module level 207 | - Remove group and add timezone, currency attributes 208 | - Drop StyleCI multi-language support (paid feature now!) 209 | - Update composer dependencies 210 | - Prepare and tweak testing configuration 211 | - Highlight variables in strings explicitly 212 | - Update StyleCI options 213 | - Update PHPUnit options 214 | - Rename model activation and deactivation methods 215 | - Add tag model factory 216 | 217 | ## [v0.0.4] - 2018-02-18 218 | - Rename tenantable global scope 219 | - Update supplementary files 220 | - Add PublishCommand to artisan 221 | - Move slug auto generation to the custom HasSlug trait 222 | - Add Rollback Console Command 223 | - Refactor tenants active instance 224 | - Remove useless TenantBadFormatException exception 225 | - Add missing composer dependencies 226 | - Typehint method returns 227 | - Update composer dependencies 228 | - Drop useless model contracts (models already swappable through IoC) 229 | - Add Laravel v5.6 support 230 | - Simplify IoC binding 231 | - Convert tenant owner to polymorphic and rename it to user 232 | - Drop Laravel 5.5 support 233 | - Update PHPUnit to 7.0.0 234 | 235 | ## [v0.0.3] - 2017-09-09 236 | - Fix many issues and apply many enhancements 237 | - Rename package rinvex/laravel-tenants from rinvex/tenantable 238 | 239 | ## [v0.0.2] - 2017-06-29 240 | - Enforce consistency 241 | - Tweak active flag column 242 | - Add Laravel 5.5 support 243 | - Replace hardcoded table names 244 | - Tweak model event registration 245 | - Drop sorting order support, no need 246 | - Add owner_id foreign key constraint 247 | - Fix wrong slug generation method order 248 | - Fix country_code & language_code validation rules 249 | - Fix validation rules and check owner id existence 250 | 251 | ## v0.0.1 - 2017-04-11 252 | - Tag first release 253 | 254 | [v8.1.2]: https://github.com/rinvex/laravel-tenants/compare/v8.1.1...v8.1.2 255 | [v8.1.1]: https://github.com/rinvex/laravel-tenants/compare/v8.1.0...v8.1.1 256 | [v8.1.0]: https://github.com/rinvex/laravel-tenants/compare/v8.0.0...v8.1.0 257 | [v8.0.0]: https://github.com/rinvex/laravel-tenants/compare/v7.3.1...v8.0.0 258 | [v7.3.1]: https://github.com/rinvex/laravel-tenants/compare/v7.3.0...v7.3.1 259 | [v7.3.0]: https://github.com/rinvex/laravel-tenants/compare/v7.2.1...v7.3.0 260 | [v7.2.1]: https://github.com/rinvex/laravel-tenants/compare/v7.2.0...v7.2.1 261 | [v7.2.0]: https://github.com/rinvex/laravel-tenants/compare/v7.1.0...v7.2.0 262 | [v7.1.0]: https://github.com/rinvex/laravel-tenants/compare/v7.0.1...v7.1.0 263 | [v7.0.1]: https://github.com/rinvex/laravel-tenants/compare/v7.0.0...v7.0.1 264 | [v7.0.0]: https://github.com/rinvex/laravel-tenants/compare/v6.0.5...v7.0.0 265 | [v6.0.5]: https://github.com/rinvex/laravel-tenants/compare/v6.0.4...v6.0.5 266 | [v6.0.4]: https://github.com/rinvex/laravel-tenants/compare/v6.0.3...v6.0.4 267 | [v6.0.3]: https://github.com/rinvex/laravel-tenants/compare/v6.0.2...v6.0.3 268 | [v6.0.2]: https://github.com/rinvex/laravel-tenants/compare/v6.0.1...v6.0.2 269 | [v6.0.1]: https://github.com/rinvex/laravel-tenants/compare/v6.0.0...v6.0.1 270 | [v6.0.0]: https://github.com/rinvex/laravel-tenants/compare/v5.0.2...v6.0.0 271 | [v5.0.2]: https://github.com/rinvex/laravel-tenants/compare/v5.0.1...v5.0.2 272 | [v5.0.1]: https://github.com/rinvex/laravel-tenants/compare/v5.0.0...v5.0.1 273 | [v5.0.0]: https://github.com/rinvex/laravel-tenants/compare/v4.1.0...v5.0.0 274 | [v4.1.0]: https://github.com/rinvex/laravel-tenants/compare/v4.0.6...v4.1.0 275 | [v4.0.6]: https://github.com/rinvex/laravel-tenants/compare/v4.0.5...v4.0.6 276 | [v4.0.5]: https://github.com/rinvex/laravel-tenants/compare/v4.0.4...v4.0.5 277 | [v4.0.4]: https://github.com/rinvex/laravel-tenants/compare/v4.0.3...v4.0.4 278 | [v4.0.3]: https://github.com/rinvex/laravel-tenants/compare/v4.0.2...v4.0.3 279 | [v4.0.2]: https://github.com/rinvex/laravel-tenants/compare/v4.0.1...v4.0.2 280 | [v4.0.1]: https://github.com/rinvex/laravel-tenants/compare/v4.0.0...v4.0.1 281 | [v4.0.0]: https://github.com/rinvex/laravel-tenants/compare/v3.0.3...v4.0.0 282 | [v3.0.3]: https://github.com/rinvex/laravel-tenants/compare/v3.0.2...v3.0.3 283 | [v3.0.2]: https://github.com/rinvex/laravel-tenants/compare/v3.0.1...v3.0.2 284 | [v3.0.1]: https://github.com/rinvex/laravel-tenants/compare/v3.0.0...v3.0.1 285 | [v3.0.0]: https://github.com/rinvex/laravel-tenants/compare/v2.1.1...v3.0.0 286 | [v2.1.1]: https://github.com/rinvex/laravel-tenants/compare/v2.1.0...v2.1.1 287 | [v2.1.0]: https://github.com/rinvex/laravel-tenants/compare/v2.0.0...v2.1.0 288 | [v2.0.0]: https://github.com/rinvex/laravel-tenants/compare/v1.0.3...v2.0.0 289 | [v1.0.3]: https://github.com/rinvex/laravel-tenants/compare/v1.0.2...v1.0.3 290 | [v1.0.2]: https://github.com/rinvex/laravel-tenants/compare/v1.0.1...v1.0.2 291 | [v1.0.1]: https://github.com/rinvex/laravel-tenants/compare/v1.0.0...v1.0.1 292 | [v1.0.0]: https://github.com/rinvex/laravel-tenants/compare/v0.0.5...v1.0.0 293 | [v0.0.5]: https://github.com/rinvex/laravel-tenants/compare/v0.0.4...v0.0.5 294 | [v0.0.4]: https://github.com/rinvex/laravel-tenants/compare/v0.0.3...v0.0.4 295 | [v0.0.3]: https://github.com/rinvex/laravel-tenants/compare/v0.0.2...v0.0.3 296 | [v0.0.2]: https://github.com/rinvex/laravel-tenants/compare/v0.0.1...v0.0.2 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rinvex Tenants 2 | 3 | **Rinvex Tenants** is a contextually intelligent polymorphic Laravel package, for single db multi-tenancy. You can completely isolate tenants data with ease using the same database, with full power and control over what data to be centrally shared, and what to be tenant related and therefore isolated from others. 4 | 5 | [![Packagist](https://img.shields.io/packagist/v/rinvex/laravel-tenants.svg?label=Packagist&style=flat-square)](https://packagist.org/packages/rinvex/laravel-tenants) 6 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/rinvex/laravel-tenants.svg?label=Scrutinizer&style=flat-square)](https://scrutinizer-ci.com/g/rinvex/laravel-tenants/) 7 | [![Travis](https://img.shields.io/travis/rinvex/laravel-tenants.svg?label=TravisCI&style=flat-square)](https://travis-ci.org/rinvex/laravel-tenants) 8 | [![StyleCI](https://styleci.io/repos/87875339/shield)](https://styleci.io/repos/87875339) 9 | [![License](https://img.shields.io/packagist/l/rinvex/laravel-tenants.svg?label=License&style=flat-square)](https://github.com/rinvex/laravel-tenants/blob/develop/LICENSE) 10 | 11 | 12 | ## Installation 13 | 14 | 1. Install the package via composer: 15 | ```shell 16 | composer require rinvex/laravel-tenants 17 | ``` 18 | 19 | 2. Publish resources (migrations and config files): 20 | ```shell 21 | php artisan rinvex:publish:tenants 22 | ``` 23 | 24 | 3. Execute migrations via the following command: 25 | ```shell 26 | php artisan rinvex:migrate:tenants 27 | ``` 28 | 29 | 4. Done! 30 | 31 | 32 | ## Usage 33 | 34 | **Rinvex Tenants** is developed with the concept that every tenantable model can be attached to multiple tenants at the same time, so you don't need special column in your model database table to specify the tenant it belongs to, tenant relationships simply stored in a separate central table. 35 | 36 | ### Scope Queries 37 | 38 | To scope your queries correctly, apply the `\Rinvex\Tenants\Traits\Tenantable` trait on primary models. This will ensure that all calls to your parent models are scoped to the current tenant, and that calls to their child relations are scoped through the parent relationships. 39 | 40 | ```php 41 | namespace App\Models; 42 | 43 | use App\Models\Feature; 44 | use Rinvex\Tenants\Traits\Tenantable; 45 | use Illuminate\Database\Eloquent\Model; 46 | 47 | class Product extends Model 48 | { 49 | use Tenantable; 50 | 51 | public function features() 52 | { 53 | return $this->hasMany(Feature::class); 54 | } 55 | } 56 | ``` 57 | 58 | #### Scope Child Model Queries 59 | 60 | If you have child models, like product features, and these features belongs to tenantable products via a relationship, you may need to scope these feature model queries as well. For that, you need to apply the `\Rinvex\Tenants\Traits\TenantableChild` trait on your child models, and define a new method `getRelationshipToTenantable` that returns a string of the parent relationship. Check the following example. 61 | 62 | ```php 63 | namespace App\Models; 64 | 65 | use App\Models\Product; 66 | use Illuminate\Database\Eloquent\Model; 67 | use Rinvex\Tenants\Traits\TenantableChild; 68 | 69 | class Feature extends Model 70 | { 71 | use TenantableChild; 72 | 73 | public function getRelationshipToTenantable(): string 74 | { 75 | return 'product'; 76 | } 77 | 78 | public function product() 79 | { 80 | return $this->belongsTo(Product::class); 81 | } 82 | } 83 | ``` 84 | 85 | And this will automatically scope the all `App\Models\Feature::class` queries to the current tenant. Note that the limitation of this is that you need to be able to define a relationship to a primary model, so if you need to do this on deeper level of children hierarchy, like a `App\Models\Discount::class` model that belongs to `App\Models\Feature::class` which belongs to `App\Models\Product::class` which belongs to a Tenant `Rinvex\Tenants\Models\Tenant::class`, you need to define some strange relationship. Laravel supports HasOneThrough, but not BelongsToThrough, so you'd need to do some hacks around that. 86 | 87 | ### Manage your tenants 88 | 89 | Nothing special here, just normal [Eloquent](https://laravel.com/docs/master/eloquent) model stuff: 90 | 91 | ```php 92 | // Create a new tenant 93 | app('rinvex.tenants.tenant')->create([ 94 | 'name' => 'ACME Inc.', 95 | 'slug' => 'acme', 96 | 'domain' => 'acme.test', 97 | 'email' => 'info@acme.test', 98 | 'language_code' => 'en', 99 | 'country_code' => 'us', 100 | ]); 101 | 102 | // Get existing tenant by id 103 | $tenant = app('rinvex.tenants.tenant')->find(1); 104 | ``` 105 | 106 | > **Notes:** since **Rinvex Tenants** extends and utilizes other awesome packages, checkout the following documentations for further details: 107 | > - Translatable out of the box using [`spatie/laravel-translatable`](https://github.com/spatie/laravel-translatable) 108 | > - Automatic Slugging using [`spatie/laravel-sluggable`](https://github.com/spatie/laravel-sluggable) 109 | 110 | ### Automatic Tenants Registration 111 | 112 | Tenants are automatically registered into [Service Container](https://laravel.com/docs/master/container) very early in the request, through service provider `boot` method. 113 | 114 | That way you'll have access to the current active tenant before models are loaded, scopes are needed, or traits are booted. That's also earlier than routes registration, and middleware pipeline, so you can assure any resources that needs to be scoped, are correctly scoped. 115 | 116 | ### Changing Active Tenant 117 | 118 | You can easily change current active tenant at any point of the request as follows: 119 | 120 | ```php 121 | $tenant = app('rinvex.tenants.tenant')->find(123); 122 | app()->bind('request.tenant', fn() => $tenant); 123 | ``` 124 | 125 | And to deactivate your tenant and stop scoping by it, simply set the same container service binding to `null` as follows: 126 | 127 | ```php 128 | app()->bind('request.tenant', null); 129 | ``` 130 | 131 | > **Notes:** 132 | > - Only one tenant could be active at a time, even if your resources belongs to multiple tenants. 133 | > - You can change the active tenant at any point of the request, but that newly activated tenant will only scope models retrieved after that change, while any other models retrieved at an earlier stage of the request will be scoped with the previous tenant, or not scoped at all (according to your logic). 134 | > - If a resource belongs to multiple tenants, you can switch between tenants by to a different tenant by reinitializing the request. Example: since tenants are currently resolved by domains or subdomains, to switch tenants you'll need to redirect the user the new tenant domain/subdomain, and the currently active tenant will be switched as well as the request will be automatically scoped by the new tenant. 135 | 136 | ### Default Tenant Resolvers 137 | 138 | **Rinvex Tenants** resolve currently active tenant using Resolver Classes. It comes with few default resolvers that you can use, or you can build your own custom resolver to support additional functionality. 139 | 140 | Default tenant resolver classes in config options: 141 | 142 | ```php 143 | // Tenant Resolver Class: 144 | // - \Rinvex\Tenants\Http\Resolvers\DomainTenantResolver::class 145 | // - \Rinvex\Tenants\Http\Resolvers\SubdomainTenantResolver::class 146 | // - \Rinvex\Tenants\Http\Resolvers\SubdomainOrDomainTenantResolver::class 147 | 'resolver' => \Rinvex\Tenants\Resolvers\SubdomainOrDomainTenantResolver::class, 148 | ``` 149 | 150 | The default tenant resolver used is `SubdomainOrDomainTenantResolver::class`, so this package automatically resolve currently active tenant using both domains and subdomains. You can change that via config options. 151 | 152 | ### Central Domains 153 | 154 | **Rinvex Tenants** supports running your application on multiple domains, we call them central domains. It also supports more sophisticated use cases, but that's out of this package's scope. 155 | 156 | For that reason, this package expects you to have the following config option in your `config/app.php`: 157 | 158 | ```php 159 | 'domains' => [ 160 | 'domain.net' => [], 161 | 'example.com' => [], 162 | ], 163 | ``` 164 | 165 | ### Default Domain 166 | 167 | The reason you need to add the above config option in the same format, is that it's meant to support more advanced use cases that's not covered by this package. If you need to check some of these use cases proceed to [Cortex Tenants](https://github.com/rinvex/cortex-tenants) which is an application module implementing accessareas concepts, and allows different domains to access different accessareas (i.e. frontarea, adminarea, managerarea, tenantarea ..etc). The baseline here is that you need to add the above config option to your `config/app.php` and specify all your application domains. 168 | 169 | You need to add the default domain to the domains list, since this package automatically overrides the default Laravel config option `app.url` with the matched domain, although you may need to write some application logic. Checkout the above mentioned [Cortex Tenants](https://github.com/rinvex/cortex-tenants) module for an example. 170 | 171 | ### Tenant Domains 172 | 173 | Tenants could be accessed via central subdomains (obviously subdomains on central domains), or via their own dedicated domains. 174 | 175 | For example if the default domain is `rinvex.com`, and tenant slug is `cortex` then central subdomain will be `cortex.rinvex.com`. 176 | 177 | Note that since this package supports multiple central domains, tenants will be accessible via all central subdomains, so if we have another alias central domain `rinvex.net`, you can expect `cortex` to be available on `cortex.rinvex.net` as well. 178 | 179 | Tenants can optionally have top level domains of their own too, something like `test-example.com`, which means it's now accessible through three different domains: 180 | - `cortex.rinvex.com` 181 | - `cortex.rinvex.net` 182 | - `test-example.com` 183 | 184 | ### Session Domain 185 | 186 | Since **Rinvex Tenants** supports multiple central and tenant domains, it needs to change the default laravel session configuration on the fly, and that's actually what it does. It will dynamically change `session.domain` config option based on the current request host. 187 | 188 | > **Note:** Due to security reasons, accessing the same application through multiple top level domains (.rinvex.com & .rinvex.net) means the user will need to login for each different domain, as their session and cookies are tied to the top level domain. Example: if the user logged in to `cortex.rinvex.com` they will stay logged-in to the top level domain `rinvex.com` and all it's subdomains like `website.rinvex.com`, but they will not be logged in to `cortex.rinvex.net` even if both domains directs to the same application, they will need to login again there. This is a known limitation due to enforced security restrictions by the browser. We may create a workaround in the future, but it's a bit complicated, and involves third-party cookies and CORS, so feel free to send a PR if you have a creative solution. 189 | 190 | ### Querying Tenant Scoped Models 191 | 192 | After you've added tenants, all queries against a tenantable Model will be scoped automatically: 193 | 194 | ```php 195 | // This will only include Models belonging to the currently active tenant 196 | $tenantProducts = \App\Models\Product::all(); 197 | 198 | // This will fail with a `ModelNotFoundForTenantException` if it belongs to the wrong tenant 199 | $product = \App\Models\Product::find(2); 200 | ``` 201 | 202 | If you need to query across all tenants, you can use `forAllTenants()` method: 203 | 204 | ```php 205 | // Will include results from ALL tenants, just for this query 206 | $allTenantProducts = \App\Models\Product::forAllTenants()->get(); 207 | ``` 208 | 209 | Under the hood, **Rinvex Tenants** uses Laravel's [Global Scopes](https://laravel.com/docs/master/eloquent#global-scopes), which means if you are scoping by active tenant, and you want to exclude one single query, you can do so: 210 | 211 | ```php 212 | // Will NOT be scoped, and will return results from ALL tenants, just for this query 213 | $allTenantProducts = \App\Models\Product::withoutTenants()->get(); 214 | ``` 215 | 216 | > **Notes:** 217 | > - When you are developing multi-tenancy applications, it can be confusing sometimes why you keep getting `ModelNotFound` exceptions for rows that **DO** exist, because they belong to the wrong tenant. 218 | > - **Rinvex Tenants** will catch those exceptions, and re-throw them as `ModelNotFoundForTenantException`, to help you out 🙂 219 | 220 | ### Manage your tenantable model 221 | 222 | The API is intutive and very straightforward, so let's give it a quick look: 223 | 224 | ```php 225 | // Get instance of your model 226 | $product = new \App\Models\Product::find(1); 227 | 228 | // Get attached tenants collection 229 | $product->tenants; 230 | 231 | // Get attached tenants query builder 232 | $product->tenants(); 233 | ``` 234 | 235 | You can attach tenants in various ways: 236 | 237 | ```php 238 | // Single tenant id 239 | $product->attachTenants(1); 240 | 241 | // Multiple tenant IDs array 242 | $product->attachTenants([1, 2, 5]); 243 | 244 | // Multiple tenant IDs collection 245 | $product->attachTenants(collect([1, 2, 5])); 246 | 247 | // Single tenant model instance 248 | $tenantInstance = app('rinvex.tenants.tenant')->first(); 249 | $product->attachTenants($tenantInstance); 250 | 251 | // Single tenant slug 252 | $product->attachTenants('test-tenant'); 253 | 254 | // Multiple tenant slugs array 255 | $product->attachTenants(['first-tenant', 'second-tenant']); 256 | 257 | // Multiple tenant slugs collection 258 | $product->attachTenants(collect(['first-tenant', 'second-tenant'])); 259 | 260 | // Multiple tenant model instances 261 | $tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get(); 262 | $product->attachTenants($tenantInstances); 263 | ``` 264 | 265 | > **Notes:** 266 | > - The `attachTenants()` method attach the given tenants to the model without touching the currently attached tenants, while there's the `syncTenants()` method that can detach any records that's not in the given items, this method takes a second optional boolean parameter that's set detaching flag to `true` or `false`. 267 | > - To detach model tenants you can use the `detachTenants()` method, which uses **exactly** the same signature as the `attachTenants()` method, with additional feature of detaching all currently attached tenants by passing null or nothing to that method as follows: `$product->detachTenants();`. 268 | 269 | And as you may have expected, you can check if tenants attached: 270 | 271 | ```php 272 | // Single tenant id 273 | $product->hasAnyTenants(1); 274 | 275 | // Multiple tenant IDs array 276 | $product->hasAnyTenants([1, 2, 5]); 277 | 278 | // Multiple tenant IDs collection 279 | $product->hasAnyTenants(collect([1, 2, 5])); 280 | 281 | // Single tenant model instance 282 | $tenantInstance = app('rinvex.tenants.tenant')->first(); 283 | $product->hasAnyTenants($tenantInstance); 284 | 285 | // Single tenant slug 286 | $product->hasAnyTenants('test-tenant'); 287 | 288 | // Multiple tenant slugs array 289 | $product->hasAnyTenants(['first-tenant', 'second-tenant']); 290 | 291 | // Multiple tenant slugs collection 292 | $product->hasAnyTenants(collect(['first-tenant', 'second-tenant'])); 293 | 294 | // Multiple tenant model instances 295 | $tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get(); 296 | $product->hasAnyTenants($tenantInstances); 297 | ``` 298 | 299 | > **Notes:** 300 | > - The `hasAnyTenants()` method check if **ANY** of the given tenants are attached to the model. It returns boolean `true` or `false` as a result. 301 | > - Similarly the `hasAllTenants()` method uses **exactly** the same signature as the `hasAnyTenants()` method, but it behaves differently and performs a strict comparison to check if **ALL** of the given tenants are attached. 302 | 303 | ### Advanced Usage 304 | 305 | #### Generate Tenant Slugs 306 | 307 | **Rinvex Tenants** auto generates slugs and auto detect and insert default translation for you if not provided, but you still can pass it explicitly through normal eloquent `create` method, as follows: 308 | 309 | ```php 310 | app('rinvex.tenants.tenant')->create(['name' => ['en' => 'My New Tenant'], 'slug' => 'custom-tenant-slug']); 311 | ``` 312 | 313 | > **Note:** Check **[Sluggable](https://github.com/spatie/laravel-sluggable)** package for further details. 314 | 315 | #### Smart Parameter Detection 316 | 317 | **Rinvex Tenants** methods that accept list of tenants are smart enough to handle almost all kinds of inputs as you've seen in the above examples. It will check input type and behave accordingly. 318 | 319 | #### Retrieve All Models Attached To The Tenant 320 | 321 | You may encounter a situation where you need to get all models attached to certain tenant, you do so with ease as follows: 322 | 323 | ```php 324 | $tenant = app('rinvex.tenants.tenant')->find(1); 325 | $tenant->entries(\App\Models\Product::class); 326 | ``` 327 | 328 | #### Query Scopes 329 | 330 | Yes, **Rinvex Tenants** shipped with few awesome query scopes for your convenience, usage example: 331 | 332 | ```php 333 | // Single tenant id 334 | $product->withAnyTenants(1)->get(); 335 | 336 | // Multiple tenant IDs array 337 | $product->withAnyTenants([1, 2, 5])->get(); 338 | 339 | // Multiple tenant IDs collection 340 | $product->withAnyTenants(collect([1, 2, 5]))->get(); 341 | 342 | // Single tenant model instance 343 | $tenantInstance = app('rinvex.tenants.tenant')->first(); 344 | $product->withAnyTenants($tenantInstance)->get(); 345 | 346 | // Single tenant slug 347 | $product->withAnyTenants('test-tenant')->get(); 348 | 349 | // Multiple tenant slugs array 350 | $product->withAnyTenants(['first-tenant', 'second-tenant'])->get(); 351 | 352 | // Multiple tenant slugs collection 353 | $product->withAnyTenants(collect(['first-tenant', 'second-tenant']))->get(); 354 | 355 | // Multiple tenant model instances 356 | $tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get(); 357 | $product->withAnyTenants($tenantInstances)->get(); 358 | ``` 359 | 360 | > **Notes:** 361 | > - The `withAnyTenants()` scope finds products with **ANY** attached tenants of the given. It returns normally a query builder, so you can chain it or call `get()` method for example to execute and get results. 362 | > - Similarly there's few other scopes like `withAllTenants()` that finds products with **ALL** attached tenants of the given, `withoutTenants()` which finds products without **ANY** attached tenants of the given, and lastly `withoutAnyTenants()` which find products without **ANY** attached tenants at all. All scopes are created equal, with same signature, and returns query builder. 363 | 364 | #### Tenant Translations 365 | 366 | Manage tenant translations with ease as follows: 367 | 368 | ```php 369 | $tenant = app('rinvex.tenants.tenant')->find(1); 370 | 371 | // Update title translations 372 | $tenant->setTranslation('name', 'en', 'New English Tenant Title')->save(); 373 | 374 | // Alternatively you can use default eloquent update 375 | $tenant->update([ 376 | 'name' => [ 377 | 'en' => 'New Tenant', 378 | 'ar' => 'مستأجر جديد', 379 | ], 380 | ]); 381 | 382 | // Get single tenant translation 383 | $tenant->getTranslation('name', 'en'); 384 | 385 | // Get all tenant translations 386 | $tenant->getTranslations('name'); 387 | 388 | // Get tenant title in default locale 389 | $tenant->name; 390 | ``` 391 | 392 | > **Note:** Check **[Translatable](https://github.com/spatie/laravel-translatable)** package for further details. 393 | 394 | 395 | ## Changelog 396 | 397 | Refer to the [Changelog](CHANGELOG.md) for a full history of the project. 398 | 399 | 400 | ## Support 401 | 402 | The following support channels are available at your fingertips: 403 | 404 | - [Chat on Slack](https://bit.ly/rinvex-slack) 405 | - [Help on Email](mailto:help@rinvex.com) 406 | - [Follow on Twitter](https://twitter.com/rinvex) 407 | 408 | 409 | ## Contributing & Protocols 410 | 411 | Thank you for considering contributing to this project! The contribution guide can be found in [CONTRIBUTING.md](CONTRIBUTING.md). 412 | 413 | Bug reports, feature requests, and pull requests are very welcome. 414 | 415 | - [Versioning](CONTRIBUTING.md#versioning) 416 | - [Pull Requests](CONTRIBUTING.md#pull-requests) 417 | - [Coding Standards](CONTRIBUTING.md#coding-standards) 418 | - [Feature Requests](CONTRIBUTING.md#feature-requests) 419 | - [Git Flow](CONTRIBUTING.md#git-flow) 420 | 421 | 422 | ## Security Vulnerabilities 423 | 424 | If you discover a security vulnerability within this project, please send an e-mail to [help@rinvex.com](help@rinvex.com). All security vulnerabilities will be promptly addressed. 425 | 426 | 427 | ## About Rinvex 428 | 429 | Rinvex is a software solutions startup, specialized in integrated enterprise solutions for SMEs established in Alexandria, Egypt since June 2016. We believe that our drive The Value, The Reach, and The Impact is what differentiates us and unleash the endless possibilities of our philosophy through the power of software. We like to call it Innovation At The Speed Of Life. That’s how we do our share of advancing humanity. 430 | 431 | 432 | ## License 433 | 434 | This software is released under [The MIT License (MIT)](LICENSE). 435 | 436 | (c) 2016-2022 Rinvex LLC, Some rights reserved. 437 | --------------------------------------------------------------------------------