Skip to content

About

Laravel Query Builder 是一个基于接口设计、具备高扩展性的渐进式搜索构建包,旨在让 Laravel 中的复杂搜索更简单、更清晰、更优雅。

Topics

Resources

Stars

3 stars

Watchers

2 watching

Forks

Latest commit

 

History

97 Commits

Folders and files

Repository files navigation

Laravel Query Builder

Laravel Query Builder 是一个基于接口设计、具备高扩展性的渐进式搜索构建包,旨在让 Laravel 中的复杂搜索变得更简单、更清晰、更优雅。

它将搜索逻辑从控制器中彻底解耦,使列表搜索功能的开发更加集中、可维护,同时支持高度复用和灵活扩展。rules() 里声明条件,调用后仍是原来的 Eloquent 查询,可以继续 with()、paginate()、get()。

本包的设计灵感来源于 zhuzhichao/laravel-advanced-search 和 spatie/laravel-query-builder,并结合实际业务需求进行了增强与重构。

环境要求

  • PHP ^8.2
  • Laravel ^11.0 | ^12.0 | ^13.0

安装

composer require mitoop/laravel-query-builder

定义 Filter

php artisan make:filter UserFilter

命令会在 App\Http\Filters 下生成类:

<?php

namespace App\Http\Filters;

use Mitoop\LaravelQueryBuilder\AbstractFilter;

class UserFilter extends AbstractFilter
{
    protected array $allowedSorts = ['id'];

    protected function rules(): array
    {
        return [];
    }
}

控制器里:

use App\Http\Filters\UserFilter;
use App\Models\User;

$users = User::filter(UserFilter::class)->paginate();

不传第二个参数,或传入空数组时,使用当前请求的全部输入。要换成别的数据时传进去:

User::filter(UserFilter::class, $request->validated())->paginate();

规则也可以直接写成数组,不必先建类:

User::filter([
    'name',
    'status|in',
])->paginate();

值为 null、空字符串或空数组的规则会跳过。

搜索规则

规则写在 rules() 里。字符串的格式是:

[请求字段]:[数据库字段]|[操作符]

冒号和竖线都可以省略。省略冒号时,请求字段和数据库字段同名。省略操作符时,用 eq。

use Mitoop\LaravelQueryBuilder\ValueResolvers\DateRange;
use Mitoop\LaravelQueryBuilder\ValueResolvers\Like;

protected function rules(): array
{
    return [
        'name',
        'email|like' => new Like,
        'created_from:created_at|gte',
        'created_to:created_at|lte',
        'created_at|between' => new DateRange,
        'nickname:profile->nickname|like' => new Like,
        'position$name|like' => new Like,
        'u.name',
    ];
}
写法 含义
name 请求里的 name 等于 name 列
email|like 用 like 比较 email
created_from:created_at|gte 请求字段是 created_from,列是 created_at
profile->nickname JSON 路径
position$name position 关联上的 name 列
u.name 带表别名的列

position$name 会变成 whereHas('position', ...)。关联名会转成驼峰,user_profile$bio 对应 userProfile。

操作符

操作符 作用
eq =,默认
ne <>
gt / lt > / <
gte / ge >=
lte / le <=
like like ? escape ?。通配符由值处理器加上,操作符补上 ESCAPE '\'
in / not_in 数组,或 1,2 这种逗号分隔的字符串
between 正好两个值,数组或 start,end。数量不对会抛异常
is_null / not_null 请求值为真时才加上 whereNull / whereNotNull
json_contains JSON 包含

数据库不认识的操作符会抛出 InvalidArgumentException。

自定义操作符在 AppServiceProvider::boot() 里注册:

use Illuminate\Database\Eloquent\Builder;
use Mitoop\LaravelQueryBuilder\Contracts\OperatorFactoryInterface;
use Mitoop\LaravelQueryBuilder\Contracts\OperatorInterface;

class BeforeOperator implements OperatorInterface
{
    public function apply(Builder $builder, string $whereType, string $field, $value): void
    {
        $builder->{$whereType}($field, '<', $value);
    }
}

app(OperatorFactoryInterface::class)->register('before', fn () => new BeforeOperator);

注册名就是规则里的操作符,例如 published_at|before。

值处理

操作符只决定比较方式。%、日期边界这类转换交给值处理器。

  • Mitoop\LaravelQueryBuilder\ValueResolvers\Like:调用 LikeClause::pattern(),两侧加上 %,并转义输入里的 %、_ 和 \
  • Mitoop\LaravelQueryBuilder\ValueResolvers\LikeAny:同一套 LikeClause::pattern(),匹配多个列
  • Mitoop\LaravelQueryBuilder\LikeColumn:同一个关键字按请求里的类型选一列。类型不在名单里时用默认列
  • Mitoop\LaravelQueryBuilder\ValueResolvers\DateRange:把两个日期转成当天的开始和结束。格式不对时这条规则跳过
use Mitoop\LaravelQueryBuilder\ValueResolvers\DateRange;
use Mitoop\LaravelQueryBuilder\ValueResolvers\Like;
use Mitoop\LaravelQueryBuilder\ValueResolvers\LikeAny;

protected function rules(): array
{
    return [
        'email|like' => new Like,
        'keyword|like_any' => new LikeAny(['name', 'email']),
        'created_at|between' => new DateRange,
    ];
}

Like 的前后缀可以改,例如只匹配开头:new Like('', '%')。

同一个关键字要按类型落到不同列时,用 LikeColumn。它直接放进 rules(),一次只查一列。

use Mitoop\LaravelQueryBuilder\LikeColumn;

