lmc / api-filter
API查询参数的过滤器解析器/构建器。
Requires
- php: ^8.0
- ext-mbstring: *
- beberlei/assert: ^3.0
- mf/collections-php: ^6.0
Requires (Dev)
- doctrine/orm: ^2.7
- ergebnis/composer-normalize: ^2.5
- lmc/coding-standard: ^3.0
- mockery/mockery: ^1.4
- php-parallel-lint/php-parallel-lint: ^1.2
- phpstan/extension-installer: ^1.0
- phpstan/phpstan: ^0.12.23
- phpstan/phpstan-beberlei-assert: ^0.12.2
- phpstan/phpstan-mockery: ^0.12.5
- phpunit/phpunit: ^9.5
Suggests
- doctrine/orm: To allow applying filters directly to QueryBuilder
README
API查询参数的过滤器解析器/构建器。
它只是一个过滤器解析器/构建器,不是一个业务逻辑的地方,因此如果您想对过滤器更加严格,应该由您的类包装。同样,如果您想为每个实体/表设置不同的设置,也应该通过围绕此库的特定包装器来完成。
目录
安装
composer require lmc/api-filter
用法
例如,让我们从以下请求中获取查询参数
GET http://host/endpoint/?field=value
初始化
// in DI container/factory $apiFilter = new ApiFilter(); $apiFilter->registerApplicator(...); // optional, when you want to use non-standard implementation // in service/controller/... $filters = $apiFilter->parseFilters($request->query->all()); // [ // 0 => Lmc\ApiFilter\Filter\FilterWithOperator { // private $title => 'eq' // private $operator => '=' // private $column => 'field' // private $value => Lmc\ApiFilter\Entity\Value { // private $value => 'value' // } // } // ]
使用Doctrine Query Builder Applicator
- 需要安装
doctrine/orm
- 应用过滤器使用 克隆
QueryBuilder
-> 原始QueryBuilder
是 未更改
示例
// in EntityRepository/Model $queryBuilder = $this->createQueryBuilder('alias'); $queryBuilder = $apiFilter->applyFilters($filters, $queryBuilder); // or one by one foreach ($filters as $filter) { $queryBuilder = $apiFilter->applyFilter($filter, $queryBuilder); } // get prepared values for applied filters $preparedValues = $apiFilter->getPreparedValues($filters, $queryBuilder); // ['field_eq' => 'value'] // get query $queryBuilder ->setParameters($preparedValues) ->getQuery();
更简短的示例 (与☝相同)
// in EntityRepository/Model $queryBuilder = $this->createQueryBuilder('alias'); $apiFilter ->applyFilters($filters, $queryBuilder) // query builder with applied filters ->setParameters($apiFilter->getPreparedValues($filters, $queryBuilder)) // ['field_eq' => 'value'] ->getQuery();
使用 SQL Applicator
- ❗这只是一个 原始实现,应谨慎使用❗
- 它仍然可以在没有
ORDER BY
、GROUP BY
等SQL
的简单情况下使用,因为它只是将过滤器作为WHERE
条件添加
SQL Applicator
必须显式注册
// in DI container $apiFilter->registerApplicator(new SqlApplicator(), Priority::MEDIUM);
示例
// in Model/EntityRepository $sql = 'SELECT * FROM table'; $sql = $apiFilter->applyFilters($filters, $sql); // "SELECT * FROM table WHERE 1 AND field = :field_eq" // or one by one foreach ($filters as $filter) { $sql = $apiFilter->applyFilter($filter, $sql); } // get prepared values for applied filters $preparedValues = $apiFilter->getPreparedValues($filters, $sql); // ['field_eq' => 'value'] // execute query $stmt = $connection->prepare($sql); $stmt->execute($preparedValues);
更简短的示例 (与☝相同)
// in EntityRepository/Model $sql = 'SELECT * FROM table'; $stmt = $connection->prepare($apiFilter->applyFilters($filters, $sql)); // SELECT * FROM table WHERE 1 AND field = :field_eq $stmt->execute($apiFilter->getPreparedValues($filters, $sql)); // ['field_eq' => 'value']
支持的过滤器
等于 - EQ (=)
GET http://host/endpoint/?field[eq]=value GET http://host/endpoint/?field=value
两个示例 ☝ 是相同的
不等于 - NEQ (!=)
GET http://host/endpoint/?field[neq]=value
大于 - GT (>)
GET http://host/endpoint/?field[gt]=value
大于等于 - GTE (>=)
GET http://host/endpoint/?field[gte]=value
小于 - LT (<)
GET http://host/endpoint/?field[lt]=value
小于等于 - LTE (<=)
GET http://host/endpoint/?field[lte]=value
IN
GET http://host/endpoint/?type[in][]=one&type[in][]=two
IN
过滤器不允许使用Tuples
函数
GET http://host/endpoint?fullName=(Jon,Snow)
- 您可以在 这里 看到使用
functions
的更多选项和可能性
过滤器中的 Tuples
元组
- 在过滤器中非常重要,如果您有一些必须一起发送的值
- 由两个或多个值组成(
Tuple
中一个值的元组只是一个值) - 项目 必须 在
(
)
中,并用,
分隔Tuple
中的array
必须 在[
]
中,并用;
分隔项目
- 建议不要在值之间使用 空格,因为 URL 的特定行为
- 有关
Tuples
的更多信息,请参阅 https://github.com/MortalFlesh/MFCollectionsPHP#immutabletuple
包含 Tuple
的列
由 Tuple
声明的列的行为与单个值相同,但其值也必须是 Tuple
。列可以为每个值包含过滤器规范。
- 默认过滤器是单个值的
EQ
,以及值数组的IN
(在Tuple
中)
包含 Tuple
的值
在元组
中的值必须与列数相同。值可以包含对元组
中所有值的筛选规格。
❗注意:筛选规格不能同时出现在列和值中。
用法
GET http://host/endpoint/?(first,second)[eq]=(one,two)
☝ 表示您有两个列 first
和 second
,它们必须一起发送。列 first
必须等于值 "one"
,列 second
必须等于值 "two"
。
示例
❗为了示例的简洁性,它们显示在SQL 应用程序
上,该应用程序不是自动注册的❗
IN
+ EQ
筛选
GET http://host/person/?type[in][]=student&type[in][]=admin&name=Tom
$parameters = $request->query->all(); // [ // "type" => [ // "in" => [ // 0 => "student" // 1 => "admin" // ] // ], // "name" => "Tom" // ] $filters = $apiFilter->parseFilters($parameters); $sql = 'SELECT * FROM person'; foreach ($filters as $filter) { $sql = $apiFilter->applyFilter($filter, $sql); // 0. SELECT * FROM person WHERE 1 AND type IN (:type_in_0, :type_in_1) // 1. SELECT * FROM person WHERE 1 AND type IN (:type_in_0, :type_in_1) AND name = :name_eq } $preparedValues = $apiFilter->getPreparedValues($filters, $sql); // [ // 'type_in_0' => 'student', // 'type_in_1' => 'admin', // 'name_eq' => 'Tom', // ]
GT
+ LT
筛选(介于两者之间)
GET http://host/person/?age[gt]=18&age[lt]=30
$parameters = $request->query->all(); // [ // "age" => [ // "gt" => 18 // "lt" => 30 // ], // ] $filters = $apiFilter->parseFilters($parameters); $sql = 'SELECT * FROM person'; $sql = $apiFilter->applyFilters($filters, $sql); // SELECT * FROM person WHERE 1 AND age > :age_gt AND age < :age_lt $preparedValues = $apiFilter->getPreparedValues($filters, $sql); // ['age_gt' => 18, 'age_lt' => 30]
EQ
与 元组
GET http://host/person/?(firstname,surname)=(John,Snow)
$parameters = $request->query->all(); // ["(firstname,surname)" => "(John,Snow)"] $sql = 'SELECT * FROM person'; $filters = $apiFilter->parseFilters($parameters); // [ // 0 => Lmc\ApiFilter\Filter\FilterWithOperator { // private $title => "eq" // private $operator => "=" // private $column => "firstname" // private $value => Lmc\ApiFilter\Entity\Value { // private $value => "John" // } // }, // 1 => Lmc\ApiFilter\Filter\FilterWithOperator { // private $title => "eq" // private $operator => "=" // private $column => "surname" // private $value => Lmc\ApiFilter\Entity\Value { // private $value => "Snow" // } // } // ] $sql = $apiFilter->applyFilters($filters, $sql); // SELECT * FROM person WHERE 1 AND firstname = :firstname_eq AND surname = :surname_eq $preparedValues = $apiFilter->getPreparedValues($filters, $sql); // ['firstname_eq' => 'John', 'surname_eq' => 'Snow']
更多示例
等于(隐式和显式)
GET http://host/person/?fullName=Jon Snow GET http://host/person/?fullName[eq]=Jon Snow
结果
- column: fullName filters: eq value: Jon Snow
多个筛选(隐式和显式)
通过单个值
GET http://host/person/?firstName=Jon&surname=Snow GET http://host/person/?firstName[eq]=Jon&surname[eq]=Snow
通过元组
GET http://host/person/?(firstName,surname)=(Jon,Snow) GET http://host/person/?(firstName,surname)[eq]=(Jon,Snow) GET http://host/person/?(firstName[eq],surname[eq])=(Jon,Snow)
结果
- column: firstName filters: eq value: Jon - column: surname filters: eq value: Snow
多个筛选
您可以将所有类型的筛选(元组、显式、隐式)混合使用。
通过通用筛选找到完美的妻子
通过单个值
GET http://host/person/?age[gte]=18&age[lt]=30&category[in][]=serious&category[in][]=marriage&sense-of-humor=true
通过元组
GET http://host/person/?(age[gte],age[lt],category,sense-of-humor)=(18,30,[serious;marriage],true)
结果
- column: age filters: gte value: 18 - column: age filters: lt value: 30 - column: category filters: in value: [ serious, marriage ] - column: sense-of-humor filters: eq value: true
通过通用筛选找到想看电影的
通过单个值
GET http://host/movie/?year[gte]=2018&rating[gte]=80&genre[in][]=action&genre[in][]=fantasy
通过元组
GET http://host/movie/?(year[gte],rating[gte],genre)=(2018,80,[action;fantasy])
结果
- column: year filters: gte value: 2018 - column: rating filters: gte value: 80 - column: genre filters: in value: [ action, fantasy ]
过滤器中的函数
使用函数可以处理所有可能的问题,这些问题可能只用简单的筛选(如 eq
等)会有问题。
fullName
函数示例
让我们看看如何使用函数以及需要做什么。我们将在示例中展示。
预期的API
GET http://host/endpoint?fullName=(Jon,Snow)
☝️ 上面的示例显示了我们要提供给消费者的内容。它既简单又明确。
它甚至可能隐藏一些内部差异,例如,对于简单筛选,数据库列必须与筛选中的字段同名,但对于函数,我们可以更改它。
假设在数据库中我们有一些类似的内容
type Person = { first_name: string lastname: string }
初始化
首先,您必须定义您想要使用的函数。
// in DI container/factory $apiFilter = new ApiFilter(); $apiFilter->declareFunction( 'fullName', [ new ParameterDefinition('firstName', 'eq', 'first_name'), // parameter name and field name are different, so we need to define it 'lastname`, // parameter name and field name are the same and we use the implicit `eq` filter, so it is defined simply ] );
declareFunction
方法将根据参数创建具有筛选的函数。
还有一个 registerFunction
方法,它允许您传递任何您想要的函数。这可能在使用根本不需要筛选功能或有一些自定义存储等情况时很有用。
解析和应用筛选
现在当请求带有 ?fullName=(Jon,Snow)
时,ApiFilter
可以将其解析为
// in service/controller/... $sql = 'SELECT * FROM person'; $filters = $apiFilter->parseFilters($request->query->all()); // [ // 0 => Lmc\ApiFilter\Filter\FilterFunction { // private $title => 'function' // private $column => 'fullName' // private $value => Lmc\ApiFilter\Entity\Value { // private $value => Closure // } // }, // // 1 => Lmc\ApiFilter\Filter\FunctionParameter { // private $title => 'function_parameter' // private $column => 'firstName' // private $value => Lmc\ApiFilter\Entity\Value { // private $value => 'Jon' // } // }, // // 2 => Lmc\ApiFilter\Filter\FunctionParameter { // private $title => 'function_parameter' // private $column => 'lastname' // private $value => Lmc\ApiFilter\Entity\Value { // private $value => 'Snow' // } // } // ] $appliedSql = $apiFilter->applyFilters($filters, $sql); // SELECT * FROM person WHERE first_name = :firstName_function_parameter AND lastname = :lastname_function_parameter $preparedValues = $apiFilter->getPreparedValues($filters, $sql); // [ // 'firstName_function_parameter' => 'Jon', // 'lastname_function_parameter' => 'Snow', // ]
支持的功能使用
以下所有示例的结果相同。我们有很多选项,因此我们可以允许尽可能多的不同消费者。
### Explicit function call GET http://host/endpoint?fullName=(Jon,Snow) ### Explicit function call with values GET http://host/endpoint?function=fullName&firstName=Jon&lastname=Snow ### Implicit function call by values GET http://host/endpoint?firstName=Jon&lastname=Snow ### Explicit function call by tuple GET http://host/endpoint?(function,firstName,surname)=(fullName, Jon, Snow) ### Implicit function call by tuple GET http://host/endpoint?(firstName,surname)=(Jon, Snow) ### Explicit function call by filter parameter GET http://host/endpoint?filter[]=(fullName,Jon,Snow)
功能参数定义
要 声明
或 注册
函数,您必须定义其参数。有许多方法/需求来完成此操作。
定义为字符串
这是最简单的方法。您只需定义一个名称。
这意味着
- 您想要
eq
筛选(或IN
用于数组)以及列名和参数名相同 - 此参数的值是必需的
$apiFilter->declareFunction('fullName', ['firstName', 'surname']);
定义为数组
这允许您为参数传递更多选项。
只有一个项目
如果您只通过给出唯一的项目来声明它,它就与上面的字符串定义相同。
$apiFilter->declareFunction('fullName', [['firstName'], ['surname']]);
多个项目
这意味着
firstName
参数使用eq
筛选,在存储中有first_name
列,并且是必需的surname
参数使用eq
筛选,在存储中有lastname
列,其值为Snow
(它将始终使用,无法用其他值覆盖)
$apiFilter->declareFunction('fullName', [ ['firstName', 'eq', 'first_name'], ['surname', 'eq', 'lastname', 'Snow'] ]);
定义为对象
这允许您传递与数组相同的选项,但以显式定义的对象形式。 (它还具有一些特殊的构造方法来简化创建)
$apiFilter->declareFunction('fullName', [ new ParameterDefinition('firstName', 'eq', 'first_name'), new ParameterDefinition('surname', 'eq', 'lastname, new Value('Snow')) ]);
组合
可以将所有选项组合起来以最好地适应参数。
声明
$apiFilter->declareFunction('fullNameGrownMan', [ ['firstName', 'eq', 'first_name'], 'surname', ['age', 'gte', 'age', 18], ParameterDefinition::equalToDefaultValue('gender', new Value('male')), ]);
用法
GET http://endpoint/host?fullNameGrownMan=(Jon,Snow)
注册和执行函数
下面的示例只是为了明确说明,您可能永远不会允许执行这样的 SQL 查询。
在PHP中的使用
// in DI container/factory $apiFilter = new ApiFilter(); $apiFilter->registerFunction( 'sql', ['query'], function (\PDO $client, FunctionParameter $query): \PDOStatement { return $client->query($query->getValue()->getValue()); } ); // in service/controller/... $statement = $apiFilter->executeFunction('sql', $queryParameters, $client); // \PDOStatement $statement->execute(); // fetch result, etc...
API 的使用
以下所有示例的结果相同。我们有很多选项,因此我们可以允许尽可能多的不同消费者。
### Explicit function call GET http://endpoint/host?sql=SELECT * FROM person ### Explicit function call with values GET http://host/endpoint?function=sql&query=SELECT * FROM person ### Implicit function call by values GET http://host/endpoint?query=SELECT * FROM person ### Explicit function call by tuple GET http://host/endpoint?(function,query)=(sql, SELECT * FROM person) ### Explicit function call by filter parameter GET http://host/endpoint?filter[]=(sql, SELECT * FROM person)
异常和错误处理
已知的 ApiFilter 实现中发生的异常发生 Lmc\ApiFilter\Exception\ApiFilterExceptionInterface
。异常树是
请注意,如果您将自定义的应用器注册到ApiFilter(通过$apiFilter->registerApplicator()
),它可能会抛出其他异常,这些异常可能未实现ApiFilterExceptionInterface
。
开发
安装
composer install
测试
composer all
待办事项
- defineAllowed:(这应该在DI级别上)
- 字段(列)
- 过滤器
- 值
- 添加更多示例
- 每个实体/表的不同配置