sebastian/comparator 源码级指南:PHP 值相等性比较组件的安装、原理与自定义扩展

发布时间:2026/10/15 6:44:28
sebastian/comparator 源码级指南:PHP 值相等性比较组件的安装、原理与自定义扩展
开发工具测试【免费下载链接】comparatorProvides the functionality to compare PHP values for equality.项目地址https://gitcode.com/gh_mirrors/co/comparator点击查看免费下载sebastian/comparator 是 Sebastian Bergmann 出品的 PHP 值相等性equality比较组件为 PHPUnit 等测试框架提供了一套可扩展、类型感知的比较基础设施它通过Factory为任意一对 PHP 值自动挑选合适的比较器再以统一的assertEquals()接口完成比较并产出结构化的差异信息。读完本文你将掌握它的安装方式、核心 API 用法、内置比较器的优先级与行为差异、delta/canonicalize/ignoreCase参数的实际语义并能够编写自定义比较器接入自己的测试与业务代码。一、组件定位与安装1.1 它解决什么问题PHP 原生提供的比较运算符、语义过于简单0 Foobar这类字符串与数字的意外相等、浮点数精度误差、时区不同的两个DateTime是否代表同一时刻、数组键顺序是否影响相等性等问题都不是原生运算符能妥善回答的。sebastian/comparator 以 src/Factory.php 为入口根据传入值的类型分发到 src/Comparator.php 派生出的各专用比较器统一回答两个值是否相等以及若不相等差异到底是什么。1.2 环境要求与依赖从 composer.json 可以看到当前版本的硬性要求PHP 8.4sebastian/diff^9.0.1用于在ComparisonFailure中生成 Expected/Actual 的统一差异diffsebastian/exporter^8.2.1负责把比较失败的期望值与实际值导出为可读字符串扩展ext-dom与ext-mbstring可选建议ext-bcmath仅在需要比较BcMath\Number高精度数字对象时启用详见 src/NumberComparator.php。1.3 安装命令按 README.md 的说明通过 Composer 以项目级依赖引入composer require sebastian/comparator若仅在开发阶段使用例如运行测试套件建议作为开发时依赖安装composer require --dev sebastian/comparator二、快速上手Factory 驱动的比较流程README.md 给出的用法示例完整展现了标准调用链先原样继承并逐步解读?php use SebastianBergmann\Comparator\Factory; use SebastianBergmann\Comparator\ComparisonFailure; $date1 new DateTime(2013-03-29 04:13:35, new DateTimeZone(America/New_York)); $date2 new DateTime(2013-03-29 03:13:35, new DateTimeZone(America/Chicago)); $factory new Factory; $comparator $factory-getComparatorFor($date1, $date2); try { $comparator-assertEquals($date1, $date2); print Dates match; } catch (ComparisonFailure $failure) { print Dates dont match; }这段代码背后蕴含三个关键步骤new Factory构造时立即注册全部默认比较器见 src/Factory.phpgetComparatorFor($expected, $actual)按自定义比较器优先、默认比较器其次的顺序遍历返回第一个accepts($expected, $actual)为true的比较器src/Factory.phpassertEquals()执行比较不相等时抛出ComparisonFailure异常。示例中两个日期虽然时区不同但代表同一时刻New York 04:13:35 与 Chicago 03:13:35 相差 1 小时因此输出Dates match。这正是DateTimeComparator时区无关、按真实时刻比较的设计意图对应源码 src/DateTimeComparator.php 中先将两侧统一转换到 UTC 再与delta构造的时间窗比对。三、核心架构Comparator 抽象基类与 Factory 的职责3.1 Comparator两个抽象方法定义契约所有比较器都继承自抽象类 src/Comparator.phpabstract public function accepts(mixed $expected, mixed $actual): bool声明该比较器能处理哪种类型组合Factory依赖它做分发abstract public function assertEquals(mixed $expected, mixed $actual, float $delta 0.0, bool $canonicalize false, bool $ignoreCase false): void执行比较不相等时抛出ComparisonFailure。基类还提供了两个final受保护方法contextLines()返回当前工厂配置的 diff 上下文行数未绑定工厂时默认为 3exporter()返回当前工厂配置的导出器未绑定工厂时新建Exporter。这一设计保证了自定义比较器也能复用全局配置。3.2 Factory默认比较器的注册顺序即优先级src/Factory.php 中registerDefaultComparator()按如下顺序注册默认比较器越靠前优先级越高顺序比较器处理对象1ClosureComparator两个Closure2MockObjectComparator两个 PHPUnit Mock 对象Stub3DateTimeComparator两个DateTime/DateTimeImmutable4DateIntervalComparator两个DateInterval5DOMNodeComparator两个DOMNode6SplObjectStorageComparator两个SplObjectStorage7ExceptionComparator两个Exception8EnumerationComparator两个同类UnitEnum9NumberComparator条件注册BcMath\Number仅当extension_loaded(bcmath)且 PHP 8.4.010ObjectComparator两个对象通用兜底11ResourceComparator两个资源句柄12ArrayComparator两个数组13NumericComparator数值型组合非双字符串14ScalarComparator标量 /null/ 含__toString()的对象15TypeComparator任意值最终兜底按类型严格比对注意一个精妙细节NumericComparator和ScalarComparator的注册顺序在TypeComparator之前、ArrayComparator之后——例如0与0这种字符串 vs 数字组合会命中NumericComparator因为is_numeric(0)为真而两个纯字符串0与0则会落到ScalarComparatorNumericComparator::accepts()明确排除两个都是字符串的情况见 src/NumericComparator.php。这一映射关系被 tests/unit/FactoryTest.php 的instanceProvider数据提供器逐条断言验证。3.3 getComparatorFor 的查找逻辑与异常getComparatorFor()先遍历自定义比较器$this-customComparators再遍历默认比较器命中即返回若全部不命中则抛出RuntimeExceptionNo suitable Comparator implementation found但因为有TypeComparator其accepts()恒为true见 src/TypeComparator.php兜底正常情况不会触发。TypeComparator::assertEquals()用gettype()严格比较两侧类型类型不同即抛出ComparisonFailure。3.4 Factory 的高级配置src/Factory.php 还暴露了以下扩展点getInstance()单例静态单例入口PHPUnit 内部即使用单例FactoryTest::testIsSingletontests/unit/FactoryTest.php验证了这一点setContextLines(int)配置失败信息 diff 的上下文行数默认3tests/unit/FactoryTest.php 验证默认值与修改行为setExporter(Exporter)替换值导出器可自定义对象如何被呈现为字符串测试testDefaultComparatorUsesExporterConfiguredForFactory展示了传入自定义SampleClassExporter后失败信息变为SampleClass(4)而非完整属性列表见 tests/unit/FactoryTest.phprecordClosureComparison()/closureComparisonOccurred()/resetClosureComparisonTracking()供ClosureComparator内部使用的闭包比较追踪用于检测是否发生过闭包比较即使闭包嵌套在数组或对象内部tests/unit/FactoryTest.php 均验证追踪生效。四、内置比较器逐个拆解4.1 ScalarComparator标量比较与总是按字符串比较策略src/ScalarComparator.php 接受标量、null以及字符串 含__toString()的对象组合。其核心策略是只要一侧是字符串且另一侧非布尔就把两侧都转成字符串再比较从而避免0 Foobar这类原生宽松比较的诡异行为源码注释直白说明了动机见 src/ScalarComparator.php。ignoreCase开启时使用mb_strtolower(..., UTF-8)做大小写无关比较。字符串不等时它会裁掉超长40 字符的公共前缀/后缀仅保留差异附近的 25 个字符上下文避免 diff 输出被大段相同文本淹没。4.2 NumericComparator数值比较与 delta 容差src/NumericComparator.php 处理所有数值组合但排除两个都是字符串。它有三个显著行为delta容差abs($actual - $expected) $delta才判定失败解决浮点误差问题INF特判同为正无穷/同为负无穷视为相等一正一负或与有限值混比则失败NaN特判任一侧为NaN直接失败因为NaN与任何数都不相等。测试集覆盖了进制字面量0x539、0o2471、科学计数法1.2e3、7E-10、acos(8)产生的NaN等边界tests/unit/NumericComparatorTest.php。4.3 NumberComparatorBcMath\Number 高精度比较src/NumberComparator.php 仅在 PHP 8.4 且启用ext-bcmath时注册src/Factory.php。它继承自ObjectComparator接受BcMath\Number与 int/数值字符串的任意混合组合非Number一侧会被自动包装为new Number(...)delta也按双方scale的最大值格式化为Number后再参与区间判定从而实现无精度损失的高精度数值比较。4.4 ArrayComparator键值对语义、键序无关与 canonicalizesrc/ArrayComparator.php 的注释明确三点语义数组相等 包含相同键值对键的顺序无关键值对的类型无关。实现上默认按期望数组中的每个键在实际数组中都存在且值相等逐个校验多余的键$remaining同样导致失败嵌套值会递归调用factory()-getComparatorFor()选取子比较器因此[2.3]vs[2.5]在delta 0.5下也能相等tests/unit/ArrayComparatorTest.php 验证了数组与对象嵌套的 delta 递归传递canonicalize当两侧都是 list 时用usort按类型排序后比较排序规则见typeOrder()与compare()null bool 数值 字符串 数组 对象从而忽略顺序非 list 时则对两侧ksort后比较忽略键顺序差异。canonicalize场景见 tests/unit/ArrayComparatorTest.php。4.5 ObjectComparator类一致、循环依赖防护与自定义表示src/ObjectComparator.php 是多数对象比较器的基类行为包括类必须一致$actual::class ! $expected::class直接失败循环依赖防护通过$processed数组记录已比较过的[$actual, $expected]配对正序、反序都会命中避免自引用对象无限递归——源码注释明确指出这是为了规避 maximum function nesting level reached 错误src/ObjectComparator.phpidentical 短路$actual $expected同一实例直接判定相等属性级比较委托ArrayComparator::assertEquals()比较toArray()展开的属性数组失败信息中Array (会被替换为{ClassName} Object自定义表示优先若配置的 exporter 对任一对象提供了自定义表示hasCustomRepresentationFor失败信息改用 exporter 的完整对象表示而非逐属性列表。4.6 DateTimeComparator时区无关的时刻比较src/DateTimeComparator.php 接受DateTime与DateTimeImmutable的任意组合。实现先将$actual与$expected均克隆并切到 UTC 时区再把delta秒支持小数如0.5秒构造为DateInterval生成期望时间窗[expected - delta, expected delta]落在窗内即相等。测试 tests/unit/DateTimeComparatorTest.php 覆盖了跨时区同刻New York 04:13:35 vs Chicago 03:13:35、delta容差10 秒、65 秒、100 秒、0.5 秒与 Unix 时间戳写法等场景。4.7 DateIntervalComparator以秒为单位的时长比较src/DateIntervalComparator.php 将两个DateInterval分别应用到 Unix 纪元new DateTimeImmutable(0)上换算成总秒数含微秒再按delta容差比较。源码注释特别说明月、年组件以固定锚点折算因此日历组件不同但折算时长相同的两个区间在容差内仍可能判等src/DateIntervalComparator.php。4.8 EnumerationComparator同类枚举的严格相等src/EnumerationComparator.php 要求两侧都是UnitEnum且类完全一致$expected::class $actual::class随后用判等否则抛出含枚举类名与 case 名的失败信息。4.9 DOMNodeComparator规范化后的 XML 比较src/DOMNodeComparator.php 对两个DOMNode先执行C14N(false, true)规范化消除命名空间前缀差异、去除空文本节点、合并相邻文本节点再重新加载、格式化输出最终比对规范化文本C14N不可用时回退到saveXML()序列化。ignoreCase开启时两侧统一mb_strtolower(..., UTF-8)。失败信息会区分 two DOM documents 与 two DOM nodes。4.10 ExceptionComparator忽略位置信息的异常比较src/ExceptionComparator.php 继承ObjectComparator并在toArray()中移除file、line、trace、string、xdebug_message等与异常身份无关的字段因此两个仅抛出位置不同的异常对象可以判等。4.11 MockObjectComparator剥离 PHPUnit 内部状态的 Mock 比较src/MockObjectComparator.php 针对 PHPUnit 的Stub对象在toArray()中剔除所有__phpunit_前缀的内部属性仅比较用户可见状态避免 Mock 内部记录干扰相等性判断。4.12 ClosureComparator基于反射的三维闭包比较src/ClosureComparator.php 是唯一直接继承Comparator而非ObjectComparator的闭包比较器。它通过ReflectionFunction从三个维度判断两个闭包是否等效declaration函数名、文件名、起始行、结束行完全一致bindinggetClosureScopeClass()类一致且getClosureThis()递归比较capturedStategetClosureUsedVariables()捕获变量递归比较。递归比较复用factory()-getComparatorFor()因此捕获变量里的数组、对象都能正确分派。任一维度不等即抛出ComparisonFailure失败信息明确给出两侧闭包的声明文件与行号如closure declared at /path/app.php:10。每次比较还会调用recordClosureComparison()供上层追踪。4.13 其余比较器速览ResourceComparatorsrc/ResourceComparator.php对两个资源句柄使用宽松!比较SplObjectStorageComparatorsrc/SplObjectStorageComparator.php双向验证两侧存储中的每个对象都能在对方offsetExists命中TypeComparatorsrc/TypeComparator.php终极兜底gettype()类型不同即失败。五、assertEquals 的三大参数语义所有比较器的assertEquals()统一签名对象类比较器还会追加一个内部使用的array $processed参数assertEquals(mixed $expected, mixed $actual, float $delta 0.0, bool $canonicalize false, bool $ignoreCase false)$delta容差数值比较NumericComparator与时长/时刻比较DateTimeComparator、DateIntervalComparator使用绝对差容差支持小数秒经数组递归传递给嵌套值tests/unit/ArrayComparatorTest.php。默认0.0表示精确比较。$canonicalize规范化仅对数组比较生效用于忽略顺序——list 排序后比较关联数组按键排序后比较src/ArrayComparator.php。默认false。$ignoreCase忽略大小写ScalarComparator对字符串做mb_strtolowerUTF-8DOMNodeComparator对规范化文本做同样处理。默认false。六、ComparisonFailure结构化失败信息与 diff每次比较失败都会抛出 src/ComparisonFailure.php它继承自RuntimeException提供丰富访问器getExpected()/getActual()原始期望值与实际值getExpectedAsString()/getActualAsString()导出为字符串后的表示getDiff()基于sebastian/diff的StrictUnifiedDiffOutputBuilder生成--- Expected / Actual统一 diff上下文行数由工厂的contextLines配置控制src/ComparisonFailure.phptoString()消息 diff 的完整拼接可直接用于日志输出。它实现了__serialize()/__unserialize()可被serialize()/unserialize()安全地跨进程传递如放入队列或存储即使调用栈中含不可序列化对象也能序列化成功tests/unit/ComparisonFailureTest.php 验证了普通序列化与含不可序列化栈参数的场景。getDiff()的典型输出形如--- Expected Actual | -A B七、自定义比较器从注册到实战当内置比较器无法满足业务语义时例如比较两个自定义值对象需要忽略某些字段可按三步扩展7.1 编写比较器继承Comparator或更省事的ObjectComparator/ArrayComparator并实现两个方法。参考仓库测试夹具 tests/_fixture/TestClassComparator.php继承ObjectComparator的空实现即构成一个合法的自定义比较器use SebastianBergmann\Comparator\Comparator; final class MyValueComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return $expected instanceof MyValue $actual instanceof MyValue; } public function assertEquals(mixed $expected, mixed $actual, float $delta 0.0, bool $canonicalize false, bool $ignoreCase false): void { // 自定义语义不相等时 throw new ComparisonFailure(...) } }7.2 注册与注销Factory::register()使用array_unshift将自定义比较器插入自定义列表头部因此它的accepts()会先于所有其他比较器被调用src/Factory.php 的注释明确说明优先级。对应 API$factory new Factory; $factory-register($myComparator); // 注册优先级最高 $factory-unregister($myComparator); // 注销恢复默认行为 $factory-reset(); // 清空全部自定义比较器这三者的行为由 tests/unit/FactoryTest.php 验证注册TestClassComparator后getComparatorFor()返回自定义比较器注销后回落为ObjectComparatorreset()后同样回落。7.3 自定义比较器与 Factory 配置联动自定义比较器通过基类注入的factory()访问contextLines()与exporter()因此全局配置自动生效。tests/unit/FactoryTest.php 验证了自定义比较器同样使用工厂配置的 exporter 生成失败字符串。八、运行测试与质量保障仓库自带完整的 PHPUnit 测试套件require-dev依赖phpunit/phpunit ^13.4.1见 composer.json配置位于 phpunit.xml。按比较器逐一覆盖Factory的分发矩阵与注册/注销/重置/单例/上下文行数/exporter/闭包追踪tests/unit/FactoryTest.php、DateTime的跨时区与 deltatests/unit/DateTimeComparatorTest.php、数组的键序与 canonicalizetests/unit/ArrayComparatorTest.php、数值的边界与容差tests/unit/NumericComparatorTest.php、闭包的三维等效性tests/unit/ClosureComparatorTest.php、失败信息的 diff 与序列化tests/unit/ComparisonFailureTest.php等。安装依赖后即可运行./tools/phpunit或vendor/bin/phpunit执行全套测试观察每个比较器的边界行为。九、典型应用场景小结测试断言作为 PHPUnit 断言底层的值比较引擎享受时区无关的时间比较、带容差的数值比较、忽略顺序的数组比较等测试友好语义对象等效性判断借助ExceptionComparator忽略堆栈位置、MockObjectComparator剥离 Mock 内部状态业务数据比对通过delta/canonicalize/ignoreCase三参数应对浮点误差、顺序无关集合、大小写差异等真实业务场景领域模型定制register()自定义比较器将特定值对象的业务相等性规则统一收敛到一处供全项目复用。通过 src/Factory.php、src/Comparator.php 与各比较器实现src/ 目录、tests/unit/ 测试套件的对照阅读你可以把 sebastian/comparator 从黑盒工具彻底变成可预测、可扩展的比较基础设施。赞分享开发工具测试【免费下载链接】comparatorProvides the functionality to compare PHP values for equality.项目地址https://gitcode.com/gh_mirrors/co/comparator点击查看免费下载相关推荐sebastian/comparator 组件深度解析Laravel 示例项目中的 PHP 值相等性比较架构与实践sebastian/comparator 组件深度解析Laravel 示例项目中的 PHP 值相等性比较架构与实践 sebastian/comparator示例工程数据库教程后端ShowDoc 项目中的 sebastian/comparatorPHP 值相等性比较组件原理与实践指南ShowDoc 项目中的 sebastian/comparatorPHP 值相等性比较组件原理与实践指南 导读 sebastian/comparator 是文档知识库后端前端Stack Overflow 公开数据集解析BigQuery posts_wiki_placeholder Wiki 占位帖表完整指南Stack Overflow 公开数据集解析BigQuery posts_wiki_placeholder Wiki 占位帖表完整指南 posts_wiki_开发工具测试上一篇FIO 存储性能测试解决方案从基础基准到企业级架构实战下一篇Celery 守护进程化部署完全指南init.d、systemd、supervisor 与 macOS launchd 实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考