leek/filament-subtenant-scope
Adds second-level tenancy to Filament panels with a top-nav dropdown that scopes all Eloquent queries to a sub-tenant (region, location, department, etc.). Persists via session/URL and auto-filters resources, widgets, badges, and global search.
Installation
composer require leek/filament-subtenant-scope
Add the package to your PanelProvider:
use Leek\FilamentSubtenantScope\FilamentSubtenantScopePlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(FilamentSubtenantScopePlugin::make());
}
Configure Vite Theme
Add this to your panel’s CSS file (e.g., resources/css/filament/panel.css):
@source '../../../../vendor/leek/filament-subtenant-scope/resources/views/**/*.blade.php';
Rebuild assets:
npm run dev
First Use Case
Define a subtenant model (e.g., ServiceArea) and attach it to a panel:
use Leek\FilamentSubtenantScope\Concerns\ScopesSubtenants;
class ServiceArea extends Model
{
use ScopesSubtenants;
}
Register the subtenant in your PanelProvider:
FilamentSubtenantScopePlugin::make()
->subtenants([
'service_area' => ServiceArea::class,
]);
Subtenant Model Setup
Use the ScopesSubtenants trait on your subtenant model (e.g., ServiceArea, Region):
class ServiceArea extends Model
{
use ScopesSubtenants;
public static function getGlobalSearchResultOptions(): array
{
return [
'label' => 'Service Area',
'query' => fn (string $search) => self::where('name', 'like', "%{$search}%"),
];
}
}
Panel Integration Register subtenants in the plugin configuration:
FilamentSubtenantScopePlugin::make()
->subtenants([
'service_area' => ServiceArea::class,
'region' => Region::class,
])
->defaultSubtenant('service_area'); // Optional: Set a default
Dynamic Scoping
The plugin automatically scopes all Eloquent queries in the panel. No manual ->where() calls needed.
Example: Queries for User, Post, or custom models will auto-filter by the selected subtenant.
URL Persistence
The selected subtenant is stored in the URL (e.g., ?subtenant=service_area&subtenant_id=1).
Use SubtenantScope::getCurrentSubtenant() to access the active subtenant in code.
Global Search Integration
Subtenants appear in Filament’s global search dropdown if getGlobalSearchResultOptions() is defined.
Disable scoping for specific models:
class Post extends Model
{
public static function shouldScopeSubtenants(): bool
{
return request()->has('exclude_posts');
}
}
Extend the default scoping logic:
class ServiceArea extends Model
{
use ScopesSubtenants;
public static function scopeSubtenantQuery(Builder $query, ?self $subtenant): Builder
{
return $query->where('service_area_id', $subtenant?->id)
->orWhereNull('service_area_id'); // Include unassigned records
}
}
Combine with Filament’s built-in tenancy:
// In a resource's query builder:
$query->where('company_id', auth()->user()->company_id)
->when($subtenant = SubtenantScope::getCurrentSubtenant(), fn ($q) => $q->where('service_area_id', $subtenant->id));
Use the subtenant in widgets:
use Leek\FilamentSubtenantScope\Facades\SubtenantScope;
class ServiceAreaStats extends Widget
{
protected function getData(): array
{
$subtenant = SubtenantScope::getCurrentSubtenant();
return [
'active_users' => User::where('service_area_id', $subtenant?->id)->count(),
];
}
}
Asset Compilation
@source is added to your panel’s CSS file and assets are rebuilt (npm run dev or npm run build).Query Overrides
scopeSubtenantQuery() not working.public static function scopeSubtenantQuery(Builder $query, ?self $subtenant): Builder
URL Conflicts
getRouteParameters() in custom resources:
public static function getRouteParameters(): array
{
return array_diff(parent::getRouteParameters(), ['subtenant', 'subtenant_id']);
}
Caching Quirks
->fresh() in widget queries:
User::where(...)->fresh()->count();
Model Polymorphism
$query->whereHas('morphTarget', fn ($q) => $q->where('service_area_id', $subtenant->id));
Inspect Active Subtenant Dump the current subtenant in a Tinker session:
php artisan tinker
>>> \Leek\FilamentSubtenantScope\Facades\SubtenantScope::getCurrentSubtenant();
Check Query Scoping
Enable Laravel’s query logging in .env:
DB_ENABLE_QUERY_LOG=1
Then inspect the generated SQL in storage/logs/laravel.log.
Validate Plugin Registration
Ensure the plugin is registered in PanelProvider before defining resources:
// Correct order:
->plugin(FilamentSubtenantScopePlugin::make())
->resources([UserResource::class]);
Custom UI Override the dropdown blade template:
FilamentSubtenantScopePlugin::make()
->dropdownView('filament-subtenant-scope::dropdown.custom');
Dynamic Subtenant Sources Fetch subtenants dynamically (e.g., from an API):
class ServiceArea extends Model
{
public static function getSubtenantOptions(): array
{
return cache()->remember('service_area_options', now()->addHours(1), fn () => [
['id' => null, 'label' => 'All Areas'],
...self::all()->pluck('name', 'id')->toArray(),
]);
}
}
Permission-Based Visibility Hide the dropdown for unauthorized users:
FilamentSubtenantScopePlugin::make()
->canSee(fn () => auth()->user()->can('manage_subtenants'));
Multi-Select Support Extend for multiple subtenants (requires custom logic):
// Example: Allow selecting multiple service areas
public static function scopeSubtenantQuery(Builder $query, ?array $subtenants): Builder
{
return $query->whereIn('service_area_id', $subtenants ?? [null]);
}
How can I help you explore Laravel packages today?