Multi-Tenancy for Laravel and Laravel-Doctrine.

This library provides the necessary infra-structure for a complex multi-tenant application. Multi-tenancy allows an application to be silo'd into protected areas by some form of tenant identifier. This could be by sub-domain, URL parameter or some other scheme.

Article image for: Multi-Tenancy for Laravel and Laravel-Doctrine

Getting Started.

Terminology
Types of Tenancy
Getting Started
Security Models
Routing
Middleware & Views
Issues / Links

Github Print

Setup / Getting Started

  • add \Somnambulist\Tenancy\TenancyServiceProvider::class to your config/app.php
  • add \Somnambulist\Tenancy\EventSubscribers\TenantOwnerEventSubscriber::class to config/doctrine.php subscribers
  • create or import the config/tenancy.php file
  • create your TenantParticipant entity / repository and add to the config file
  • create your participant mappings in the config file (at least class => class)
  • create your User with tenancy support
  • create an App\Http\Controller\TenantController to handle the various tenant redirects
  • add the basic routes
  • for multi-site
    • in bootstrap/app.php
      • change Application instance to \Somnambulist\Tenancy\Foundation\TenantAwareApplication
      • Note: if multi-site is enabled and this changed is not made, an exception will be raised.
    • in HttpKernel:
      • add TenantSiteResolver middleware to middleware, after CheckForMaintenanceMode
      • add TenantRouteResolver middleware to middleware, after TenantSiteResolver
      • remove RouteServiceProvider from config/app.php
  • for standard app tenancy and/or for tenancy within multi-site
    • add AuthenticateTenant as auth.tenant to HttpKernel route middlewares
    • add EnsureTenantType as auth.tenant.type to HttpKernel route middlewares

Doctrine Event Subscriber

An event subscriber is provided that will automatically set the tenancy information on any tenant aware entity upon persist. Note that this only occurs on prePersist and once created should not be modified. If this information is subsequently removed, then records may simply disappear when accessing the tenant aware repositories.

Example User

The following is an example of a tenant aware user that has a single tenant:

namespace App\Entity;

use Somnambulist\Tenancy\Contracts\BelongsToTenant as BelongsToTenantContract;
use Somnambulist\Tenancy\Contracts\BelongsToTenantParticipants;
use Somnambulist\Tenancy\Contracts\TenantParticipant;
use Somnambulist\Tenancy\Traits\BelongsToTenant;
class User implements AuthenticatableContract, AuthorizableContract,
       CanResetPasswordContract, BelongsToTenantContract, BelongsToTenantParticipant
{
    use BelongsToTenant;

    protected $tenant;

    public function getTenantParticipant()
    {
        return $this->tenant;
    }

    public function setTenantParticipant(TenantParticipant $tenant)
    {
        $this->tenant = $tenant;
    }
}

Example Tenant Participant

The following is an example of a tenant participant:

namespace App\Entity;

use Somnambulist\Doctrine\Traits\Identifiable;
use Somnambulist\Doctrine\Traits\Nameable;
use Somnambulist\Tenancy\Contracts\TenantParticipant as TenantParticipantContract;
use Somnambulist\Tenancy\Traits\TenantParticipant;

class Account implements TenantParticipantContract
{
    use Identifiable;
    use Nameable;
    use TenantParticipant;
}

Basic Routes

The two authentication middlewares expect the following routes to be defined and available:

// tenant selection and error routes
Route::group(['prefix' => 'tenant', 'as' => 'tenant.', 'middleware' => ['auth']], function () {
    Route::get('select',          ['as' => 'select_tenant',             'uses' => 'TenantController@selectTenantAction']);
    Route::get('no-tenants',      ['as' => 'no_tenants',                'uses' => 'TenantController@noTenantsAvailableAction']);
    Route::get('no-access',       ['as' => 'access_denied',             'uses' => 'TenantController@accessDeniedAction']);
    Route::get('not-supported',   ['as' => 'tenant_type_not_supported', 'uses' => 'TenantController@tenantTypeNotSupportedAction']);
    Route::get('invalid-request', ['as' => 'invalid_tenant_hierarchy',  'uses' => 'TenantController@invalidHierarchyAction']);
});

As a separate block (or within the previous section) add the areas of the application that require tenancy support / enforcement. These routes should be prefixed with at least: {tenant_creator_id}. {tenant_owner_id} can be used (first) which will force the auth.tenant middleware to validate that the creator belongs to the owner as well as the current user having access to the creator.

