FilterableFilterable
Home
📦 Installation
  • Setting Up Filterable
  • Discover Command
  • Listing All Filters
  • Testing Filters
  • Inspecting Filterable Classes
  • Caching
GitHub
Home
📦 Installation
  • Setting Up Filterable
  • Discover Command
  • Listing All Filters
  • Testing Filters
  • Inspecting Filterable Classes
  • Caching
GitHub
  • Home
  • Introduction
  • Installation
  • Service Provider
  • How It Works
  • Engines

    • Invokable
    • Tree
    • Ruleset
    • Expression
  • Features

    • Lifecycle Hooks
    • Header-Driven Filter Mode
    • Auto Register Filterable Macro
    • Conditional Logic
    • Filter Aliases
    • Through callbacks
    • Auto Binding
    • Custom engines
    • Data Provisioning
  • Execution

    • Invoker
  • API Reference

    • Filterable
    • Filterable facade
    • Payload
    • Sorter
  • Caching

    • Overview
    • Getting Started
    • Strategies
    • Auto Invalidation
    • Cache Profiles
    • Scoping Cache
    • Monitoring Cached Items
    • API Reference
    • Examples
  • CLI

    • Setup Filterable
    • Discover Filters
    • Test Filter
    • List Filters
    • Inspect Filter
  • Exceptions
  • Event System
  • Profile Management
  • Profiler
  • Sorting
  • Authorization
  • Validation
  • Sanitization

Cache Scoping

What is Cache Scoping?

Cache scoping allows you to segment your cache by different contexts (users, tenants, organizations, etc.) to ensure that cached data is properly isolated and invalidated.

Overview

Without scoping, cached results might be shared across different users or tenants, leading to data leaks or incorrect results. Cache scoping solves this by adding contextual information to cache keys.

Benefits

  • Data Isolation: Keep user and tenant data separate
  • Precise Invalidation: Clear cache for specific users/tenants only
  • Multi-tenancy Support: Essential for SaaS applications
  • Security: Prevent data leaks between contexts

User Scoping

Basic User Scoping

Automatically scope cache by the authenticated user:

use App\Models\Post;

// Automatic user scoping
$posts = Post::filter()
    ->cache(3600)
    ->scopeByUser()  // Uses auth()->id()
    ->get();

// Each user gets their own cached result
// Cache keys:
// - filterable:post_filter:user:1:...
// - filterable:post_filter:user:2:...

Explicit User ID

Specify a user ID explicitly:

// Scope by specific user
$posts = Post::filter()
    ->cache(3600)
    ->scopeByUser(123)
    ->get();

Use Cases

Personalized Feeds

class FeedController
{
    public function index()
    {
        // Each user sees their own cached feed
        return Post::filter()
            ->cache(1800)
            ->scopeByUser()
            ->apply(['feed_type' => 'personalized'])
            ->get();
    }
}

User Dashboards

class DashboardController
{
    public function show()
    {
        $stats = DashboardStats::filter()
            ->cache(600)
            ->scopeByUser()
            ->get();

        return view('dashboard', compact('stats'));
    }
}

User Preferences

// Cache user-specific filtered results
$products = Product::filter()
    ->cache(3600)
    ->scopeByUser()
    ->apply([
        'category' => auth()->user()->favorite_category,
        'price_range' => auth()->user()->price_preference,
    ])
    ->get();

Tenant Scoping

Basic Tenant Scoping

Essential for multi-tenant applications:

use App\Models\Product;

// Scope by current tenant
$products = Product::filter()
    ->cache(3600)
    ->scopeByTenant(tenant()->id)
    ->get();

// Cache keys:
// - filterable:product_filter:tenant:acme:...
// - filterable:product_filter:tenant:globex:...

Integration with Tenant Packages

Stancl/Tenancy

use Stancl\Tenancy\Facades\Tenancy;

$orders = Order::filter()
    ->cache(3600)
    ->scopeByTenant(tenant('id'))
    ->apply(['status' => 'pending'])
    ->get();

Spatie Multi-Tenancy

use Spatie\Multitenancy\Models\Tenant;

$data = Model::filter()
    ->cache(3600)
    ->scopeByTenant(Tenant::current()->id)
    ->get();

Use Cases

Tenant Dashboards

class TenantDashboardController
{
    public function index()
    {
        $metrics = Metric::filter()
            ->cache(600)
            ->scopeByTenant(tenant()->id)
            ->get();

        return view('tenant.dashboard', compact('metrics'));
    }
}

Tenant Reports

// Each tenant has their own cached reports
$report = SalesReport::filter()
    ->cache(7200)
    ->scopeByTenant(tenant()->id)
    ->apply(['year' => 2024])
    ->get();

Tenant Settings

// Cache tenant-specific configuration
$settings = Setting::filter()
    ->cacheForever()
    ->scopeByTenant(tenant()->id)
    ->get();

Custom Scoping

Single Custom Scope

Add any custom scope to your cache:

use App\Models\Document;

