Resolution hooks define the points in Laravel’s request lifecycle where tenant identification can occur. Each hook represents a specific moment when Sprout can attempt to resolve the current tenant.
Sprout supports multiple resolution hooks to accommodate different identification strategies. Some strategies (like subdomain or path) can resolve very early, while others (like session) require Laravel services that aren’t available until later in the lifecycle.
Request
│
▼
┌─────────────────────────┐
│ Route Matching │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ RouteMatched Event │
│ ┌───────────────────┐ │
│ │ Routing Hook │ │ ← Route and parameters available
│ │ (if enabled) │ │ Best for most resolvers
│ └───────────────────┘ │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Middleware │
│ ┌───────────────────┐ │
│ │ Middleware Hook │ │ ← Sessions, auth available
│ │ (if enabled) │ │ Required for session resolver
│ └───────────────────┘ │
│ │
│ Tenant required here │ ← Exception if no tenant
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Controller/Action │
└─────────────────────────┘
ResolutionHook::Booting
Occurs during the booting of service providers.
Note: This hook exists in the enum but is not currently used anywhere in the framework. It’s reserved for potential future use cases where tenant resolution needs to happen before routing.
ResolutionHook::Routing
Occurs when Laravel’s RouteMatched event fires, immediately after the router has matched the request to a route.
Implementation: When this hook is enabled, SproutServiceProvider registers IdentifyTenantOnRouting as a listener
for the RouteMatched event. The listener checks if the matched route has Sprout’s tenanted middleware (
sprout.tenanted), and if so, attempts resolution.
When it fires:
What’s available:
Route object and its parametersWhat’s NOT available:
Best for:
This is the recommended hook for most applications because:
ResolutionHook::Middleware
Occurs during the route middleware stack, as part of Sprout’s SproutTenantContextMiddleware.
When it fires:
What’s available:
What’s NOT available:
Best for:
Tenant Enforcement:
After the Middleware hook completes (whether resolution was attempted or not), SproutTenantContextMiddleware enforces
that a tenant exists. If no tenant has been resolved by any enabled hook, a NoTenantFoundException is thrown:
if (! $this->sprout->hasCurrentTenancy() || ! $this->sprout->getCurrentTenancy()?->check()) {
throw NoTenantFoundException::make($resolverName, $tenancyName);
}
This means the Middleware hook is effectively the “last chance” for tenant resolution on tenanted routes.
Hooks are enabled in the Sprout configuration:
// config/sprout/core.php
'hooks' => [
\Sprout\Core\Support\ResolutionHook::Routing,
\Sprout\Core\Support\ResolutionHook::Middleware,
],
The order hooks appear in the configuration array does not matter — each hook fires at its predetermined point in the lifecycle. The configuration simply controls which hooks are enabled.
Default configuration: Both Routing and Middleware are enabled by default, which covers most use cases.
| Hook | Triggered By | Handler Class |
|---|---|---|
| Routing | RouteMatched event |
IdentifyTenantOnRouting |
| Middleware | Middleware execution | SproutTenantContextMiddleware |
Registered as an event listener in SproutServiceProvider:
if ($this->sprout->supportsHook(ResolutionHook::Routing)) {
$events->listen(RouteMatched::class, IdentifyTenantOnRouting::class);
}
The listener:
sprout.tenanted or sprout.tenanted.optionalResolutionHelper::handleResolution() with ResolutionHook::RoutingThe middleware (aliased as sprout.tenanted):
ResolutionHelper::handleResolution()NoTenantFoundException if no tenant resolvedAn alternative middleware (aliased as sprout.tenanted.optional) that does not throw an exception if no tenant is
found. Useful for routes that can work both with and without a tenant context.
Both hooks delegate to ResolutionHelper::handleResolution(), which orchestrates the resolution process:
ResolutionHelper::handleResolution(
$request,
$hook,
$sprout,
$resolverName,
$tenancyName
);
The resolution flow:
$sprout->setCurrentHook($hook) records which hook is being processed$tenancy->check()), skip resolution!$resolver->canResolve(...)), skip resolution$sprout->setCurrentTenancy($tenancy)$resolver->resolveFromRoute()$resolver->resolveFromRequest()$tenancy->resolvedVia($resolver)->resolvedAt($hook)$tenancy->identify($identity) loads the tenant via the provider// Resolver decides if it can work at this hook
public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $hook): bool
{
// Session resolver can only work during Middleware hook
if ($this->driver === 'session') {
return $hook === ResolutionHook::Middleware;
}
return true;
}
For resolvers that implement IdentityResolverUsesParameters (subdomain, path, domain), the resolution helper checks if
the route has the expected parameter:
if (
$resolver instanceof IdentityResolverUsesParameters
&& $route !== null
&& $route->hasParameter($resolver->getRouteParameterName($tenancy))
) {
$identity = $resolver->resolveFromRoute($route, $tenancy, $request);
$route->forgetParameter($resolver->getRouteParameterName($tenancy));
} else {
$identity = $resolver->resolveFromRequest($request, $tenancy);
}
The parameter is removed from the route after resolution (forgetParameter) so it doesn’t appear in controller method
signatures or route parameter arrays.
Not all resolvers work at all hooks:
| Resolver | Routing | Middleware | Notes |
|---|---|---|---|
| Subdomain | ✓ | ✓ | Works at either |
| Path | ✓ | ✓ | Works at either |
| Header | ✓ | ✓ | Works at either |
| Cookie | ✓ | ✓ | Works at either |
| Session | ✗ | ✓ | Requires session middleware |
| Domain (Canopy) | ✓ | ✓ | Works at either |
When using the Middleware hook with the session resolver, you may need to adjust middleware priority to ensure
Sprout’s middleware runs after StartSession:
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToPriorityList(
\Illuminate\Session\Middleware\StartSession::class,
\Sprout\Core\Http\Middleware\SproutTenantContextMiddleware::class
);
});
The middleware is also available via the alias sprout.tenanted.
Warning: Only adjust middleware priority if you’re using the session resolver. Changing priority when using other resolvers can cause issues with the session service override and other Sprout features.
The tenancy tracks where resolution occurred:
// Get the hook where the tenant was resolved
$hook = $tenancy->hook(); // ResolutionHook::Routing
// Check if resolution happened
if ($tenancy->wasResolved()) {
$resolver = $tenancy->resolver();
$hook = $tenancy->hook();
}
Sprout also tracks the current hook during request processing:
// During request lifecycle
$currentHook = sprout()->getCurrentHook();
// Check if we're at a specific hook
if (sprout()->isCurrentHook(ResolutionHook::Routing)) {
// ...
}
Most applications should use the default configuration:
'hooks' => [
ResolutionHook::Routing,
ResolutionHook::Middleware,
],
With a resolver that works at Routing (subdomain, path, header, etc.), the tenant is resolved early and available
throughout the middleware stack.
For applications using session-based tenant identification:
'hooks' => [
ResolutionHook::Middleware,
],
Or keep both enabled — the session resolver will simply skip the Routing hook via its canResolve() method.
For APIs that accept tenant identification via header or cookie:
'hooks' => [
ResolutionHook::Routing,
],
The Middleware hook isn’t needed since neither resolver requires session data.