Note: the user does not need access to the tenant owner, access to the tenant creator implies permission to access a sub-set of the data.

// Tenant Aware Routes
Route::group(['prefix' => 'account/{tenant_creator_id}', 'as' => 'tenant.', 'namespace' => 'Tenant', 'middleware' => ['auth', 'auth.tenant']], function () {
    Route::get('/', ['as' => 'index', 'uses' => 'DashboardController@indexAction']);

    // routes that should be limited to certain ParticipantTypes
    Route::group(['prefix' => 'customer', 'as' => 'customer.', 'namespace' => 'Customer', 'middleware' => ['auth.tenant.type:crm']], function () {
        Route::get('/', ['as' => 'index', 'uses' => 'CustomerController@indexAction']);
    });
});

AuthController Changes

When using tenancy, the AuthController must be modified to include the redirector service to know where to go to after a successful login. If your AuthController is the standard Laravel provided one, simply add an authenticated method:

class AuthController extends Controller
{
    /**
     * @param Request $request
     * @param User    $user
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    protected function authenticated($request, $user)
    {
        // do post authentication stuff...
        //$user->setLastLogin(Carbon::now());
        //$em = app('em');
        //$em->flush($user);

        // redirect to tenant uri
        return app('auth.tenant.redirector')->resolve($user);
    }
}

In addition, if you allow registration of new users you will need to now add support for the tenancy component. This must be done by overriding the postRegister method:

class AuthController ...
{
    public function postRegister(Request $request)
    {
        $validator = $this->validator($request->all());

        if ($validator->fails()) {
            $this->throwValidationException(
                $request,
                $validator
            );
        }

        $user = $this->create($request->all());
        Auth::login($user);

        // call into redirector which was previously mapped above
        return $this->authenticated($request, $user);
    }
}

It is up to the implementer to figure out what to do with new registrations or if this should even be allowed.

Tenant Aware Entity

Finally you need something that is actually tenant aware! So lets create a really basic customer:

namespace App\Entity;

use Somnambulist\Doctrine\Contracts\GloballyTrackable as GloballyTrackableContract;
use Somnambulist\Doctrine\Traits\GloballyTrackable;
use Somnambulist\Tenancy\Contracts\TenantAware as TenantAwareContract;
use Somnambulist\Tenancy\Traits\TenantAware;

class Customer implements GloballyTrackableContract, TenantAwareContract
{
    use GloballyTrackable;
    use TenantAware;
}

This creates a Customer entity that will track the tenant information. To save typing this uses the built-in trait. A corresponding repository will need to be created along with the Doctrine mapping file. Here is an example yaml file:

App\Entity\Customer:
    type: entity
    table: customers
    repositoryClass: App\Repository\CustomerRepository

    uniqueConstraints:
        uniq_customers_uuid:
            columns: [ uuid ]

    id:
        id:
            type: bigint
            generator:
                strategy: auto

    fields:
        uuid:
            type: guid

        tenantOwnerId:
            type: integer

        tenantCreatorId:
            type: integer

        name:
            type: string
            length: 255

        createdBy:
            type: string
            length: 36

        updatedBy:
            type: string
            length: 36

        createdAt:
            type: datetime

        updatedAt:
            type: datetime

Tenant Aware Repositories

Note: applies to Doctrine only.

Tenant aware repositories simply wrap an existing entity repository with the standard repository interface. They should be defined and created as we actually want to be able to inject these as dependency and set them up in the container.

First you will need to create an App level TenantAwareRepository that extends:

  • Somnambulist\Tenancy\Repositories\TenantAwareRepository

For example:

namespace App\Repository;

use Somnambulist\Tenancy\Repositories\TenantAwareRepository;

class AppTenantAwareRepository extends TenantAwareRepository
{

}

Provided you don't have a custom security model, this should be good to extend again as a namespaced "tenant" repository for our customer:

namespace App\Repository\TenantAware;

use App\Repository\AppTenantAwareRepository;

class CustomerRepository extends AppTenantAwareRepository
{

}

Now, the config/tenancy.php can be updated to add a repository config definition so this class will be automatically available in the container.

Note: this step presumes the standard repository is already mapped to the container using the repository class as the key.

[
    'repository' => \App\Repository\TenantAware\CustomerRepository::class,
    'base'       => \App\Repository\CustomerRepository::class,
    //'alias'      => 'app.repository.tenant_aware_customer', // optionally alias
    //'tags'       => ['repository', 'tenant_aware'], // optionally tag
],