Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Subtenant Scope Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. 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());
    }
    
  2. 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
    
  3. 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,
        ]);
    

Implementation Patterns

Core Workflow

  1. 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}%"),
            ];
        }
    }
    
  2. 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
    
  3. 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.

  4. 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.

  5. Global Search Integration Subtenants appear in Filament’s global search dropdown if getGlobalSearchResultOptions() is defined.


Advanced Patterns

Conditional Scoping

Disable scoping for specific models:

class Post extends Model
{
    public static function shouldScopeSubtenants(): bool
    {
        return request()->has('exclude_posts');
    }
}

Custom Query Modifiers

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
    }
}

Multi-Level Tenancy

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));

Widget Filtering

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(),
        ];
    }
}

Gotchas and Tips

Pitfalls

  1. Asset Compilation

    • Issue: Plugin styles/classes missing after installation.
    • Fix: Ensure @source is added to your panel’s CSS file and assets are rebuilt (npm run dev or npm run build).
  2. Query Overrides

    • Issue: Custom scopeSubtenantQuery() not working.
    • Fix: Verify the method signature matches exactly:
      public static function scopeSubtenantQuery(Builder $query, ?self $subtenant): Builder
      
  3. URL Conflicts

    • Issue: Subtenant query params clashing with Filament’s built-in filters.
    • Fix: Exclude the subtenant route from Filament’s getRouteParameters() in custom resources:
      public static function getRouteParameters(): array
      {
          return array_diff(parent::getRouteParameters(), ['subtenant', 'subtenant_id']);
      }
      
  4. Caching Quirks

    • Issue: Scoped queries returning stale data in widgets.
    • Fix: Clear the cache or use ->fresh() in widget queries:
      User::where(...)->fresh()->count();
      
  5. Model Polymorphism

    • Issue: Subtenant scoping not applying to polymorphic relations.
    • Fix: Manually scope polymorphic queries:
      $query->whereHas('morphTarget', fn ($q) => $q->where('service_area_id', $subtenant->id));
      

Debugging Tips

  1. Inspect Active Subtenant Dump the current subtenant in a Tinker session:

    php artisan tinker
    >>> \Leek\FilamentSubtenantScope\Facades\SubtenantScope::getCurrentSubtenant();
    
  2. 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.

  3. Validate Plugin Registration Ensure the plugin is registered in PanelProvider before defining resources:

    // Correct order:
    ->plugin(FilamentSubtenantScopePlugin::make())
    ->resources([UserResource::class]);
    

Extension Points

  1. Custom UI Override the dropdown blade template:

    FilamentSubtenantScopePlugin::make()
        ->dropdownView('filament-subtenant-scope::dropdown.custom');
    
  2. 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(),
            ]);
        }
    }
    
  3. Permission-Based Visibility Hide the dropdown for unauthorized users:

    FilamentSubtenantScopePlugin::make()
        ->canSee(fn () => auth()->user()->can('manage_subtenants'));
    
  4. 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]);
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky