Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

Notifications You must be signed in to change notification settings

dibakarmitra/laravel-dynamic-filters

Repository files navigation

Laravel Dynamic Filters

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.

About

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

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /