☰
Doctrine Collections 表达式系统完全指南:Comparison、CompositeExpression 与 Criteria 组合查询实战
2026/10/10 8:29:20 网站建设 项目流程
  • 后端

【免费下载链接】collections

Collections Abstraction Library

项目地址:https://gitcode.com/gh_mirrors/co/collections
点击查看免费下载

导读

本文聚焦 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命名空间:

类命名空间职责
ComparisonExpr描述"某个字段与某个值之间的单个比较"
CompositeExpressionExpr用 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::ININ字段值在给定值列表中
Comparison::NINNIN字段值不在给定值列表中
Comparison::CONTAINSCONTAINS字段值包含给定字符串(子串)
Comparison::MEMBER_OFMEMBER_OF字段值(数组/集合)中包含给定元素
Comparison::STARTS_WITHSTARTS_WITH字段值以给定字符串开头
Comparison::ENDS_WITHENDS_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_AND
  • CompositeExpression::TYPE_OR
  • CompositeExpression::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:

  1. Value不能作为 AND/OR 的子表达式:如果子表达式中出现Value实例,抛'Values are not supported expressions as children of and/or expressions.'——因为Value只是裸值包装,不是真正的布尔条件;
  2. 每个子表达式必须是Expression实例:否则抛'No expression given to CompositeExpression.';
  3. 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(); // 输出 10

setFirstResult()返回$this支持链式调用;getFirstResult()返回int|null(构造器默认将firstResult初始化为 0)。语义等同于 SQL 的OFFSET,配合maxResults实现分页。

4.7 setMaxResults() / getMaxResults():最大返回条数

设置并读取本次Criteria最多返回的结果条数:

$criteria->setMaxResults(20); echo $criteria->getMaxResults(); // 输出 20

setMaxResults(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()内部流程为:

  1. 用ClosureExpressionVisitor把Criteria中的表达式树翻译成判定闭包;
  2. 若设置了orderings,用ClosureExpressionVisitor::sortByField()按字段与方向构造多级排序比较器并排序;
  3. 应用firstResult/maxResults偏移与截断;
  4. 返回一个新的、保留原键的集合。

测试用例 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')
多条件 ANDandWhere()连续追加,或CompositeExpression::TYPE_AND
多条件 ORorWhere()连续追加,或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

项目地址:https://gitcode.com/gh_mirrors/co/collections
点击查看免费下载
上一篇:Anteon日志轮转配置:避免监控系统自身日志占用过多磁盘空间
下一篇:Buzz 离线语音转文字:三平台安装、实时录音转字幕的 7 个常见问题一次讲清

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询