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.
pokhreldipesh/model-filter 适用场景与选型建议
pokhreldipesh/model-filter 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 06 月 11 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「search」 「filter」 「query」 「sort」 「laravel」 「eloquent」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 pokhreldipesh/model-filter 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 pokhreldipesh/model-filter 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 pokhreldipesh/model-filter 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
A Laravel package to retrieve data from Google Search Console
Nova searchable filter for belongsTo relationships.
Indexed Search Autocomplete - Extends the TYPO3 Core Extension Indexed_Search searchform with an autocomplete feature.
Query filtering in your frontend
Abstraction Layer to index and search entities
Anax Database Active Record module for model classes.
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 39
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-11