jpnut / eloquent-nested-filter
为 Eloquent 模型提供嵌套过滤器结构
Requires
- php: ^7.4|^8.0
- illuminate/database: ^8.0
- myclabs/php-enum: ^1.7
- phpdocumentor/reflection-docblock: ^5.1
Requires (Dev)
- ext-json: *
- orchestra/testbench: ^6.0
- phpunit/phpunit: ^9.1
- vimeo/psalm: ^3.11
This package is auto-updated.
Last update: 2024-09-29 05:48:48 UTC
README
此包提供了一种为 Eloquent 模型快速简洁地定义嵌套过滤器结构的方法。这使得根据用户输入以更复杂的方式过滤数据变得更加容易。
例如,给定如下定义的过滤器对象
... class ProductFilter extends AbstractFilter { public ?StringFilterObject $name; }
我们可以使用它来过滤我们的 Product
查询
... $filter = new ProductFilter([ 'or' => [ [ 'name' => [ 'value' => 'foo', 'operator' => 'BEGINS', ] ], [ 'name' => [ 'value' => 'foo', 'operator' => 'ENDS', ] ], ] ]); $query = $filter->filter(Product::query()); ...
这将生成一个 sql 查询
SELECT * FROM `products` WHERE (`name` LIKE 'foo%' or `name` LIKE '%foo')
安装
您可以通过 composer 安装此包
composer require jpnut/eloquent-nested-filter
使用方法
要开始使用,您需要为要过滤的每个模型创建一个过滤器类。这个类应该扩展 JPNut\EloquentNestedFilter\AbstractFilter
。在这个类中,您应该定义所有可过滤的属性。此包附带内置过滤器,用于主键、字符串、数字、布尔值和日期时间;尽管您可以自由创建和使用任何其他过滤器。您还可以通过包含一个引用相关模型过滤器类的属性来过滤关系。让我们看看一个示例过滤器类
// app/ProductFilter.php namespace App; ... class ProductFilter extends AbstractFilter { public ?IDFilterObject $id = null; public ?StringFilterObject $name = null; public ?NumberFilterObject $amount = null; public ?BooleanFilterObject $in_stock = null; public ?DateFilterObject $created_at = null; /** @var \App\CategoryFilter[]|null */ public ?array $category = null; }
首先,请注意,我们通过其类型声明来定义每个属性的过滤器类型。对于数组过滤器,如 category
属性,由于 PHP 目前不支持泛型,我们必须在文档块中指定过滤器类型。每个 AbstractFilter
实例也预定义了 and
和 or
属性,这些属性期望一个父类的数组。这些属性允许我们以预期的方式将过滤器分组在一起。
由于手动构建这些对象会很麻烦,构造函数接受一个单个关联数组,并将尝试根据类型信息转换过滤器属性。当然,如果传递了正确对象的一个实例,将使用该对象。但是,如果传递了一个数组,类将尝试构建正确的数组/对象。
让我们重新使用我们在本说明书的开头定义的示例
... $filter = new ProductFilter([ 'or' => [ [ 'name' => [ 'value' => 'foo', 'operator' => 'BEGINS', ] ], [ 'name' => [ 'value' => 'foo', 'operator' => 'ENDS', ] ], ] ]); ...
or
属性由 ProductFilter
实例的数组组成。在这种情况下,我们传递了两个关联数组 - 这些将通过将它们的值传递给 ProductFilter
的构造函数自动转换为 ProductFilter
的实例。名称过滤器以类似的方式构建。
典型请求工作流程
此库的主要用途来自用户输入。通常这意味着将 filter
关联数组作为 JSON 编码的字符串提供。然后可以通过传递解码的值构建过滤器。
... $filter = $request->has('filter') ? new ProductFilter(json_decode($request->query('filter'), true)) : null; ...
AbstractFilter
的实例公开了一个 filter
方法,它期望一个参数 - 基础 eloquent 查询。通常这将是 Product::query()
,尽管当然可以限制或操纵过滤器。
... $results = $filter->query(Product::query()->withTrashed())->get(); ...
自定义过滤器对象
您可能希望添加新的过滤器对象。为此,只需创建一个新类,该类扩展 JPNut\EloquentNestedFilter\AbstractFilterObject
或实现 JPNut\EloquentNestedFilter\Contracts\FilterObject
。您的类 必须 定义两个方法
abstract public function filter(string $name, Builder $query): Builder; abstract public static function fromArray(array $properties = []): self;
filter
方法接受两个参数 - 被过滤的字段名称和查询实例,并应返回修改后的查询实例。通常,过滤器方法应处理所有有效的 Operator
值(例如,IS
、IS_NOT
等),如果使用无效的操作符,则抛出异常。例如
public function filter(string $name, Builder $query): Builder { if ($this->operator->equals(Operator::IS())) { return $query->where($name, $this->value); } if ($this->operator->equals(Operator::IS_NOT())) { return $query->where($name, '<>', $this->value); } ... throw $this->invalidOperator($this->operator); }
fromArray
方法接受一个参数 - 属性数组 - 应返回一个新的过滤器对象实例。当从关联数组创建过滤器对象时,会调用此方法。这是一种验证或转换过滤器值的好方法。例如,让我们看看NumberFilterObject
中的fromArray
函数。
public static function fromArray(array $properties = []): self { return new static( is_null($properties['value'] ?? null) ? null : floatval($properties['value']), static::operatorFromProperties($properties) ); }
在这里,我们可以看到我们允许null值,并尝试将值转换为浮点数。我们还根据提供的operator
键值对获取Operator
枚举实例。
验证/查询复杂度
如果不验证或限制提供的查询,用户可能会构建非常昂贵的查询,即使没有恶意意图。目前,此库不尝试验证或计算查询的复杂度。作为替代方案,可以设置最大过滤器深度和最大过滤器总数。深度是指AbstractFilter
的嵌套实例:例如,每个嵌套的and
或or
过滤器会增加深度1。过滤器总数由AbstractFitler
和AbstractFilterObject
的实例数确定。
回到文件顶部的示例,我们有一个深度为1(因为有1个嵌套的AbstractFilter
实例,由or
语句生成),总过滤器数量为3(or
语句1个,每个name
约束1个)。默认情况下,最大深度设置为10,允许的最大过滤器数量为100,但可以通过更改默认属性值进行修改。
... class ProductFilter extends AbstractFilter { protected ?int $max_depth = 5; protected ?int $max_filters = 50; } ...
要禁用这些限制,只需将属性值设置为null。
测试
vendor/bin/phpunit
许可协议
MIT许可协议(MIT)。有关更多信息,请参阅许可文件。