- 开发工具
- 测试
【免费下载链接】exporter
Provides the functionality to export PHP variables for visualization
sebastian/exporter 是一个用于将 PHP 变量(标量、数组、对象、资源、枚举等)导出为可读文本的组件,其输出比print_r()更规范、可预测。本文以 README.md 为主线,结合仓库源码与测试用例,系统讲解基础导出、简洁导出、对象自定义导出(ObjectExporter接口)、链式组合(ObjectExporterChain)与ExportContext递归上下文机制,帮助读者在实际调试、日志输出与测试命名场景中正确使用该库。
安装
sebastian/exporter 通过 Composer 作为项目级依赖安装。它要求 PHP >= 8.4,并依赖ext-mbstring与sebastian/recursion-context(见 composer.json)。
composer require sebastian/exporter若仅在开发阶段使用(例如运行测试套件时输出断言差异),建议作为开发期依赖安装:
composer require --dev sebastian/exporter安装后,所有类位于SebastianBergmann\Exporter命名空间(src/ 目录通过 classmap 自动加载),核心入口为Exporter类。
基础导出:Exporter::export()
Exporter::export()将任意值导出为格式化字符串。与print_r()相比,其输出在多个方面做了规范化(见 src/Exporter.php 的文档注释):
null渲染为null(而非空字符串)true渲染为true(而非1)false渲染为false(而非空字符串)- 字符串一律用单引号包裹
- 回车与换行被规范化为
\n - 递归与重复出现被妥善处理(见下文"重复出现"一节)
最简单的用法:
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter; /* Exception Object #4 ( 'message' => '', 'string' => '', 'code' => 0, 'file' => '/home/sebastianbergmann/test.php', 'line' => 34, 'previous' => null, ) */ print $exporter->export(new Exception);注意异常对象的trace属性(\0Exception\0trace)不会被导出——toArray() 中显式跳过它,因为 trace 常引用内存中成百上千个对象,纳入输出会带来严重性能开销。
导出简单类型
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter; // 46 print $exporter->export(46); // 4.0 print $exporter->export(4.0); // 'hello, world!' print $exporter->export('hello, world!'); // false print $exporter->export(false); // NAN print $exporter->export(acos(8)); // -INF print $exporter->export(log(0)); // null print $exporter->export(null); // resource(13) of type (stream) print $exporter->export(fopen('php://stderr', 'w')); // Binary String: 0x000102030405 print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));对应源码中的处理逻辑(recursiveExport())为:
- 浮点数:
exportFloat()显式区分NAN、INF、-INF,其余数值临时将 PHPprecision设为-1以避免精度截断,且仅在缺少小数点和科学计数法时追加.0(exportFloat())。因此4.0不会变成4,测试数据(tests/ExporterTest.php)验证了1.0、1.2、NAN、INF、-INF等输出。 - 资源:已关闭的资源输出为
resource (closed),正常资源输出为resource(%d) of type (%s),其中%d是资源 ID、%s来自get_resource_type()。 - 字符串:可打印字符按规范输出;含 NUL 字节或不可打印字节占比超过 30% 时,输出
Binary String: 0x开头的紧凑十六进制转储;大部分可打印但夹杂控制字符的字符串,则输出Binary String: "..."并仅内联转义违规字节(如Hello\x01World,见 exportString() 与 tests/ExporterTest.php)。
导出复杂类型:数组与对象的递归与引用
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter; /* Array &0 [ 0 => Array &1 [ 0 => 1, 1 => 2, 2 => 3, ], 1 => Array &2 [ 0 => '', 1 => 0, 2 => false, ], ] */ print $exporter->export([[1, 2, 3], ['', 0, false]]); /* Array &0 [ 'self' => Array &1 [ 'self' => Array &1, ], ] */ $array = []; $array['self'] = &$array; print $exporter->export($array); /* stdClass Object #4 ( 'self' => stdClass Object #4, ) */ $obj = new stdClass; $obj->self = $obj; print $exporter->export($obj);这段输出揭示了两个重要机制:
- 引用编号:每个数组首次出现时被分配
Array &N编号(N从 0 开始递增);对象首次出现时被分配Object #N编号(N是spl_object_id())。这一机制由ExportContext内部的RecursionContext支撑(见 src/ExportContext.php)。 - 重复出现与自引用:同一对象/数组第二次出现时,不再展开内容,而是输出
Array &N/Object #N的引用形式。测试 tests/ExporterTest.php 验证了递归数组Array &0 [ 0 => Array &1 [ 0 => Array &1 ] ]的稳定输出。
对象默认按属性逐一导出,键名规则与 PHP 内部一致:私有属性键为\0DeclaringClassName\0propertyName、受保护属性为\0*\0propertyName、公有属性为propertyName(toArray())。当派生类重声明父类同名私有属性时,键名会加类名前缀(如ParentClass::property),避免互相覆盖,相关断言见 tests/ExporterTest.php。另外SplObjectStorage等内部类无法走快速机制,会按类似print_r()的格式输出并保留迭代器位置(toArray())。
对象导出中的枚举处理
PHP 枚举也是对象,因此会进入对象导出流程,但处理顺序有讲究(exportObject()):
- 先咨询自定义对象导出器(可定制枚举表示);
- 再处理
BackedEnum/UnitEnum,输出为ClassName Enum #id (Name),带值枚举(backed enum)额外输出值,如ExampleStringBackedEnum Enum #8 (Value, 'value'); - 最后才咨询递归上下文——因为枚举 case 是单例,直接给出表示比输出引用更有信息量。
仓库测试夹具 tests/_fixture/ExampleEnum.php、tests/_fixture/ExampleStringBackedEnum.php 与 tests/_fixture/ExampleIntegerBackedEnum.php 分别对应纯枚举、字符串背衬枚举与整数背衬枚举,测试断言见 tests/ExporterTest.php。
简洁导出:shortenedExport()与shortenedRecursiveExport()
调试与日志场景常常需要单行、定长、稳定的字符串。Exporter提供两个简洁导出方法:
shortenedExport()
shortenedExport()把值折叠为单行:换行被替换为可见的\n,数组与对象的内容用...代替;字符串超长时被截断(src/Exporter.php):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter; // [] print $exporter->shortenedExport([]); // [...] print $exporter->shortenedExport([1, 2, 3, 4, 5]); // stdClass Object () print $exporter->shortenedExport(new stdClass); // Exception Object (...) print $exporter->shortenedExport(new Exception); // 'this\nis\na\nsuper\nlong\nstr...nspace' print $exporter->shortenedExport( <<<LONG_STRING this is a super long string that wraps a lot and eats up a lot of space LONG_STRING );截断实现位于 shorten():移除\r、\n后,若mb_strlen()超过上限,则保留前maxLength - 10个字符,插入...,再保留末尾 7 个字符。因为使用mb_strlen/mb_substr,多字节字符也能正确截断(tests/ExporterTest.php 验证了日文等多字节场景)。
shortenedRecursiveExport()
shortenedRecursiveExport()递归折叠整个数组数据集,专为"把数据集表示拼进测试名"这类场景设计(如数据提供者驱动的测试命名,README 明确提及)。其签名与行为(src/Exporter.php):
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string- 数据量超限时追加
, ...%d more elements说明被省略的元素个数; - 递归到已处理的数组时输出
*RECURSION*; $processed参数可传入外部RecursionContext,用于跨调用共享"已处理"状态。
测试数据(tests/ExporterTest.php)展示了 20,000 元素大数组在limit=10时的输出以...19990 more elements收尾,深嵌套数组也能稳定折叠。
自定义对象导出:ObjectExporter接口
默认情况下对象按属性逐一导出。若希望控制某类对象的表示方式,实现ObjectExporter接口即可(src/ObjectExporter.php):
interface ObjectExporter { public function handles(object $object): bool; public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string; }接口要求两个方法:
handles(object $object): bool——判断该导出器是否负责此类对象;export(...)——返回该对象的完整字符串表示,入参包括当前Exporter实例、缩进层级与ExportContext。
README 给出的完整示例:
<?php declare(strict_types=1); use SebastianBergmann\Exporter\ExportContext; use SebastianBergmann\Exporter\Exporter; use SebastianBergmann\Exporter\ObjectExporter; final class MoneyExporter implements ObjectExporter { public function handles(object $object): bool { return $object instanceof Money; } public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string { assert($object instanceof Money); return sprintf('Money (%d %s)', $object->amount(), $object->currency()); } }对象导出器通过构造函数传入Exporter,且在对象的默认表示之前被咨询(src/Exporter.php 的构造函数第三个参数$objectExporter,以及 exportObject() 中"先咨询自定义导出器"的顺序):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter(objectExporter: new MoneyExporter); // Money (1999 EUR) print $exporter->export(new Money(1999, 'EUR')); /* stdClass Object #6 ( 'net' => Money (1999 EUR), 'gross' => Money (2379 EUR), ) */ $price = new stdClass; $price->net = new Money(1999, 'EUR'); $price->gross = new Money(2379, 'EUR'); print $exporter->export($price);可以看到:Money被自定义表示替代;而嵌套它的stdClass仍走默认属性导出,其属性值再递归命中MoneyExporter。
链式组合:ObjectExporterChain
ObjectExporterChain把多个对象导出器组合为单个导出器。链中的导出器按组合顺序被咨询,第一个handles()返回true的导出器负责生成表示(src/ObjectExporterChain.php):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; use SebastianBergmann\Exporter\ObjectExporterChain; $exporter = new Exporter( objectExporter: new ObjectExporterChain( [ new BasketExporter, new MoneyExporter, ], ), );实现细节:
handles()用array_any()判断链中是否存在任一导出器能处理该对象;export()顺序遍历,命中即委托;若无一命中则抛出ObjectNotSupportedException(src/exceptions/ObjectNotSupportedException.php 继承自RuntimeException并实现本组件的 src/exceptions/Exception.php 标记接口)。
ObjectExporterChainTest 验证了四条核心行为:任一导出器可处理即返回true、全部不可处理则返回false、委托给第一个可处理的导出器、以及无一命中时抛出ObjectNotSupportedException。测试夹具 tests/_fixture/ObjectExporterThatHandlesObjectsOfSpecificType.php 展示了"按类型精确匹配"的典型写法。
导出器与枚举、默认表示的关系
枚举是对象,因此同样会先被交给对象导出器咨询。当没有导出器处理某对象时,使用默认表示(属性逐一导出或枚举专用表示)。相关测试见 tests/ExporterTest.php:配置"处理所有对象"的链时,枚举输出自定义表示;配置"不处理任何对象"的链时,枚举回退到默认Enum #id (Name)格式。
重复出现:默认表示与自定义表示的行为差异
默认表示:重复出现替换为引用
当同一对象多次出现,默认表示只用于首次出现,后续出现替换为对象引用(Object #id):
/* Array &0 [ 0 => Money Object #8 ( 'amount' => 1999, 'currency' => 'EUR', ), 1 => Money Object #8, ] */ $money = new Money(1999, 'EUR'); print (new Exporter)->export([$money, $money]);自定义表示:每次出现都完整导出
对象导出器对其处理对象的"整个表示"负责,因此该对象的每次出现都会由它导出,不会替换为引用:
/* Array &0 [ 0 => Money (1999 EUR), 1 => Money (1999 EUR), ] */ print $exporter->export([$money, $money]);这一差异的根源在源码 exportObject():自定义导出器在递归上下文被咨询之前就被调用——"每次出现都同样导出",而默认路径在 src/Exporter.php 处先检查$context->contains($value)再决定是否展开。测试 tests/ExporterTest.php 分别验证了"自定义导出器每次出现都导出"与"未处理对象重复出现替换为引用"两种行为。
唯一例外:自嵌套对象
唯一的例外是(直接或间接)嵌套在自身内部的对象:若仍由自定义导出器导出会无限递归,因此替换为对象引用。相关测试(tests/ExporterTest.php)使用Node夹具构造children指向自身的链表,验证输出中内层对象以Node Object #id引用形式出现,shortenedExport()也不会无限递归(tests/ExporterTest.php)。
导出嵌套值:ExportContext必须透传
对象导出器可以使用传入的Exporter导出其对象内部嵌套的值。关键约束是:必须把传入的ExportContext一并透传,否则嵌套值的导出会以空上下文重新开始,导致引用编号错乱——例如给已在同一导出中使用的数组重新分配引用。
正确写法(README 示例):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\ExportContext; use SebastianBergmann\Exporter\Exporter; use SebastianBergmann\Exporter\ObjectExporter; final class BasketExporter implements ObjectExporter { public function handles(object $object): bool { return $object instanceof Basket; } public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string { assert($object instanceof Basket); return 'Basket ' . $exporter->export($object->items(), $indentation, $context); } }/* Array &0 [ 'basket' => Basket Array &1 [ 0 => Money (1999 EUR), ], ] */ print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);注意输出中Basket内部的数组引用编号为&1,与外部数组&0连续,证明同一上下文被正确共享。对应源码文档注释位于 src/Exporter.php 与 src/ExportContext.php。
仓库测试夹具 tests/_fixture/ObjectExporterThatExportsNestedValues.php 是教科书式实现——export()中用$exporter->export($object->children, $indentation, $context)透传上下文。测试 tests/ExporterTest.php 验证了透传上下文后,嵌套数组不会复用外部数组的引用编号(外层&0、内层&1各自独立且连续)。
自嵌套对象不会被再次交给自定义导出器
ExportContext内部用SplObjectStorage记录"正在被自定义导出器导出"的对象(src/ExportContext.php):beginExportByObjectExporter()标记、endExportByObjectExporter()清除、isBeingExportedByObjectExporter()查询。当导出流程再次遇到正处于导出中的对象(即自嵌套),exportObject() 直接返回Class Object #id引用,避免无限递归。
简洁导出中的自定义表示
shortenedExport()与shortenedRecursiveExport()同样会咨询对象导出器:自定义表示被折叠为单行,超过配置的最大长度时被截断(src/Exporter.php):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter(objectExporter: new MoneyExporter); // Money (1999 EUR) print $exporter->shortenedExport(new Money(1999, 'EUR'));两个验证用例(tests/ExporterTest.php):多行自定义表示"first\nsecond"被折叠成firstsecond;41 个字符的表示被截断为 30 字符 +...+ 7 字符。此外,shortenedExport()使用ReflectionClass统计属性数量,且不会初始化惰性对象(lazy objects,见 tests/ExporterTest.php)。
重要提醒:这些表示用于需要短、单行、稳定的字符串的场景。例如数据集的表示由shortenedRecursiveExport()构建,会被拼进数据提供者驱动的测试名。因此对象导出器应提供紧凑且跨导出稳定的表示——若表示每次都变(如包含随机数),测试名也会随之变化。
查询自定义表示:hasCustomRepresentationFor()
Exporter::hasCustomRepresentationFor()告知对象导出器是否为某对象提供表示,供"自己渲染对象、但有自定义表示时想复用"的代码使用(src/Exporter.php):
<?php declare(strict_types=1); use SebastianBergmann\Exporter\Exporter; $exporter = new Exporter(objectExporter: new MoneyExporter); // true var_dump($exporter->hasCustomRepresentationFor(new Money(1999, 'EUR'))); // false var_dump($exporter->hasCustomRepresentationFor(new stdClass));实现上,它等价于$this->objectExporter->handles($object);未配置导出器时始终返回false。测试见 tests/ExporterTest.php。同样牢记:直接或间接自嵌套的对象会被替换为引用,而不会再次交给自定义导出器。
构造函数参数速查
Exporter构造函数(src/Exporter.php)接收三个可选参数,均可按命名参数方式传入:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
shortenArraysLongerThan | int(非负) | 0 | 在shortenedRecursiveExport()中,元素总数超过该值时追加, ...%d more elements;0表示不限制 |
maxLengthForStrings | int(正) | 40 | 简洁导出时字符串(含自定义表示折叠后)的最大长度,超长部分被截断 |
objectExporter | ?ObjectExporter | null | 自定义对象导出器(可为ObjectExporterChain组合链),在默认表示之前被咨询 |
export()与shortenedExport()也接受$maxLengthForStrings参数覆盖构造默认值;shortenedRecursiveExport()的参数$processed可复用外部RecursionContext。
小结
sebastian/exporter 的核心价值在于提供比print_r()更规范、可预测、可定制的变量可视化导出:
- 标量导出规范化
null/true/false/浮点边界值/字符串换行,并智能处理二进制数据; - 递归导出通过
ExportContext(内部基于RecursionContext与SplObjectStorage)统一管理引用编号、重复出现与自嵌套,避免无限递归; - 自定义导出通过
ObjectExporter接口 +ObjectExporterChain链式组合,让特定类型拥有专属表示,且与简洁导出、hasCustomRepresentationFor()深度集成; - 嵌套值导出要求透传
ExportContext,这是保持整个导出引用编号一致的关键约束。
若要继续深入,可阅读 src/Exporter.php(导出核心逻辑)、src/ObjectExporterChain.php(链式组合)、src/ExportContext.php(递归上下文),以及 tests/ExporterTest.php 与 tests/ObjectExporterChainTest.php 中的完整断言;ChangeLog.md 记录了对象导出器机制(8.2.0 引入)等演进历史。
- 开发工具
- 测试
【免费下载链接】exporter
Provides the functionality to export PHP variables for visualization
相关推荐
ShowDoc 中的 PHP 变量可视化导出:sebastian/exporter 组件深度解析与实战指南
ShowDoc 中的 PHP 变量可视化导出:sebastian/exporter 组件深度解析与实战指南 导读 本文聚焦于 ShowDoc 项目依赖链中一个低
文档知识库后端前端Sebastian递归上下文:PHP复杂对象遍历的终极解决方案指南
Sebastian递归上下文:PHP复杂对象遍历的终极解决方案指南 在处理PHP开发中的复杂数据结构时,你是否曾经遇到过无限递归的困扰?🤔 当对象图中存在循环
测试OpenRGB:一个开源软件终结所有RGB控制烦恼,跨平台统一管理你的设备灯光
OpenRGB:一个开源软件终结所有RGB控制烦恼,跨平台统一管理你的设备灯光 还在为电脑里塞满不同品牌的RGB控制软件而头疼吗?雷蛇、海盗船、华硕、微星……每
桌面应用硬件开发智能硬件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考