Skip to content

Conversation

@binaryfire
Copy link
Contributor

@binaryfire binaryfire commented Nov 15, 2025

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:

  • Allows custom tenant resolution middleware to be reused by Filament
  • Saves 1 db query per request if tenant has already been resolved upstream
  • Maintains all existing security checks (canAccessTenant, route validation)
  • Backward compatible - no impact when tenant isn't pre-set

@danharrin danharrin added the bug Something isn't working label Nov 17, 2025
@danharrin danharrin added this to the v4 milestone Nov 17, 2025
@danharrin danharrin requested a review from Copilot November 17, 2025 09:19
Copy link
Member

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?

Copy link
Contributor Author

@binaryfire binaryfire Nov 18, 2025

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:

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@binaryfire binaryfire Nov 18, 2025

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?

Copy link
Contributor Author

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

@github-project-automation github-project-automation bot moved this from Todo to In Progress in Roadmap Nov 17, 2025
Copilot finished reviewing on behalf of danharrin November 17, 2025 09:23
Copy link
Contributor

Copilot AI left a 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) {
Copy link

Copilot AI Nov 17, 2025

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.

Suggested change
if ($currentTenant && $currentTenantKey === $tenantRouteKey) {
if ($currentTenant && (string) $currentTenantKey === (string) $tenantRouteKey) {

Copilot uses AI. Check for mistakes.
Copy link
Member

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;
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working pending changes

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants