dibakar/laravel-dynamic-filters

A flexible and dynamic filtering system for Laravel Eloquent models that allows for easy implementation of complex filtering and searching capabilities in your applications.

1.0.0 2025-09-18 06:33 UTC

This package is auto-updated.

Last update: 2025-09-18 06:34:05 UTC


README

Latest Version on Packagist Total Downloads License PHP Version Laravel Version

A robust and flexible filtering system for Laravel Eloquent models that makes building complex, dynamic queries a breeze. This package provides an elegant, fluent API for filtering, searching, and sorting your Eloquent models with minimal configuration.

✨ Features

  • Expressive Filtering: Chainable methods and intuitive syntax for complex queries
  • Advanced Search: Full-text search with fuzzy matching and term normalization
  • Relationship Support: Filter across model relationships with nested conditions
  • Type Safety: Strict type checking and automatic value casting
  • Performance Optimized: Efficient query building with minimal overhead
  • Security First: Whitelisting and input validation out of the box
  • Extensible: Easy to create and register custom filters
  • Modern PHP: Built with PHP 8.1+ features and type hints

🚀 Installation

Requirements

  • PHP 8.1 or higher
  • Laravel 10.x or later
  • Composer

Install via Composer

composer require dibakar/laravel-dynamic-filters

Configuration (Optional)

Publish the configuration file to customize the package behavior:

php artisan vendor:publish --provider="Dibakar\LaravelDynamicFilters\DynamicFiltersServiceProvider" --tag="config"

This will create a dynamic-filters.php file in your config directory with sensible defaults.

Service Provider & Facade

The package uses Laravel's package auto-discovery, but you can manually register it in config/app.php if needed:

'providers' => [
    // Other service providers...
    Dibakar\LaravelDynamicFilters\DynamicFiltersServiceProvider::class,
],

'aliases' => [
    // Other aliases...
    'DynamicFilter' => Dibakar\LaravelDynamicFilters\Facades\DynamicFilter::class,
],

📦 Version Compatibility

Laravel PHP Package
12.x 8.2+ ^1.0
11.x 8.2+ ^1.0
10.x 8.1+ ^1.0

🚀 Quick Start

1. Prepare Your Model

Add the HasDynamicFilter trait to your Eloquent model and define the filterable, searchable, and sortable fields:

use Dibakar\LaravelDynamicFilters\Traits\HasDynamicFilter;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasDynamicFilter;
    
    /**
     * Fields that can be searched.
     */
    protected $searchable = [
        'title', 
        'content',
        'author.name',    // Search in relationships
        'tags.name'       // Search in many-to-many relationships
    ];
    
    /**
     * Fields that can be filtered with operators.
     */
    protected $filterable = [
        'id',
        'status',
        'category_id',
        'published_at',
        'views',
        'is_featured',
    ];
    
    /**
     * Fields that can be used for sorting.
     */
    protected $sortable = [
        'created_at' => 'desc',  // Default sort
        'title' => 'asc',
        'views' => 'desc',
    ];
    
    /**
     * Default filter presets.
     */
    protected $filterPresets = [
        'published' => [
            'status' => 'published',
            'sort' => '-published_at',
        ],
        'popular' => [
            'views' => ['gt' => 1000],
            'is_featured' => true,
            'sort' => '-views',
        ],
    ];
        'status',                   // Simple filter: ?status=published
        'category_id',              // Exact match: ?category_id=5
        'created_at' => [           // Date filtering
            'operators' => ['=', '>', '<', '>=', '<=', '!='],
            'cast' => 'date',
        ],
        'views' => [                // Numeric filtering
            'operators' => ['=', '>', '<', '>=', '<=', '!='],
            'cast' => 'int',
        ],
        'author_id' => 'author.id', // Relationship filtering
        'tag_id' => 'tags.id'      // Many-to-many relationship
    ];
    
    /**
     * Get the default filters that should be applied to all queries.
     *
     * @return array
     */
    public function getDefaultFilters()
    {
        return [
            'status' => 'published',
            'sort' => '-created_at',
        ];
    }
}

2. Basic Filtering

Filter your models using query parameters in your controller:

// GET /posts?status=published&created_at[gt]=2023-01-01&sort=-views,title
public function index(Request $request)
{
    $posts = Post::filter($request->query())
        ->with(['author', 'category', 'tags']) // Eager load relationships
        ->paginate($request->per_page ?? 15);

    return response()->json($posts);
}

3. Search Functionality

Search across searchable fields with a simple API:

// GET /posts?q=laravel+framework
public function search(Request $request)
{
    $posts = Post::search($request->q)
        ->filter($request->except('q')) // Apply additional filters
        ->paginate($request->per_page ?? 15);

    return response()->json($posts);
}