protected function rules(): array
{
    return [
        new LikeColumn(
            field: 'keyword',
            type: 'keyword_type',
            columns: ['name', 'email', 'phone'],
            default: 'name',
        ),
    ];
}
参数 作用
field 关键字所在的请求字段
type 列类型所在的请求字段
columns 允许被选中的列。请求里的类型必须和其中一项完全一致
default 没传类型、类型为空,或类型不在 columns 里时使用的列

上面的规则对应这些请求:

请求 查询
keyword=ada name like
keyword=ada&keyword_type=name name like。name 在 columns 里,按这个类型查询
keyword=ada&keyword_type=email 只查 email
keyword=ada&keyword_type=other name like。other 不在 columns 里,所以用 default

字段有值时才追加一段查询,用 whenValue()。手写查询也走 LikeClause。pattern() 转义输入里的 %、_ 和 \。后两个参数是前后缀,省略时两侧都是 %。下面只在后面加 %,输入 ada 时,$pattern 是 ada%。where() 和 orWhere() 加上 ESCAPE。

use Illuminate\Database\Eloquent\Builder;
use Mitoop\LaravelQueryBuilder\Support\LikeClause;

$this->whenValue('keyword', function (Builder $builder, string $keyword): void {
    $pattern = LikeClause::pattern($keyword, '', '%');

    if ($pattern === '') {
        return;
    }

    LikeClause::where($builder, 'name', $pattern);
    LikeClause::orWhere($builder, 'email', $pattern);
});

要实现自己的值处理器,实现 Mitoop\LaravelQueryBuilder\Contracts\ValueResolver。resolve() 接收请求里的原始值,返回交给操作符使用的值。

use Mitoop\LaravelQueryBuilder\Contracts\ValueResolver;

class UpperName implements ValueResolver
{
    public function resolve($value): string
    {
        return strtoupper((string) $value);
    }
}

规则写成 'name' => new UpperName 时,等值比较用的就是 resolve() 的返回值。

关联、Scope 和原生条件

rules() 里也可以放 Scope、闭包和 DB::raw()。

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Mitoop\LaravelQueryBuilder\Scope;

protected function rules(): array
{
    return [
        new Scope('active'),
        (new Scope('role', (int) $this->value('role_id')))->when((bool) $this->value('role_id')),
        function (Builder $builder): void {
            $builder->where('is_verified', true);
        },
        DB::raw('users.score > 100'),
    ];
}

new Scope('emailVerified') 和 new Scope('email_verified') 都会调用模型上的 scopeEmailVerified。when() 的条件不成立时,这个 Scope 不会加入查询。

Laravel 12 起,模型方法也可以用 #[Scope] 标记。Laravel 11 只有 scopeXxx 这种写法。

排序

请求参数默认是 sorts。字段前加 - 表示降序:

sorts=-id,name

只有 $allowedSorts 里的字段会生效,其余忽略。默认只允许 id。

protected array $allowedSorts = ['id', 'name', 'created_at'];

换参数名时改 $sortField。每个 Filter 用自己的名字:

protected string $sortField = 'order_by';

sorts() 返回非空数组时,不再读取请求里的排序:

protected function sorts(): array
{
    return [
        'id' => 'desc',
    ];
}

数字下标的值会原样写进 orderByRaw(),例如 'created_at asc'。

预加载

Filter 定义了 with() 时,查询构建前会自动预加载:

protected function with(): array
{
    return ['org', 'roles'];
}

with() 需要是 public 或 protected。重写 boot() 时先调用父类,否则上面的预加载不会执行:

public function boot(): void
{
    parent::boot();
}

默认筛选值

booting() 在请求数据写入之后、规则生效之前运行。请求里已经带了 status 时,下面这行不会覆盖它:

public function booting(): void
{
    $this->data['status'] ??= 'active';
}

完整示例

<?php

namespace App\Http\Filters;

use Illuminate\Database\Eloquent\Builder;
use Mitoop\LaravelQueryBuilder\AbstractFilter;
use Mitoop\LaravelQueryBuilder\LikeColumn;
use Mitoop\LaravelQueryBuilder\Scope;
use Mitoop\LaravelQueryBuilder\ValueResolvers\DateRange;
use Mitoop\LaravelQueryBuilder\ValueResolvers\Like;
use Mitoop\LaravelQueryBuilder\ValueResolvers\LikeAny;

class UserFilter extends AbstractFilter
{
    protected array $allowedSorts = ['id', 'created_at'];

    protected function rules(): array
    {
        return [
            'id',
            'name|like' => new Like,
            'email|like' => new Like,
            'status|in',
            'created_from:created_at|gte',
            'created_to:created_at|lte',
            'created_at|between' => new DateRange,
            'nickname:profile->nickname|like' => new Like,
            'tag:profile->tags|json_contains',
            'position$name|like' => new Like,
            'q|like_any' => new LikeAny(['title', 'company']),
            new LikeColumn(
                field: 'keyword',
                type: 'keyword_type',
                columns: ['name', 'email', 'phone'],
                default: 'name',
            ),
            new Scope('active'),
            $this->whenValue('role_id', function (Builder $builder, int|string $roleId): void {
                $builder->where('role_id', (int) $roleId);
            }),
        ];
    }

    protected function with(): array
    {
        return ['position'];
    }
}
User::filter(UserFilter::class)->paginate();

贡献

欢迎提交 issue 或 Pull Request。

协议

MIT

About

Laravel Query Builder 是一个基于接口设计、具备高扩展性的渐进式搜索构建包,旨在让 Laravel 中的复杂搜索更简单、更清晰、更优雅。

Topics

Resources

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages