pokhreldipesh/model-filter
最新稳定版本:v1.0.1
Composer 安装命令:
composer require pokhreldipesh/model-filter
包简介
A composable query filter system for Laravel Eloquent models. Define filters and sorting behavior on models, then apply them from request parameters.
README 文档
README
A composable query filter system for Laravel Eloquent models. Define filters and sorting behavior directly on your models, then apply them from request parameters with zero boilerplate.
Requirements
- PHP 8.3+
- Laravel 13+
Installation
composer require pokhreldipesh/model-filter
The service provider is auto-discovered. To publish the config:
php artisan vendor:publish --tag=model-filter-config
Quick Start
1. Add the trait to your model
use Dipeshpokhrel\ModelFilter\HasQueryFilters; class Post extends Model { use HasQueryFilters; public function filters(): array { return [ $this->textSearch('q', ['title', 'body']), $this->exactMatch('status_id'), $this->relationshipExists('author_id')->relation('author'), ]; } public function allowedSorts(): array { return ['title', 'created_at', 'views_count']; } public function defaultSort(): string { return 'created_at'; } }
2. Use the scopes in your controller
public function index(Request $request) { return PostResource::collection( Post::with('author') ->applyFilter($request) ->applySort($request) ->paginate($request->input('per_page', 15)), ); }
3. Make a request
GET /api/v1/posts?q=laravel&status_id=1&author_id=5&sort_by=title&sort_dir=desc&per_page=20
Filter Types
TextSearch
Search across one or more columns using ilike (PostgreSQL) or like (MySQL/SQLite). Auto-detects the operator based on your database driver. Skips when the value is an empty string.
use Dipeshpokhrel\ModelFilter\Filters\TextSearch; // Single column (defaults to the parameter name as column) TextSearch::make('q') // Multiple columns TextSearch::make('q')->columns(['title', 'isbn', 'description']) // Force a specific operator TextSearch::make('q')->mode('like')
Request: ?q=laravel
ExactMatch
Exact value match on a column. Useful for FK filtering, status filtering, etc.
use Dipeshpokhrel\ModelFilter\Filters\ExactMatch; ExactMatch::make('publisher_id') ExactMatch::make('book_type_id') ExactMatch::make('book_status_id')
Request: ?publisher_id=1
RelationshipExists
Filter by related model existence via whereHas. Automatically resolves the related model's table and filters by its id column.
use Dipeshpokhrel\ModelFilter\Filters\RelationshipExists; RelationshipExists::make('genre_id')->relation('genres') RelationshipExists::make('author_id')->relation('authors')
When relation() is not called, the parameter name is used as the relationship method name.
Request: ?genre_id=3
NullCheck
whereNull / whereNotNull filter.
use Dipeshpokhrel\ModelFilter\Filters\NullCheck; NullCheck::make('deleted_at')
NullCheck with column override
NullCheck::make('parent') ->column('parent_id')
NullCheck with value mapping
Map request values to null/not-null checks:
NullCheck::make('parent') ->column('parent_id') ->mapping([ 'top' => 'null', 'sub' => 'not_null', ])
Request: ?parent=top adds WHERE parent_id IS NULL
Custom
Arbitrary closure-based filter. Best for enum-style or multi-condition logic. Has no default behavior -- always use via using() or constructor callback.
use Dipeshpokhrel\ModelFilter\Filters\Custom; Custom::make('parent', function (Builder $query, mixed $value): void { match ($value) { 'top' => $query->whereNull('parent_id'), 'sub' => $query->whereNotNull('parent_id'), default => null, }; })
Request: ?parent=top
Helper Shortcuts
The HasQueryFilters trait provides shorthand methods so you don't need to import filter classes:
$this->textSearch('q', ['title', 'isbn']) // TextSearch::make('q')->columns([...]) $this->exactMatch('publisher_id') // ExactMatch::make('publisher_id') $this->relationshipExists('genre_id') // RelationshipExists::make('genre_id') $this->nullCheck('parent_id') // NullCheck::make('parent_id') $this->custom('parent', fn ($q, $v) => ...) // Custom::make('parent', fn)
Overriding Filter Behavior
Every filter supports two ways to override its default behavior:
Via make() -- pass a callback as the second argument
TextSearch::make('q', function (Builder $query, mixed $value): void { $query->where('title', $value) ->orWhereHas('tags', fn ($t) => $t->where('name', $value)); }) ExactMatch::make('publisher_id', function (Builder $query, mixed $value): void { $query->where('publisher_id', '>', $value); })
Via using() -- fluent override after construction
TextSearch::make('q')->using(function (Builder $query, mixed $value): void { $query->where('title', $value) ->orWhereHas('tags', fn ($t) => $t->where('name', $value)); })
Both approaches are equivalent. using() is defined on the base Filter class so every filter inherits it.
Customization via Closure
Filter customization
Add, remove, or modify filters before they are applied:
Post::applyFilter($request, function (array &$filters, Request $request) { $filters[] = ExactMatch::make('category_id'); });
Sort customization
Pass a closure to applySort() for complex sort logic (joins, raw expressions, etc.):
Post::applySort($request, function (Builder $query, string $sortBy, string $sortDir) { match ($sortBy) { 'author_name' => $query->join('users', 'posts.author_id', '=', 'users.id') ->orderBy('users.name', $sortDir) ->select('posts.*'), default => $query->orderBy($sortBy, $sortDir), }; });
When a resolver is provided:
- It receives the raw
sort_byvalue from the request (not validated againstallowedSorts()) sort_diris still resolved toasc/desc- The closure takes full control -- no whitelist check, no fallback to
defaultSort()
Sorting
Simple sorting
Defined on the model via allowedSorts() and defaultSort():
public function allowedSorts(): array { return ['title', 'created_at', 'views_count']; } public function defaultSort(): string { return 'title'; }
Request params: ?sort_by=title&sort_dir=desc
sort_bymust be inallowedSorts()or falls back todefaultSort()sort_dirisasc(default) ordesc
Filter API Reference
Base Filter class
All filters extend Dipeshpokhrel\ModelFilter\Filter which provides:
| Method | Description |
|---|---|
make(string $parameter, ?Closure $handler = null) |
Create a new filter instance |
using(Closure $handler) |
Override the default handler with a closure |
default(mixed $default) |
Set a default value when the parameter is missing |
required() |
Mark the filter as required |
getParameter(): string |
Get the request parameter name |
getDefault(): mixed |
Get the default value |
isRequired(): bool |
Check if the filter is required |
__invoke(Builder $query, mixed $value): void |
Apply the filter to a query |
HasQueryFilters trait
| Method | Description |
|---|---|
filters(): array |
Define filters for the model. Default: text search on string $fillable columns |
allowedSorts(): array |
Whitelist of sortable columns. Default: ['id'] |
defaultSort(): string |
Fallback sort column. Default: 'id' |
scopeApplyFilter(Builder, Request, ?Closure): Builder |
Apply all filters from the request |
scopeApplySort(Builder, Request, ?Closure): Builder |
Apply sorting from the request |
Configuration
Publish the config file:
php artisan vendor:publish --tag=model-filter-config
// config/model-filter.php return [ 'text_search_mode' => null, // 'ilike', 'like', or null for auto-detect 'default_sort' => 'id', // fallback sort column 'default_sort_direction' => 'asc', // default sort direction ];
Combining Multiple Filters
All filters are applied sequentially. Pass multiple query params to combine:
GET /api/v1/books?q=laravel&publisher_id=1&genre_id=3&sort_by=title&per_page=10
Adding Filters to a New Model
- Add
use HasQueryFilters;to the model - Implement
filters()returning an array of filter instances - Override
allowedSorts()anddefaultSort()as needed - In the controller, call
->applyFilter($request)->applySort($request)->paginate()
use Dipeshpokhrel\ModelFilter\HasQueryFilters; use Illuminate\Database\Eloquent\Model; class Publisher extends Model { use HasQueryFilters; public function filters(): array { return [ $this->textSearch('q', ['title']), ]; } public function allowedSorts(): array { return ['title', 'created_at']; } public function defaultSort(): string { return 'title'; } }
Controller:
public function index(Request $request) { return PublisherResource::collection( Publisher::applyFilter($request) ->applySort($request) ->paginate($request->input('per_page', 15)), ); }
Creating Custom Filters
Extend the base Filter class and implement handle():
<?php namespace App\Filters; use Dipeshpokhrel\ModelFilter\Filter; use Illuminate\Database\Eloquent\Builder; class DateRange extends Filter { protected string $column; public function __construct(string $parameter, ?\Closure $handler = null) { parent::__construct($parameter, $handler); $this->column = $parameter; } public function column(string $column): static { $this->column = $column; return $this; } public function handle(Builder $query, mixed $value): void { if (! is_array($value)) { return; } if (isset($value['from'])) { $query->where($this->column, '>=', $value['from']); } if (isset($value['to'])) { $query->where($this->column, '<=', $value['to']); } } }
Use it in your model:
use App\Filters\DateRange; public function filters(): array { return [ $this->textSearch('q', ['title']), DateRange::make('published_at')->column('published_at'), ]; }
Architecture
src/
├── Filter.php # Abstract base -- make(), handle(), __invoke(), using()
├── HasQueryFilters.php # Trait: filters(), allowedSorts(), applyFilter/Sort scopes
├── ModelFilterServiceProvider.php # Package service provider
└── Filters/
├── TextSearch.php # ilike/like across columns
├── ExactMatch.php # exact where clause
├── RelationshipExists.php # whereHas on related model
├── Custom.php # closure-based filter
└── NullCheck.php # whereNull / whereNotNull
Design Principles
- Each filter is an invokable class (
__invoke(Builder $query, mixed $value)) - Filters are lazy -- only apply when the query parameter is present
using()is on the baseFilterclass -- every filter inherits it for full override- Pass a callback as the second arg to
make()for the same effect:make('q', fn($q, $v) => ...) - Precedence:
using()/ constructor callback > defaulthandle()logic applySort()accepts an optional resolver callback for custom sort logic- Auto-detects
ilike(PostgreSQL) vslike(SQLite) for text search - Default
filters()uses string-type$fillablecolumns for text search - Default
allowedSorts()returns['id'] - Controllers use Laravel's native
->paginate()-- no pagination helper in this package
Development
# Install dependencies composer install # Run tests composer test # Code style composer lint # Static analysis composer test:types # Code quality composer test:rector
License
The MIT License (MIT). Please see License File for more information.
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-11