pokhreldipesh/model-filter 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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_by value from the request (not validated against allowedSorts())
  • sort_dir is still resolved to asc/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_by must be in allowedSorts() or falls back to defaultSort()
  • sort_dir is asc (default) or desc

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

  1. Add use HasQueryFilters; to the model
  2. Implement filters() returning an array of filter instances
  3. Override allowedSorts() and defaultSort() as needed
  4. 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 base Filter class -- 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 > default handle() logic
  • applySort() accepts an optional resolver callback for custom sort logic
  • Auto-detects ilike (PostgreSQL) vs like (SQLite) for text search
  • Default filters() uses string-type $fillable columns 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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-11

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固