// Scope by organization
$documents = Document::filter()
    ->cache(3600)
    ->scopeBy('organization', $organizationId)
    ->get();

// Scope by department
$reports = Report::filter()
    ->cache(3600)
    ->scopeBy('department', $departmentId)
    ->get();

Multiple Custom Scopes

Chain multiple scopes for fine-grained control:

// Scope by organization and department
$data = Model::filter()
    ->cache(3600)
    ->scopeBy('organization', $orgId)
    ->scopeBy('department', $deptId)
    ->scopeBy('region', $region)
    ->get();

// Cache key includes all scopes:
// filterable:model_filter:organization:123:department:456:region:west:...

Batch Scopes

Set multiple scopes at once:

$filters = Request::filter()
    ->cache(3600)
    ->withScopes([
        'organization' => $orgId,
        'department' => $deptId,
        'region' => $region,
        'team' => $teamId,
    ])
    ->get();

Use Cases

Multi-Level Organizations

class OrganizationController
{
    public function departmentData($orgId, $deptId)
    {
        return Data::filter()
            ->cache(1800)
            ->scopeBy('organization', $orgId)
            ->scopeBy('department', $deptId)
            ->get();
    }
}

Geographic Segmentation

// Different cache for each region
$products = Product::filter()
    ->cache(3600)
    ->scopeBy('country', $country)
    ->scopeBy('region', $region)
    ->apply(['status' => 'active'])
    ->get();

Temporal Scoping

// Different cache for different time periods
$analytics = Analytics::filter()
    ->cache(3600)
    ->scopeBy('period', 'monthly')
    ->scopeBy('year', 2024)
    ->scopeBy('month', 3)
    ->get();

Combined Scoping

User + Tenant

Perfect for SaaS applications where users belong to tenants:

// Scope by both tenant and user
$personalData = UserData::filter()
    ->cache(1800)
    ->scopeByTenant(tenant()->id)
    ->scopeByUser()
    ->get();

// Cache key:
// filterable:user_data_filter:tenant:acme:user:123:...

User + Custom Scopes

// User-specific data within an organization
$projects = Project::filter()
    ->cache(3600)
    ->scopeByUser()
    ->scopeBy('organization', $orgId)
    ->scopeBy('team', $teamId)
    ->get();

Multiple Context Layers

class MultiContextFilter
{
    public function getData($userId, $tenantId, $orgId, $role)
    {
        return Model::filter()
            ->cache(3600)
            ->scopeByUser($userId)
            ->scopeByTenant($tenantId)
            ->scopeBy('organization', $orgId)
            ->scopeBy('role', $role)
            ->get();
    }
}

Scope Patterns

Middleware-based Scoping

Automatically apply scopes via middleware:

// app/Http/Middleware/ApplyCacheScopes.php
class ApplyCacheScopes
{
    public function handle($request, Closure $next)
    {
        // Store scopes in a service
        app(FilterableCacheManager::class)
            ->addScope('tenant', tenant()->id)
            ->addScope('user', auth()->id());

        return $next($request);
    }
}
// Use in routes
Route::middleware(['auth', 'tenant', ApplyCacheScopes::class])
    ->group(function () {
        // All filterable queries here will be scoped automatically
    });

Base Filter with Default Scoping

abstract class TenantScopedFilter extends Filterable
{
    use HasFilterableCache;

    protected function applyCache(int $ttl = 3600)
    {
        return $this->cache($ttl)
            ->scopeByTenant(tenant()->id);
    }
}
// Usage
class ProductFilter extends TenantScopedFilter
{
    public function apply(array $filters = []): Builder
    {
        return parent::apply($filters)
            ->applyCache();  // Automatically tenant-scoped
    }
}

Service Provider Integration

// app/Providers/FilterableCacheServiceProvider.php
class FilterableCacheServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Auto-apply tenant scope to all filterable caches
        Filterable::resolving(function ($filter) {
            if (auth()->check() && tenant()) {
                $filter->cache(3600)
                    ->scopeByTenant(tenant()->id)
                    ->scopeByUser();
            }
        });
    }
}

Cache Invalidation with Scopes

Flush User-specific Cache

use Kettasoft\Filterable\Caching\FilterableCacheManager;

// Clear all cache for a specific user
$manager = FilterableCacheManager::getInstance();
$manager->addScope('user', $userId)
    ->flushByTags(['posts']);

Flush Tenant-specific Cache

// Clear all cache for a tenant
$manager = FilterableCacheManager::getInstance();
$manager->addScope('tenant', $tenantId)
    ->flushByTags(['products', 'orders']);

Selective Invalidation

// Clear specific scope combinations
function clearUserTenantCache($userId, $tenantId, array $tags)
{
    $manager = FilterableCacheManager::getInstance();
    $manager->addScope('user', $userId)
        ->addScope('tenant', $tenantId)
        ->flushByTags($tags);
}

clearUserTenantCache(123, 'acme', ['dashboard', 'reports']);

Real-world Examples

SaaS Platform

