Skip to content

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.

// Imagine that this represents some cloud service for storing images
class ImageService
{
public string $apiKey;
public function __construct(Repository $config)
{
$this->apiKey = $config->get('services.images.api_key');
};
public function store($image): string
{
// Pseudocode
$response = Http::put('https://cloud-service.com/upload', [
'image' => $image,
'api_key' => $this->apiKey,
]);
return $response->json('url');
}
}
class PostController extends Controller
{
public function __construct(
ImageService $images,
) {}
public function update(Request $request, Post $post)
{
if ($request->has('image')) {
$post->update([
'image_url' => $this->images->store($image),
]);
}
}
}
// Tenancy middleware included
Route::middleware(InitializeTenancyByDomain::class)
->post('/posts/update', [PostController::class, 'update']);

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.

class PostController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
}

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.

class PostController extends Controller
{
public function __construct(
ImageService $images,
) {}
public function update(Request $request, Post $post)
public function update(Request $request, Post $post, ImageService $images)
{
if ($request->has('image')) {
$post->update([
'image_url' => $this->images->store($image),
'image_url' => $images->store($image),
]);
}
}
}

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.

// Evil code that caused the controller to be instantiated
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $this->getControllerMethod()
);
[$controllerClass, $controllerMethod] = [
$this->getControllerClass(),
$this->getControllerMethod(),
];
// Code that skips instantiating the controller
if (is_a($controllerClass, HasMiddleware::class, true)) {
return $this->staticallyProvidedControllerMiddleware(
$controllerClass, $controllerMethod
);
}
if (is_a($controllerClass, Controller::class, true)) {
// Evil code moved here
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $controllerMethod
);
}

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.

if (is_a($controllerClass, Controller::class, true)) {
if (method_exists($controllerClass, 'getMiddleware')) {

In summary: since Laravel 9.35/9.36, route-level middleware will be executed correctly, before controller constructors, assuming:

  1. The controller implements HasMiddleware, or
  2. 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.

class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
];
}

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:

// Tenancy will not be initialized here
Route::middleware('central')
->get('/central-route', CentralController::class);

The opposite of this is the tenant flag, which tells the package that it should initialize tenancy on a given route (or route group):

// Tenancy will not be initialized here
Route::middleware('central')
->get('/central-route', CentralController::class);
// Tenancy will be initialized here
Route::middleware('tenant')
->get('/tenant-route', TenantController::class);

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

  1. Under the hood, this is simply an empty middleware group. central, tenant, and universal 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.

  2. With path identification, you’ll also need to use the CloneRoutesAsTenant action to create a copy of the central route prefixed with /{tenant}.