- 后端
【免费下载链接】collections
Collections Abstraction Library
导读
本文聚焦 Doctrine\Common\Collections(本仓库 Collections Abstraction Library)中用于声明式筛选集合的表达式(Expression)体系:Comparison(比较表达式)、CompositeExpression(组合表达式)与Criteria(查询条件)三者协同工作,构成一套与后端无关(in-memory 数组集合 / ORM / ODM 均可实现)的查询描述 API。读完本文,你将掌握如何构造单条比较条件、如何用 AND/OR/NOT 组合多条条件,以及如何通过Criteria的完整链式 API(where、andWhere、orWhere、orderBy、setFirstResult、setMaxResults)完成排序、分页与匹配,并理解这些表达式在底层如何被ClosureExpressionVisitor翻译成可执行的 PHP 闭包。
一、表达式体系概览:三个类如何协作
Doctrine\Common\Collections的筛选能力建立在三个核心类之上,它们全部位于Doctrine\Common\Collections\Expr与Doctrine\Common\Collections命名空间:
| 类 | 命名空间 | 职责 |
|---|---|---|
Comparison | Expr | 描述"某个字段与某个值之间的单个比较" |
CompositeExpression | Expr | 用 AND / OR / NOT 把多个表达式组合成一个表达式树 |
Criteria | 根命名空间 | 聚合"表达式 + 排序 + 分页"的完整查询条件对象 |
三者共同的类型契约是 src/Expr/Expression.php 中定义的Expression接口——它只要求一个方法visit(ExpressionVisitor $visitor): mixed,即"接受访问者",从而让表达式树可以被不同的访问者翻译成不同的目标查询语言(闭包、SQL、ODM 查询等)。从源码结构看,Comparison、CompositeExpression和Value都实现了该接口,构成了一个经典的访问者模式(Visitor Pattern)表达式树。
在集合层面,Criteria被设计为传给Selectable::matching(Criteria $criteria)的参数,见 src/Selectable.php:matching()会筛选出所有满足该Criteria的元素并返回一个保留键的新集合。这套 API 的设计目标正如接口注释所强调的——与后端无关:Expression的构造方式使得无论是内存集合(ArrayCollection)还是数据库背书的集合(ORM 的 SQL 查询)都能基于同一套表达式实现查询,应用层无需感知底层实现差异。
二、Comparison:13 个比较操作符
Doctrine\Common\Collections\Expr\Comparison用于创建将要交给Criteria使用的比较表达式。查看其源码 src/Expr/Comparison.php,它定义如下操作符常量:
| 常量 | 字符串值 | 语义 |
|---|---|---|
Comparison::EQ | = | 字段值等于给定值 |
Comparison::NEQ | <> | 字段值不等于给定值 |
Comparison::LT | < | 字段值小于给定值 |
Comparison::LTE | <= | 字段值小于等于给定值 |
Comparison::GT | > | 字段值大于给定值 |
Comparison::GTE | >= | 字段值大于等于给定值 |
Comparison::IS | = | 等于(与 EQ 无差别,见源码注释) |
Comparison::IN | IN | 字段值在给定值列表中 |
Comparison::NIN | NIN | 字段值不在给定值列表中 |
Comparison::CONTAINS | CONTAINS | 字段值包含给定字符串(子串) |
Comparison::MEMBER_OF | MEMBER_OF | 字段值(数组/集合)中包含给定元素 |
Comparison::STARTS_WITH | STARTS_WITH | 字段值以给定字符串开头 |
Comparison::ENDS_WITH | ENDS_WITH | 字段值以给定字符串结尾 |
值得一提的细节:
Comparison::IS与Comparison::EQ的常量值都是=,源码中明确注释 "no difference with EQ"。这是为兼容其他实现(如 ORM/ODM)保留的语义别名。
2.1 构造器签名与 Value 包装
Comparison的构造器签名为:
public function __construct(private string $field, private string $op, mixed $value)三个参数分别是:字段名($field)、操作符($op,传入上面任一常量)、比较值($value)。构造器内部有一个关键行为:如果传入的$value不是Value实例,会自动包装成Doctrine\Common\Collections\Expr\Value(见 src/Expr/Value.php)。Value同样是Expression的实现,它只负责"承载一个原始值",供访问者通过walkValue()取出。
一个最小可用的比较表达式:
use Doctrine\Common\Collections\Expr\Comparison; $expr = new Comparison('key', Comparison::EQ, 'value');2.2 各操作符的底层求值语义
表达式本身只是"描述",真正执行求值的是访问者。在本仓库的内存集合实现中,ClosureExpressionVisitor(src/Expr/ClosureExpressionVisitor.php)会把每个比较操作符翻译成一个返回布尔值的 PHP 闭包。其walkComparison()方法用match语句一一对应:
EQ/NEQ:使用严格比较===/!==;LT/LTE/GT/GTE:使用 PHP 原生的<、<=、>、>=比较运算符;IN/NIN:对字段值调用in_array($fieldValue, $value, is_scalar($fieldValue))——注意第三个严格参数取决于字段值是否为标量,即字段值是标量时走严格模式,非标量时走宽松模式;CONTAINS:将字段值与给定值都转成字符串后调用str_contains()判断子串关系;STARTS_WITH/ENDS_WITH:同样转字符串后调用str_starts_with()/str_ends_with();MEMBER_OF:将字段值视为数组(若不是数组则用iterator_to_array()转换),再以严格模式in_array($value, $fieldValues, true)判断给定值是否为其中一员;- 未知操作符会抛出
RuntimeException('Unknown comparison operator: ...')。
字段值提取统一走静态方法ClosureExpressionVisitor::getObjectFieldValue($object, $field):支持点号分隔的嵌套字段(如'address.city'),支持数组按键取值,对对象则用反射沿继承链向上查找属性(属性不存在时抛RuntimeException),最后通过Property::getRawValue()取值。这意味着比较表达式的字段路径在内存集合中既可以指向数组键,也可以指向对象的(含继承的)属性。
2.3 用 ExpressionBuilder 免去样板代码
直接new Comparison(...)略显冗长。Criteria::expr()返回一个共享的ExpressionBuilder单例(见 src/Criteria.php 与 src/ExpressionBuilder.php),为每个操作符提供了语义化工厂方法:
use Doctrine\Common\Collections\Criteria; $exprBuilder = Criteria::expr(); $eq = $exprBuilder->eq('price', 100); $gt = $exprBuilder->gt('price', 50); $in = $exprBuilder->in('status', ['open', 'pending']); $notIn = $exprBuilder->notIn('status', ['archived']); $like = $exprBuilder->contains('title', 'Doctrine'); $starts = $exprBuilder->startsWith('name', 'Do'); $ends = $exprBuilder->endsWith('name', 'ine'); $member = $exprBuilder->memberOf('tags', 'php'); $null = $exprBuilder->isNull('deletedAt'); $notNull = $exprBuilder->isNotNull('deletedAt');其中isNull()/isNotNull()是eq(..., null)/neq(..., null)的语义化封装。ExpressionBuilder还提供组合方法andX(...)、orX(...)、not(...),稍后介绍。
三、CompositeExpression:AND / OR / NOT 组合表达式
Doctrine\Common\Collections\Expr\CompositeExpression用于创建可交给Criteria的组合表达式。它的操作符常量如下:
CompositeExpression::TYPE_ANDCompositeExpression::TYPE_ORCompositeExpression::TYPE_NOT
构造器接受两个参数:组合类型与表达式数组:
use Doctrine\Common\Collections\Expr\CompositeExpression; use Doctrine\Common\Collections\Expr\Comparison; $expr1 = new Comparison('key', Comparison::EQ, 'value1'); $expr2 = new Comparison('key', Comparison::EQ, 'value2'); $and = new CompositeExpression(CompositeExpression::TYPE_AND, [$expr1, $expr2]);3.1 构造期的严格校验(源码级)
查看 src/Expr/CompositeExpression.php,构造器内部会逐项校验,违反规则直接抛RuntimeException:
Value不能作为 AND/OR 的子表达式:如果子表达式中出现Value实例,抛'Values are not supported expressions as children of and/or expressions.'——因为Value只是裸值包装,不是真正的布尔条件;- 每个子表达式必须是
Expression实例:否则抛'No expression given to CompositeExpression.'; NOT只允许一个子表达式:当类型为TYPE_NOT时,校验通过后的表达式数量必须恰好为 1,否则抛'Not expression only allows one expression as child.'。
这与原文档的说明一致:使用TYPE_OR和TYPE_AND时CompositeExpression接受多个表达式作为参数;使用NOT时只能提供一个表达式。同时,CompositeExpression是final readonly类,表达式列表通过getExpressionList()读取、类型通过getType()读取。
3.2 NOT 的合法用法示例
$notExpr = new CompositeExpression( CompositeExpression::TYPE_NOT, [new Comparison('key', Comparison::EQ, 'value')], );3.3 组合表达式的求值
ClosureExpressionVisitor::walkCompositeExpression()会把组合表达式翻译成组合闭包:
TYPE_AND:所有子闭包都返回true时整体为true(PHP 的array_all);TYPE_OR:任一子闭包返回true即为true(PHP 的array_any);TYPE_NOT:对唯一的子闭包结果取反。
对应实现见 src/Expr/ClosureExpressionVisitor.php。另外,ExpressionVisitor(src/Expr/ExpressionVisitor.php)是抽象访问者基类,定义了walkComparison、walkValue、walkCompositeExpression三个抽象方法与统一的dispatch(Expression $expr)入口——任何新的后端(如 ORM 的 SQL 生成器)都可以继承它把同一棵表达式树翻译成自己的查询语言,这正是"与后端无关"的架构基石。
四、Criteria API:表达式 + 排序 + 分页的完整查询条件
Doctrine\Common\Collections\Criteria是表达式的最终宿主。它既可以通过构造器一次性配置,也可以通过链式方法逐步构建。先看构造器(src/Criteria.php):
new Criteria( Expression|null $expression = null, // 初始 where 表达式 array|null $orderings = null, // 排序,形如 ['name' => Order::Ascending] int $firstResult = 0, // 起始偏移 int|null $maxResults = null, // 最大返回条数 );以下按原文档的 API 清单逐项展开。
4.1 where():设置 where 表达式
设置本次Criteria被搜索时要评估的 where 表达式:
use Doctrine\Common\Collections\Expr\Comparison; $expr = new Comparison('key', Comparison::EQ, 'value'); $criteria->where($expr);源码中where()直接替换内部表达式(返回$this支持链式)。当后续调用andWhere/orWhere时,若当前表达式为null,则直接委托给where()完成首次设置(见下方源码逻辑)。
4.2 andWhere():用 AND 追加条件
将新表达式与之前的表达式以AND方式组合:
$expr = new Comparison('key', Comparison::EQ, 'value'); $criteria->andWhere($expr);源码实现:如果已有表达式,则构造new CompositeExpression(TYPE_AND, [当前表达式, 新表达式])替换当前表达式;若还没有表达式,则等价于where($expr)。因此连续多次andWhere()会构建出一棵逐层嵌套的 AND 表达式树,语义等价于所有条件同时成立。
4.3 orWhere():用 OR 追加条件
将新表达式与之前的表达式以OR方式组合:
$expr1 = new Comparison('key', Comparison::EQ, 'value1'); $expr2 = new Comparison('key', Comparison::EQ, 'value2'); $criteria->where($expr1); $criteria->orWhere($expr2);源码实现与andWhere()对称:new CompositeExpression(TYPE_OR, [当前表达式, 新表达式])。上述代码最终等价于"key等于value1或key等于value2"。
4.4 链式组合 + 一次性构造的两种等价写法
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Order; // 写法一:链式方法 $criteria = Criteria::create() ->where(Criteria::expr()->eq('price', 100)) ->andWhere(Criteria::expr()->gt('stock', 0)) ->orderBy(['name' => Order::Ascending]) ->setFirstResult(0) ->setMaxResults(20); // 写法二:构造器一次性传入 $criteria = new Criteria( new CompositeExpression( CompositeExpression::TYPE_AND, [Criteria::expr()->eq('price', 100), Criteria::expr()->gt('stock', 0)], ), ['name' => Order::Ascending], 0, 20, );两种写法得到的Criteria等价。实际使用中,Criteria::create()是静态工厂方法(不带参数),而Criteria::expr()返回共享的表达式构建器。
4.5 orderBy() / getOrderings():结果排序
设置本次Criteria结果的排序方式:
use Doctrine\Common\Collections\Order; $criteria->orderBy(['name' => Order::Ascending]);orderBy()接收一个"字段 => 排序方向"的关联数组。排序方向在本仓库中有两个枚举来源:
Doctrine\Common\Collections\Order(src/Order.php):Order::Ascending = 'ASC'、Order::Descending = 'DESC'。该枚举被标记为@deprecated,推荐改用全局SortDirection枚举;SortDirection:SortDirection::Ascending/SortDirection::Descending,是当前推荐写法(测试用例 tests/CollectionTest.php 中new Criteria(null, ['foo' => SortDirection::Descending])即为此用法)。
源码中Criteria内部以Order|SortDirection混存排序配置,getOrderings()读取时会统一归一化为SortDirection返回;旧的orderings()方法(自 3.1 起@Deprecated)则反向归一化为Order。
在ArrayCollection::matching()的排序实现(src/ArrayCollection.php)中,每个排序字段会通过ClosureExpressionVisitor::sortByField($field, $orientation, $next)生成比较闭包:升序方向为1、降序方向为-1,多个字段时通过$next闭包逐级串联,实现"按多字段依次排序"。sortByField在字段值相等时委托给下一级比较闭包,因此排序字段的顺序就是排序优先级顺序。
4.6 setFirstResult() / getFirstResult():起始偏移
设置并读取本次Criteria应该从第几条结果开始返回:
$criteria->setFirstResult(0); // 之后 $criteria->setFirstResult(10); echo $criteria->getFirstResult(); // 输出 10setFirstResult()返回$this支持链式调用;getFirstResult()返回int|null(构造器默认将firstResult初始化为 0)。语义等同于 SQL 的OFFSET,配合maxResults实现分页。
4.7 setMaxResults() / getMaxResults():最大返回条数
设置并读取本次Criteria最多返回的结果条数:
$criteria->setMaxResults(20); echo $criteria->getMaxResults(); // 输出 20setMaxResults(int|null $maxResults)允许传null(表示不限条数,也是构造器默认值),等同于 SQL 的LIMIT。与firstResult组合即为经典分页:第 N 页的写法是setFirstResult(($page - 1) * $pageSize)->setMaxResults($pageSize)。
4.8 完整分页排序示例
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Order; // 查询 price >= 50 且 status 为 open 的商品, // 按 price 降序、name 升序,取第 2 页(每页 20 条) $criteria = Criteria::create() ->where(Criteria::expr()->gte('price', 50)) ->andWhere(Criteria::expr()->eq('status', 'open')) ->orderBy([ 'price' => Order::Descending, 'name' => Order::Ascending, ]) ->setFirstResult(20) // 第二页起始偏移 ->setMaxResults(20); // 每页 20 条 $products = $collection->matching($criteria);五、落地执行:matching() 与测试验证
5.1 从 Criteria 到筛选结果
Criteria本身不执行筛选,它只是"查询条件的描述"。执行方是实现了Selectable接口的集合(src/Selectable.php)的matching(Criteria $criteria)方法。以内存集合ArrayCollection为例(src/ArrayCollection.php),其matching()内部流程为:
- 用
ClosureExpressionVisitor把Criteria中的表达式树翻译成判定闭包; - 若设置了
orderings,用ClosureExpressionVisitor::sortByField()按字段与方向构造多级排序比较器并排序; - 应用
firstResult/maxResults偏移与截断; - 返回一个新的、保留原键的集合。
测试用例 tests/CollectionTest.php 对这条链路给出了可运行的验证:例如$this->collection->matching(new Criteria(Criteria::expr()->eq('fooBar', 42)))验证 EQ 筛选,new Criteria(null, ['fooBar' => Order::Ascending])验证排序(见 tests/ArrayCollectionTestCase.php),new Criteria(null, null, $firstResult, $maxResult)验证分页参数。这些测试同时覆盖了ArrayCollection与懒加载集合(AbstractLazyCollectionTest)两条实现路径。
5.2 为什么推荐用表达式 API 而不是手写循环
Selectable接口的设计目标(源码注释原文)是"提供一种与后端无关的从集合中获取元素的方式",Expression被刻意构造为"既能在内存集合上实现查询,也能在数据库背书集合上实现查询"。对数据库背书集合而言,matching()可以利用底层查询 API(如 ORM 的 SQL)高效执行,应用层无需直接操作EntityManager或 Repository。也就是说:同样的Criteria对象,在内存集合上翻译成闭包过滤,在 ORM 场景下翻译成 SQL,应用代码无需改动。
六、常用模式速查
| 需求 | 推荐写法 |
|---|---|
| 精确匹配 | Criteria::expr()->eq('field', $value) |
| 范围筛选 | Criteria::expr()->gte('price', 50)->andX(...)或where(gt)->andWhere(lte) |
| 多值匹配 | Criteria::expr()->in('status', ['a', 'b']) |
| 排除多值 | Criteria::expr()->notIn('status', ['x']) |
| 模糊包含 | Criteria::expr()->contains('title', 'Doctrine') |
| 前缀/后缀 | startsWith('name', 'Do')/endsWith('name', 'ine') |
| 集合成员判断 | Criteria::expr()->memberOf('tags', 'php') |
| 空值判断 | isNull('deletedAt')/isNotNull('deletedAt') |
| 多条件 AND | andWhere()连续追加,或CompositeExpression::TYPE_AND |
| 多条件 OR | orWhere()连续追加,或CompositeExpression::TYPE_OR |
| 取反 | CompositeExpression::TYPE_NOT(仅允许一个子表达式) |
| 排序 | orderBy(['name' => Order::Ascending, 'price' => SortDirection::Descending]) |
| 分页 | setFirstResult($offset)->setMaxResults($limit) |
七、补充说明与最佳实践
- 保持表达式树扁平:
andWhere()/orWhere()的连续调用会在内部逐层嵌套CompositeExpression,逻辑上等价于同层 AND/OR,但如需更清晰的树形结构(例如(A AND B) OR C这种混合优先级),建议直接构造嵌套的CompositeExpression或使用ExpressionBuilder::andX() / orX() / not()。 ExpressionBuilder的跨实现注意:其源码注释(src/ExpressionBuilder.php)明确提醒——为保证可互操作的代码,比较应只使用标量值,否则 Array、ORM、ODM 各实现之间的比较行为可能不一致。- 只读语义:
matching()返回的是新集合(保留原键),不会修改原集合内容,适合在函数式、不可变风格的业务代码中安全使用。 - 遗留 API 注意:
Order枚举与Criteria::orderings()已标记弃用(自 3.1 起),新代码请使用SortDirection与getOrderings()。
参考资料(仓库内可继续深入)
- 表达式接口与实现:src/Expr/Expression.php、src/Expr/Comparison.php、src/Expr/CompositeExpression.php、src/Expr/Value.php
- 访问者与执行器:src/Expr/ExpressionVisitor.php、src/Expr/ClosureExpressionVisitor.php
- 查询条件与构建器:src/Criteria.php、src/ExpressionBuilder.php、src/Order.php、src/Selectable.php
- 集合执行实现:src/ArrayCollection.php
- 测试用例:tests/CollectionTest.php、tests/ArrayCollectionTestCase.php、tests/Expr/ComparisonTest.php、tests/Expr/CompositeExpressionTest.php
- 后端
【免费下载链接】collections
Collections Abstraction Library
相关推荐
ScriptCat脚本猫与油猴的终极对比:为什么你应该选择脚本猫
ScriptCat脚本猫与油猴的终极对比:为什么你应该选择脚本猫 在浏览器扩展的世界里,用户脚本工具一直是提升网页体验的神器。作为两款主流的用户脚本管理器, S
前端开发者工具插件系统Watchman 查询表达式中的 allof 聚合:逻辑与(AND)组合的完整指南
Watchman 查询表达式中的 allof 聚合:逻辑与(AND)组合的完整指南 导读 allof 是 Watchman 查询表达式(Expression T
后端开发工具Doctrine Collections表达式访问者模式:ClosureExpressionVisitor深度剖析
Doctrine Collections表达式访问者模式:ClosureExpressionVisitor深度剖析 Doctrine Collections库是
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考