sinclairt / multi-tenancy
Multi tenancy solution inside a single app and single database
Requires (Dev)
- laracasts/testdummy: ~2.0
- laravel/laravel: dev-develop
- phpspec/phpspec: ~2.1
- phpunit/phpunit: ~4.0
README
Installation
composer require sinclairt/multi-tenancy
Register the service provider in the config/app.php array:
\Sinclair\MultiTenancy\Providers\MultiTenancySericeProvider::class
Optional
Publish the config:
php artisan vendor:publish
Config
ignore-roles default([ 'super-admin' ])
if you don't want your app to apply the multi-tenancy constraints add the roles here, I've assumed super-admin out of the box, but feel free to change this or leave as an empty array if you're not using roles
public-space-roles default(['super-admin'])
if you would like to protect your public domain with auth routes, you can set which roles should be allowed access to this area
relationship name default('tenant')
this is the name of the relationship method on your models, the plural is assumed for many-to-many
relationship table default('tenants')
name of the tenant table
relationship class default(\App\Models\Tenant::class)
class name of the tenant model
relationship polymorph-term default('tenantable')
the polymorphic term used in you models relationships
relationship foreign key default('tenant_id')
the foreign key used on your models tables
relationship slug column name default('slug')
the column where the slug is stored on the tenant table
should apply callback default(null)
if you want to use custom logic to decide whether to apply the scopes set you callback here the user object and ignored roles array are passed into this
should apply default default(true)
if true will automatically apply the scopes to the models, this is only used if the callback is null and the ignore roles are empty
role class default(\App\Models\Role::class)
The name of your role class - leave as an empty string if you're not using roles
Set Up
Inside config/auth.php
you will need to set up a new guard and a new provider like so:
// guard
'tenant' => [
'driver' => 'session',
'provider' => 'tenants'
]
// provider
'tenants' => [
'driver' => 'tenant',
'model' => App\Models\User::class,
],
You may also like to set the default guard to tenant or set an if statement up, otherwise you will need to specify the guard when interacting with the Auth
object:
'guard' => !is_null(constant('TENANT_SLUG')) ? 'tenant' : 'web',
The multi-tenancy package works by using global scopes to restrict queries based on a
constant TENANT_SLUG
. This package assumes you are using a sub-domain to control
which tenant is required by your user; there is a helper function to place inside
bootstrap/app.php bootstrapMultiTenancy()
, but, of course, you are free to set the
constant however you wish, but in order for this package to work it must be set.
Usage
To avoid having a foreign key on every single database table, the multi-tenancy package uses a models relationships to constrain queries. There a three ways a model can be connected to a tenant:
-
Directly - the model belongs to the tenant
-
Through - the model belongs to the tenant through another model or chain of models
-
Morph - the model belongs to the tenant via a many-to-many relationship (polymorphic included)
There are three scopes that can be applied based on the criteria above, and there are traits to ease the implementation for two of them:
the BelongsToTenant
and MorphToTenant
scopes have respective traits, just use them in your models and that's it.
The other scope BelongsToTenantThrough
requires a little more set up, but only a little, all you need to do is set the relationship(s) to go through to get to the tenant when applying the scope in the models boot method, for example:
public static function boot()
{
parent::boot();
static::addGlobalScope(new BelongsToTenantThrough('user'));
}
The vehicle model in the example above belongs to the Tenant in one way or another and so we will go through this relationship to get to the tenant.
You can also use dot notation to chain relationships i.e. driver.user
, this applies when there are series of relationships to go through to get to the tenant.
One final scenario is if your model is a polymorphic many-to-many and could potentially belong to the tenant through various channels, easy, pass in an array of all the potential links to the tenant, the multi-tenancy package only needs to find one:
static::addGlobalScope(new BelongsToTenantThrough([ 'users', 'drivers.user', 'locations' ]));
In this example we have a Phone model which stores numbers against various models: users, drivers, and locations, so we need to check whether it belongs to our given tenant through any of those connections.
User Model
Because the multi-tenancy package allows you to have a single database, it means a user can belong to more than one tenant if you want them to, useful for admin roles (although I recommend using the ignore-roles config value). If you use the sub-domain solution for multi-tenancy this will force the user to login to new tenant areas but it means they can have the same credentials, roles, and permissions across tenants.
Your User model needs to use the IsMultiTenantUser
trait if you are using roles, it provides a piece of logic for the scopes, but it also uses the MorphToTenant
trait and sets the roles relationship for you. If you're not using roles, be sure to use the MorphToTenant
trait in your User model.