Filterable for Laravel

repository·main·Indexed 41 days ago

https://github.com/thavarshan/filterable

A Laravel package that transforms HTTP request parameters into composable Eloquent query filters. It features a stateful pipeline with opt-in capabilities for validation, caching, memory management via streaming helpers, rate limiting, and query optimization.

Tokens
5.2K
Snippets
19
Records
22
Agent score
51%

What's inside Filterable

  1. Implement filtering logic in a Filter class

    main

    To create a filter, extend the Filterable\Filter class. You define which request keys map to filter methods using the $filters array. Method names should be the camelCased version of the request keys.

    Key features you can configure in the constructor:

    • enableFeatures([...]): Toggle concerns like validation, optimization, caching, etc.
    • setValidationRules([...]): Define Laravel validation rules for request parameters.
    • registerTransformer(key, callback): Transform input values (e.g., parsing a string into a Carbon instance).
    • registerPreFilters(callback): Apply constraints to the query before any request-based filters run.
    • select([...]): Define specific columns to select.
    • with([...]): Eager load relationships.
    <?php
    
    namespace Appilters;
    
    use Filterable\Filter;
    use Illuminate\Database\Eloquent\Builder;
    use Illuminate\Http\Request;
    use Illuminate\Support\Carbon;
    use Illuminate\Validation\Rule;
    
    class PostFilter extends Filter
    {
        protected array $filters = ['status', 'published_after', 'q'];
    
        public function __construct(Request $request)
        {
            parent::__construct($request);
    
            $this->enableFeatures([
                'validation',
                'optimization',
                'filterChaining',
                'valueTransformation',
            ]);
    
            $this->setValidationRules([
                'status' => ['nullable', Rule::in(['draft', 'published'])],
                'published_after' => ['nullable', 'date'],
            ]);
    
            $this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
            $this->registerPreFilters(fn (Builder $query) => $query->where('is_visible', true));
            $this->select(['id', 'title', 'status', 'published_at'])->with('author');
        }
    
        protected function status(string $value): void
        {
            $this->getBuilder()->where('status', $value);
        }
    
        protected function publishedAfter(Carbon $date): void
        {
            $this->getBuilder()->whereDate('published_at', '>=', $date);
        }
    
        protected function q(string $term): void
        {
            $this->getBuilder()->where(function (Builder $query) use ($term) {
                $query->where('title', 'like', "%{$term}%")
                    ->orWhere('body', 'like', "%{$term}%");
            });
        }
    }
  2. Install Filterable via Composer

    main

    Install the package using Composer to add Filterable to your Laravel project. The package uses auto-discovery to register the FilterableServiceProvider, which provides the make:filter Artisan command and contextual binding for the current Request.

    composer require jerome/filterable
  3. Run the filter pipeline on a query

    main

    Inject your filter class into a controller method. Use the filter() method on an Eloquent query builder to apply the filter logic. You can chain methods like forUser(), enableFeature(), and setOptions() to customize the filter instance at runtime.

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Filters\PostFilter;
    use App\Http\Resources\PostResource;
    use App\Models\Post;
    use Illuminate\Http\Request;
    
    class PostController
    {
        public function index(Request $request, PostFilter $filter)
        {
            $posts = Post::query()
                ->filter(
                    $filter
                        ->forUser($request->user())
                        ->enableFeature('caching')
                        ->setOptions(['chunk_size' => 500])
                )
                ->get();
    
            return PostResource::collection($posts);
        }
    }
  4. Implement Caching and SmartCaching

    main

    Activate caching by injecting an Illuminate\\Contracts\\Cache\\Repository or calling enableFeature('caching').

    Key Capabilities:

    • SmartCaching: Automatically caches complex queries (multiple where clauses, joins, selects) while skipping trivial single-clause lookups.
    • Granular Control: Use cacheResults() and cacheCount() to opt into specific result types. Use cacheTags(['tag']) to scope invalidation.
    • Expiration: Set TTL via setCacheExpiration(int) (seconds) or via the global config.
    • Invalidation: Use clearCache() and clearRelatedCaches() to manage state.

    Cache keys include sanitized filter values and optional user identifiers from forUser().

    $filter->enableFeature('caching')
        ->cacheTags(['posts'])
        ->cacheResults()
        ->cacheCount()
        ->setCacheExpiration(15);
    
    $posts = Post::query()->filter($filter)->get();
    $total = $filter->count();
  5. Configure Rate Limiting

    main

    Prevent abuse by limiting filter complexity and frequency. Enable with enableFeature('rateLimit').

    Default Guardrails:

    • Max filters: 10
    • Complexity budget: 100
    • Window: 60 attempts per 60 seconds
    • Decay: ceil(complexity/10) seconds

    Tuning Options:

    • setMaxFilters(int): Set maximum allowed filters.
    • setMaxComplexity(int): Set maximum complexity budget.
    • setFilterComplexity(array): Define complexity per key (array-valued filters multiply complexity).

    Extensibility: Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for custom logic.

    $filter->enableFeature('rateLimit')
        ->setMaxFilters(5)
        ->setMaxComplexity(25)
        ->setFilterComplexity(['tags' => 3, 'q' => 2]);
  6. Configure Validation and Value Transformation

    main

    You can enforce data integrity and normalize inputs using the validation and valueTransformation features.

    • Validation: Enable with enableFeature('validation'). Use setValidationRules() to define rules (e.g., using Laravel's Rule class), addValidationRule() for single rules, and setValidationMessages() for custom errors. Only active filters are validated, and a ValidationException is thrown on failure.
    • Value Transformation: Enable with enableFeature('valueTransformation'). Use registerTransformer($key, $callback) for specific keys or transformArray($array) for bulk transformations to normalize inputs before filter methods execute.
    $filter->enableFeatures(['validation', 'valueTransformation'])
        ->setValidationRules([
            'status' => ['nullable', Rule::in(['draft', 'published'])],
            'tags' => ['array'],
        ])
        ->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));
  7. Manage Permissions and User Scoping

    main

    Restrict filter access based on users or specific permissions:

    • User Scoping: Use forUser($user) to scope queries to an authenticated identifier. This identifier is automatically folded into cache keys.
    • Permissions: Enable with enableFeature('permissions'). Define required permissions via setFilterPermissions(['key' => 'permission-name']). To integrate with your existing authorization layer, override the userHasPermission() method in your filter class. Disallowed filters are dropped before execution.
    $filter->enableFeature('permissions')
        ->forUser($request->user())
        ->setFilterPermissions(['email' => 'view-sensitive-fields']);
  8. Attach the Filterable trait to an Eloquent model

    main

    To allow a model to accept a filter instance, use the Filterable\Traits\Filterable trait within your model class.

    <?php
    
    namespace App\
    Models;
    
    use Filterable\Traits\Filterable;
    use Illuminate\Database\Eloquent\Model;
    
    class Post extends Model
    {
        use Filterable;
    }
  9. Optimize Queries and Chain Filters

    main

    Improve performance and developer experience with these features:

    • Optimization: Enable with enableFeature('optimization'). This allows applying select(), with(), and chunkSize() before filters run. Use useIndex() to hint MySQL indexes.
    • Filter Chaining: Enable with enableFeature('filterChaining'). This allows queuing fluent additions after request-driven filters. Supported methods include where(), whereIn(), whereNotIn(), whereBetween(), and orderBy().
    $filter->enableFeatures(['optimization', 'filterChaining'])
        ->select(['id', 'title', 'status'])
        ->with(['author', 'tags'])
        ->chunkSize(500)
        ->where('status', 'published')
        ->orderBy('published_at', 'desc');
  10. Manage Memory with Streaming Helpers

    main

    To avoid loading large result sets into memory, enable memoryManagement and use streaming helpers.

    Important: You must call apply() before using streaming helpers, otherwise a RuntimeException will be raised.

    Available Helpers:

    • lazy(), lazyEach()
    • cursor()
    • stream(), streamGenerator()
    • chunk(), map(), filter(), reduce()

    You can control the chunk size via setOption('chunk_size', int).

    $filter->enableFeature('memoryManagement')
        ->setOption('chunk_size', 250);
    
    $filter->apply(Post::query());
    $filter->lazyEach(fn ($post) => /* ... */, 250);