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
- in bootstrap/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
],