Early identification
Early identification refers to identifying tenants (and initializing tenancy) before the controller is instantiated.
Simple applications will not need this, but you might need this if you’re:
- adding Tenancy for Laravel to an existing application that has a lot of controllers that use constructor dependency injection
- integrating with packages that use constructor dependency injection in their controllers
Luckily, in Tenancy v4 we’ve abstracted away all of the complexity that comes with early identification, so solving this problem is easy.
That said, you will need a perfect understanding of how this works to make sure you implement this properly and avoid reading/writing data in the wrong context.
The problem
Controller constructors are executed ahead of route-level middleware, therefore any controllers that inject “service”-type objects which read from the database/cache/similar will incorrectly use the central context even if identification middleware is applied on the route.
The code above would always use the central API key for the cloud service, even if you’d be changing services.images.api_key
upon tenant identification.
This is because ImageService
was injected in the controller’s constructor, which Laravel executed before tenancy was initialized. Laravel does this because it supports specifying middleware in controller constructors, which means that the constructor needs to be instantiated first to get the full list of middleware that should be used on the route.
Solutions
There are three solutions to this problem.
1. Not using constructor injection
Laravel lets you inject dependencies in route actions. Route actions are always called in the right context (i.e. tenant context if there’s tenancy middleware on the route), so simply moving the dependency injection there fixes the problem.
2. Using Laravel’s native solution
Since Laravel 9.35/9.36, the core logic that causes this problem has been improved. The Laravel’s native early identification section provides a detailed explanation of what was changed and how you can apply this in your project.
In short, if your controller implements the Illuminate\Routing\Controllers\HasMiddleware
interface, or is not an instance of Illuminate\Routing\Controller
, it will not be instantiated ahead of route-level middleware.
3. Using kernel identification
This is the most robust solution to the problem. Our package will do all of the heavy lifting for you, but you will need to apply middleware differently, and will need to use different middleware.
This approach is especially useful when you cannot change the application code, either because there’s way too much logic to change (e.g. too many controllers to modify to use Laravel’s native solution), or because you’re integrating with a third-party pacakage (this is the main use case for kernel identification).
See the Kernel identification section below for a detailed explanation of how it works.
Laravel’s native early identification
Since Laravel 9.35, route-level middleware will be executed ahead of controller constructors if the controller implements the Illuminate\Routing\Controllers\HasMiddleware
interface or is not an instance of Illuminate\Routing\Controller
.
In other words: the $this->controllerDispatcher()->getMiddleware
logic (what causes dependency injection to run too early) would only be executed if the middleware does NOT implement HasMiddleware
and IS an instance of Illuminate\Routing\Controller
.
And since Laravel 9.36, Laravel looks for a getMiddleware()
method (as opposed to checking if the controller is an instance of Illuminate\Routing\Controller
). This method is still part of Illuminate\Routing\Controller
, so the condition evaluates as true
either way, but this change is still worth mentioning for completion.
In summary: since Laravel 9.35/9.36, route-level middleware will be executed correctly, before controller constructors, assuming:
- The controller implements
HasMiddleware
, or - The controller is not an instance of
Illuminate\Routing\Controller
Therefore: If you use pure classes as controllers, or implement the HasMiddleware
interface, your controller constructors will work fine and you don’t need to use kernel identification.
Kernel identification
Kernel identification refers to using your identification middleware in the Http Kernel’s global $middleware
stack, as opposed to route-level middleware.
Usage
Open App\Http\Kernel
and add the middleware you want to use to the $middleware
array. Keep in mind that you can only use one identification approach/middleware in the kernel stack.
With this change, the specified middleware (in our example InitializeTenancyByDomain
, but you can use any other tenancy middleware the package provides) will execute on every request, long before Laravel even figures out which controller to use.
But of course we don’t want to initialize tenancy on every request, since we also have central routes. So we need some way to distinguish between central and tenant routes. The following sections will cover that.
Route context flags
Since the middleware will execute even on requests you make on yourapp.com
(say this is your central domain), we need to tell the package to not initialize tenancy on those requests.
This can be achieved by adding the central
middleware1 to your route:
The opposite of this is the tenant
flag, which tells the package that it should initialize tenancy on a given route (or route group):
You might be asking, if both central
and tenant
should be defined, what happens if you define neither?
Default route mode
Tenancy has a config key called default_route_mode
. This indicates what should happen if no flag is provided on a route.
If the value is RouteMode::CENTRAL
, routes will be treated as central by default and tenancy will not be initialized on them, even if the identification middleware is in the Http Kernel. You will need to apply the tenant
flag on a route (or route group) for tenancy to be initialized when the route is visited.
If the value is RouteMode::TENANT
, routes will be treated as tenant/tenancy will be initialized by default. If you’ll want some routes to be central (= tenancy not being initialized), you’ll need to apply the central
middleware. For example, if you wanted routes/web.php
to be central, you’d need to wrap the contents of that file in Route::group('central')
.
If the value is RouteMode::UNIVERSAL
, routes will be usable as both central and tenant. This means tenancy will be initialized if the “tenant value” is provided in some way.
- With domain identification, this means making a request on a tenant domain (domain not specified in the
central_domains
list in the tenancy config) - With path identification, this means making a request to
/tenant123/foo
instead of/foo
2 - With request data identification, this means providing the tenant in any of the supported formats (
X-Tenant
header,?tenant
query string, and others)
Intuitive defaults
By default, the package uses the RouteMode::CENTRAL
route mode and marks routes/tenant.php
as tenant
(see App\Providers\TenancyServiceProvider@mapRoutes
, ->middleware('tenant')
call).
This means that routes in tenant.php
are treated as tenant routes, and routes in any other files are treated as central.
In other words, there’s no need to add the central
flag to individual routes, as RouteMode::CENTRAL
is used by default. And there’s no need to add the tenant
flag to your routes in routes/tenant.php
, as that entire file has the tenant
middleware applied on it.
Integrating with packages
If you’re integrating with third-party packages, you may need a different default route mode. This depends on the level of control you have over the package’s routes.
Say that you’re integrating a package that needs early identification (because it uses Illuminate\Routing\Controller
-based controllers that inject some service in their constructor).
If the package lets you configure the middleware it uses on its routes, you can simply add tenant
to that list.
If the package doesn’t let you do that, you’ll need to use the RouteMode::TENANT
default route mode (so that routes that don’t have tenant
/central
/universal
explicitly defined are considered tenant). This will make Tenancy consider the package’s routes to be tenant routes and tenancy will be initialized when they’re used. You’ll just have to remember to add the central
flag to any central routes (e.g. routes/web.php
and routes/api.php
) since tenancy will consider all routes that don’t have any explicit flag to be tenant.
And if you want to use the package’s routes in both the central part of your app and the tenant part of your app, you can use RouteMode::UNIVERSAL
. That way tenancy will be initialized whenever the tenant is specified in some way (that matches what the identification middleware you added to the kernel looks for, as explained in the Default route mode section).
Footnotes
-
Under the hood, this is simply an empty middleware group.
central
,tenant,
anduniversal
are all defined like this, and they are reserved middleware by the package. Therefore you shouldn’t define them in your app to make sure you don’t override them. ↩ -
With path identification, you’ll also need to use the
CloneRoutesAsTenant
action to create a copy of the central route prefixed with/{tenant}
. ↩