4. Sorting Results

Sort your results using the sort parameter in your requests. The - prefix indicates descending order.

Basic Sorting

// In your controller
$posts = Post::sort($request->input('sort'))->get();

// Or chain it with filters
$posts = Post::filter($filters)
    ->sort($request->input('sort', 'created_at,desc'))
    ->paginate(15);

// Example requests:
// GET /posts?sort=title             // Sort by title (ascending)
// GET /posts?sort=title,asc         // Same as above (explicit ascending)
// GET /posts?sort=title,desc        // Sort by title (descending)
// GET /posts?sort=-title            // Alternative: Sort by title (descending)
// GET /posts?sort=views,desc&sort=title,asc  // Multiple sort fields
// GET /posts?sort=created_at,desc   // Sort by created_at (newest first)
// GET /posts?sort=author.name,asc   // Sort by relationship field

Default Sorting

Define default sorting in your model:

// In your model
protected $sortable = [
    'created_at' => 'desc',  // Default sort
    'title' => 'asc',
    'views' => 'desc',
    'author.name' => 'asc',  // Sort by relationship
];

Sorting in API Requests

// In your controller
public function index(Request $request)
{
    $validated = $request->validate([
        'sort' => 'sometimes|string',
        // other validation rules
    ]);

    return Post::filter($request->except('sort'))
        ->sort($validated['sort'] ?? null)
        ->paginate($request->per_page ?? 15);
}

Available Sort Options

  • field - Sort ascending
  • -field - Sort descending
  • relation.field - Sort by relationship field

5. Pagination

Pagination works seamlessly with Laravel's built-in pagination:

// GET /posts?page=2&per_page=20
$posts = Post::filter($request->query())
    ->paginate($request->per_page ?? 15);

🚀 Advanced Usage

1. Complex Filter Groups

Create complex filter conditions with AND/OR logic:

// Example: (status = 'published' AND (title LIKE '%Laravel%' OR views > 100)) AND (author_id = 1 OR author_id = 2)
$filters = [
    '_group' => [
        'boolean' => 'and',
        'filters' => [
            'status' => 'published',
        ],
        'nested' => [
            [
                'boolean' => 'or',
                'filters' => [
                    'title' => ['like' => '%Laravel%'],
                    'views' => ['gt' => 100],
                ],
            ],
            [
                'boolean' => 'or',
                'filters' => [
                    'author_id' => [1, 2],
                ],
            ],
        ],
    ],
];

$posts = Post::filter($filters)->get();

2. Custom Filter Classes

For complex filtering logic, create a custom filter class:

<?php

namespace App\Filters;

use Dibakar\LaravelDynamicFilters\Contracts\FilterContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;

class PublishedInLastDaysFilter implements FilterContract
{
    /**
     * Apply the filter to the query.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $value
     * @param string $property
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function apply(Builder $query, $value, string $property): Builder
    {
        $days = is_numeric($value) ? (int) $value : 7; // Default to 7 days if invalid
        
        return $query->where('published_at', '>=', Carbon::now()->subDays($days));
    }
    
    /**
     * Validate the filter value.
     *
     * @param mixed $value
     * @return bool
     */
    public function validate($value): bool
    {
        return is_numeric($value) && $value > 0;
    }
    
    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function getValidationMessage(): string
    {
        return 'The days parameter must be a positive number.';
    }
}

Register your custom filter in config/dynamic-filters.php:

'custom_filters' => [
    'published_in_days' => \App\Filters\PublishedInLastDaysFilter::class,
    'active_users' => \App\Filters\ActiveUsersFilter::class,
    // Add more custom filters as needed
],

Now use it in your API:

// GET /posts?published_in_days=30
$recentPosts = Post::filter(request()->query())->get();

3. Filter Presets

Define common filter presets in your configuration:

// config/dynamic-filters.php
'presets' => [
    'recent' => [
        'created_at' => ['gt' => now()->subMonth()->toDateString()],
        'sort' => '-created_at',
    ],
    'popular' => [
        'views' => ['gt' => 1000],
        'status' => 'published',
        'sort' => '-views,title',
    ],
    'featured' => [
        'is_featured' => true,
        'status' => 'published',
        'sort' => '-created_at',
    ],
],

Use presets in your API:

# Get recent posts
GET /posts?preset=recent

# Get popular posts
GET /posts?preset=popular

# Combine with additional filters
GET /posts?preset=featured&category=laravel

4. API Resource Integration

Easily integrate with Laravel's API Resources:

// app/Http/Resources/PostResource.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => $this->excerpt,
            'content' => $this->when($this->showFullContent, $this->content),
            'status' => $this->status,
            'views' => $this->views,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
            'author' => UserResource::make($this->whenLoaded('author')),
            'category' => CategoryResource::make($this->whenLoaded('category')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
        ];
    }
}

// In your controller
public function index(Request $request)
{
    $posts = Post::filter($request->query())
        ->with(['author', 'category', 'tags'])
        ->paginate($request->per_page ?? 15);

    return PostResource::collection($posts);
}

5. Performance Optimization

Database Indexing

// In a migration
public function up()
{
    Schema::table('posts', function (Blueprint $table) {
        // Single column indexes
        $table->index('status');
        $table->index('published_at');
        $table->index('views');
        
        // Composite index for common filter combinations
        $table->index(['status', 'published_at']);
        $table->index(['category_id', 'status', 'published_at']);
    });
}

Selective Field Loading

// Only select the fields you need
$posts = Post::select([
        'id', 
        'title', 
        'slug', 
        'excerpt', 
        'status', 
        'published_at',
        'author_id',
        'category_id'
    ])
    ->with([
        'author:id,name,avatar',
        'category:id,name,slug',
        'tags:id,name,slug'
    ])
    ->filter($filters)
    ->paginate(15);

Performance Tips

  1. Index Your Database: Add indexes to columns used in filtering, searching, and sorting to improve query performance.

  2. Use Select Wisely: Only select the columns you need to reduce memory usage.

    $posts = Post::filter($filters)
        ->select('id', 'title', 'created_at')
        ->with('author:id,name')
        ->get();
  3. Eager Load Relationships: Use with() to avoid N+1 query problems.

    $posts = Post::filter($filters)
        ->with(['author', 'category'])
        ->get();
  4. Limit Result Size: Always use pagination or limit for large datasets to improve performance.

    $posts = Post::filter($filters)->paginate(15);
  5. Consider Caching: For expensive queries, consider implementing caching at the application level using Laravel's caching system.

6. Error Handling

Handle filter validation errors gracefully:

try {
    $posts = Post::filter($request->query())->paginate(15);
    return PostResource::collection($posts);
} catch (\Dibakar\LaravelDynamicFilters\Exceptions\InvalidFilterException $e) {
    return response()->json([
        'message' => 'Invalid filter parameters',
        'errors' => $e->getMessage()
    ], 400);
} catch (\Exception $e) {
    return response()->json([
        'message' => 'An error occurred while processing your request.',
        'error' => config('app.debug') ? $e->getMessage() : 'Server error'
    ], 500);
}

7. Testing Your Filters

Write tests to ensure your filters work as expected:

// tests/Feature/PostFilterTest.php

public function test_can_filter_posts_by_status()
{
    $published = Post::factory()->create(['status' => 'published']);
    $draft = Post::factory()->create(['status' => 'draft']);

    $response = $this->getJson('/api/posts?status=published');

    $response->assertStatus(200)
        ->assertJsonCount(1, 'data')
        ->assertJsonFragment(['id' => $published->id])
        ->assertJsonMissing(['id' => $draft->id]);
}

public function test_can_search_posts()
{
    $laravelPost = Post::factory()->create([
        'title' => 'Getting Started with Laravel',
        'content' => 'Laravel is a web application framework...'
    ]);
    
    $symfonyPost = Post::factory()->create([
        'title' => 'Symfony vs Laravel',
        'content' => 'Comparison between the two frameworks...'
    ]);

    $response = $this->getJson('/api/posts?q=laravel');

    $response->assertStatus(200)
        ->assertJsonCount(2, 'data')
        ->assertJsonFragment(['id' => $laravelPost->id])
        ->assertJsonFragment(['id' => $symfonyPost->id]);
}

Available Operators

Operator Description Example
= Equals ?status=active
!= Not equals ?status[neq]=inactive
> Greater than ?views[gt]=100
>= Greater than or equal ?rating[gte]=4
< Less than ?price[lt]=100
<= Less than or equal ?age[lte]=30
like Like (case-sensitive) ?name[like]=%john%
ilike Like (case-insensitive) ?email[ilike]=%gmail.com
in In array ?status[in]=active,pending
not_in Not in array ?id[not_in]=1,2,3
between Between values ?created_at[between]=2023-01-01,2023-12-31
null Is null ?deleted_at[null]
notnull Is not null ?updated_at[notnull]

Security

By default, only fields defined in the $filterable array can be filtered. This is a security measure to prevent unauthorized filtering on sensitive fields.

You can also define a global whitelist in the config file that applies to all models:

'global_whitelist' => [
    'id',
    'status',
    'created_at',
    'updated_at',
],

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email dibakarmitra07@gmail.com instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.