// Multi-level scoping for SaaS
class SaaSDataController
{
    public function tenantUserData()
    {
        return Data::filter()
            ->cache(1800)
            ->scopeByTenant(tenant()->id)
            ->scopeByUser()
            ->scopeBy('subscription_tier', auth()->user()->tier)
            ->get();
    }

    public function organizationData($orgId)
    {
        return OrganizationData::filter()
            ->cache(3600)
            ->scopeByTenant(tenant()->id)
            ->scopeBy('organization', $orgId)
            ->get();
    }
}

Multi-Organization Platform

// User can belong to multiple organizations
class OrganizationDataController
{
    public function index($organizationId)
    {
        // Scope by current organization
        return Model::filter()
            ->cache(3600)
            ->scopeBy('organization', $organizationId)
            ->scopeByUser()  // User-specific within org
            ->get();
    }
}

Regional E-commerce

// Different pricing/products per region
class ProductController
{
    public function catalog()
    {
        $region = request()->header('X-Region', 'US');
        $currency = request()->header('X-Currency', 'USD');

        return Product::filter()
            ->cache(3600)
            ->scopeBy('region', $region)
            ->scopeBy('currency', $currency)
            ->scopeBy('language', app()->getLocale())
            ->apply(['status' => 'active'])
            ->get();
    }
}

Advanced Patterns

Dynamic Scope Resolution

class SmartScopeFilter extends Filterable
{
    protected function resolveScopes(): array
    {
        $scopes = [];

        if (auth()->check()) {
            $scopes['user'] = auth()->id();
        }

        if (tenant()) {
            $scopes['tenant'] = tenant()->id;
        }

        if (session()->has('organization')) {
            $scopes['organization'] = session('organization');
        }

        return $scopes;
    }

    public function apply(array $filters = []): Builder
    {
        $query = parent::apply($filters)->cache(3600);

        foreach ($this->resolveScopes() as $key => $value) {
            $query->scopeBy($key, $value);
        }

        return $query;
    }
}

Scope Inheritance

// Base filter with default scopes
abstract class ScopedFilter extends Filterable
{
    protected array $defaultScopes = [];

    protected function getScopes(): array
    {
        return array_merge($this->defaultScopes, [
            'environment' => app()->environment(),
        ]);
    }

    protected function applyScopedCache(int $ttl = 3600)
    {
        $cache = $this->cache($ttl);

        foreach ($this->getScopes() as $key => $value) {
            $cache->scopeBy($key, $value);
        }

        return $cache;
    }
}
// Specific filter inheriting scope behavior
class ProductFilter extends ScopedFilter
{
    protected array $defaultScopes = [
        'category' => 'default',
    ];
}

Best Practices

1. Always Scope Multi-tenant Data

// ❌ Bad - Shared cache across tenants
$data = Model::filter()->cache(3600)->get();

// ✅ Good - Tenant-isolated cache
$data = Model::filter()
    ->cache(3600)
    ->scopeByTenant(tenant()->id)
    ->get();

2. Scope Sensitive User Data

// ❌ Bad - User A might see User B's cached data
$personalData = UserData::filter()->cache(1800)->get();

// ✅ Good - Each user has separate cache
$personalData = UserData::filter()
    ->cache(1800)
    ->scopeByUser()
    ->get();

3. Use Consistent Scope Keys

// ❌ Bad - Inconsistent naming
->scopeBy('org', $id)
->scopeBy('organization', $id)
->scopeBy('org_id', $id)

// ✅ Good - Consistent across application
->scopeBy('organization', $id)

4. Document Your Scoping Strategy

/**
 * ProductFilter
 *
 * Cache Scoping:
 * - tenant: Current tenant ID
 * - region: Geographic region
 * - currency: Currency code
 *
 * Example cache key:
 * filterable:product_filter:tenant:acme:region:US:currency:USD:...
 */
class ProductFilter extends Filterable
{
    // ...
}

5. Monitor Scope Effectiveness

// Log scope usage for debugging
Log::debug('Cache scope applied', [
    'filter' => static::class,
    'scopes' => $this->getCacheScopes(),
    'key' => $this->getCacheKey(),
]);

Troubleshooting

Cache Leaking Between Users

// Problem: Users seeing each other's data
// Solution: Add user scoping
->scopeByUser()

Cache Not Invalidating

// Problem: Cache persists after tenant data changes
// Solution: Ensure auto-invalidation observer includes tenant scope

// config/filterable.php
'auto_invalidate' => [
    'enabled' => true,
    'models' => [
        Product::class => function($product) {
            return [
                'products',
                "tenant:{$product->tenant_id}",
            ];
        },
    ],
],

Inconsistent Cache Keys

// Problem: Same query generating different keys
// Solution: Ensure scope order is consistent

// Use withScopes() for consistent ordering
->withScopes([
    'tenant' => $tenantId,
    'user' => $userId,
    'organization' => $orgId,
])

Next Steps

  • Getting Started →
  • Auto-invalidation →
  • Cache Profiles → :::
Edit this page
Last Updated:
Contributors: kettasoft
Prev
Cache Profiles
Next
Monitoring Cached Items