-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Optimize IdentifyTenant to reuse existing tenant if set
#18513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.x
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you maybe just replace this middleware in the container? Idk, the route key check makes me feel weird. What if the tenant is identified based on domain, for example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danharrin This works for both path and domain-based tenancy because they both use route parameters. Domain patterns like {tenant:slug}.example.com create a route parameter called tenant that gets extracted via $request->route()->parameter('tenant'), just like path-based routing.
From packages/panels/routes/web.php:
if (filled($tenantDomain)) {
$routeGroup->domain($tenantDomain); // e.g., {tenant:slug}.example.com or {tenant:domain}
} else {
$routeGroup->prefix(/* ... */ . '{tenant' . /* ... */ . '}'); // e.g., /admin/{tenant}
}Source:
filament/packages/panels/routes/web.php
Line 173 in 6fb113f
| if (filled($tenantDomain)) { |
I've renamed the variable to $tenantRouteParameter to make it clearer. I think this is a pretty straightforward change - the logic is exactly the same (the tenant route param is already what's used for both path and domain based resolution). This just checks if there's already a tenant set, and providing it matches the route param (a failsafe in case the wrong tenant was set upstream), uses it instead of hitting the DB again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, I'm still weird about it to be honest. Does replacing the middleware in the container work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danharrin No worries. Yeah I can swap it in the container. I'll have to keep an eye on it in case the core tenancy logic changes but that's fine. Just thought it'd be useful for anyone using a global tenancy package like archtechx/tenancy or spatie/laravel-multitenancy.
Take Spatie's package for example. Their middleware resolves the tenant once per request and memoizes it in the container:
// Spatie's Multitenancy class
protected function determineCurrentTenant(): void
{
$tenant = $tenantFinder->findForRequest($request); // DB query
$tenant->makeCurrent(); // Stores in container
}
// Later retrieval is memoized
public static function current(): ?static
{
return app($containerKey); // No DB query - returns cached instance
}With this PR, you could add this to some custom middleware:
public function handle(Request $request, Closure $next)
{
if ($spatieCurrentTenant = \Spatie\Multitenancy\Contracts\IsTenant::current()) {
\Filament\Facades\Filament::setTenant($spatieCurrentTenant);
}
return $next($request);
}Then Filament's IdentifyTenant sees the tenant is already set, verifies it matches the route param, and reuses it. 1 DB query total.
Without this PR, there's no way to reuse the already-resolved tenant - Filament will always query it again. 2 DB queries. The only workaround is swapping IdentifyTenant in the container in every app that uses both packages. Or accepting the extra query on every request.
But yeah, totally understand if you'd prefer people handle this themselves. Just seemed like a small change that'd be beneficial to others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danharrin Or what about an ->identifyTenant() panel method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danharrin Just pushed a new version that uses a panel method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR optimizes the IdentifyTenant middleware to avoid redundant database queries when a tenant has already been resolved by upstream middleware. If a tenant is already set via Filament::setTenant() and matches the route parameter, it is reused instead of being re-queried from the database.
Key Changes:
- Introduces logic to check if the current tenant matches the expected tenant from the route parameter
- Reuses the existing tenant when there's a match, avoiding a database query
- Maintains all existing security checks and validation
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ? $currentTenant?->getAttribute($slugAttribute) | ||
| : $currentTenant?->getRouteKey(); | ||
|
|
||
| if ($currentTenant && $currentTenantKey === $tenantRouteKey) { |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The strict comparison === may fail when comparing route parameters (typically strings) with model keys (potentially integers). For example, if getRouteKey() returns 1 (int) and $tenantRouteKey is '1' (string), the comparison will fail even though they represent the same tenant. Consider using loose comparison == to handle type coercion, or explicitly cast both values to strings for comparison: (string) $currentTenantKey === (string) $tenantRouteKey.
| if ($currentTenant && $currentTenantKey === $tenantRouteKey) { | |
| if ($currentTenant && (string) $currentTenantKey === (string) $tenantRouteKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, I'm still weird about it to be honest. Does replacing the middleware in the container work?
|
|
||
| protected ?string $tenantOwnershipRelationshipName = null; | ||
|
|
||
| protected ?Closure $tenantIdentifier = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please call this $resolveTenantUsing
| return $this; | ||
| } | ||
|
|
||
| public function identifyTenant(?Closure $callback): static |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please call this resolveTenantUsing()
| return $this->tenantRegistrationPage; | ||
| } | ||
|
|
||
| public function resolveTenantForRequest(string $key): Model |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please merge this into the existing getTenant() method so we don't need another
Applications that resolve tenants via custom middleware currently can't avoid Filament re-querying the same tenant from the db.
This PR checks if a tenant has already been set and matches the expected tenant id. If yes, the existing tenant is used. This allows using
Filament::setTenant()in custom upstream tenant resolution middleware to set the Filament tenant using an already resolved model.Benefits: