Symfony VarDumper 组件打印变量
说到调试,Symfony 的 VarDumper 组件带来了革命性变化。它让我们可以用一种简洁高效的方式打印变量。如果你的项目中还没有用到,应该去试一试。
本文我们将一起看看怎么样用它来打印对象,使之更具可读性。
问题
在一个使用 Stripe 的项目用,我们用 dump
打印 StripeObject
, 我们得到如下的情况:
如你所想,可读性不是很好。其中有很多内部属性。如果展开,我们可以看到原始 HTTP 响应,这个响应被解析成数组,也解析成对象。
里面的信息太多太杂,而且还有许多重复。而且,这个 VarDumper 组件只能打印有限数量的对象。虽然本例假定它打印了所有对象。因此我们会遗漏一些东西。
方案
该组件有一个强大的方案用来缓解这一问题:Casters。它让我们可以定义如何打印一个对象。这个工具很强大,不过也有点难用。我们来看看怎样用它以更具可读性的方式来打印 StripeObject 吧。
VarDumper 组件的工作原理
要理解 caster 的工作原理,就要先理解 VarDumper 的工作原理。有点复杂,不过值得。
本文,我们不会去解释所有事情,否则覆盖的东西就太多了。我们只会专注于最重要的部分。
该组件由两个主要部分组成:
- VarCloner,负责克隆变量用来 dump 打印。它是个递归过程,可以克隆任何变量。它的主要目标是对变量进行快照,这样我们就可以在稍后打印他们;
- VarDumper 负责打印快照。它也是递归的。它的主要目标是将变量以可读的方式显示出来。Symfony 中有三个 dumper: HtmlDumper, CliDumper 以及 ServerDumper。第一个用在浏览器,第二个用在 CLI 命令行中,第三个则用在将 dump 信息发送给连接的客户端 (
vendor/bin/var-dump-server
)。
你猜对了,我们主要关注 VarCloner!
如果组合起来,大概是这样的:
function dump_something($variable, $maxDepth = 3): string
{
$cloner = new VarCloner();
// $data is the snapshot of the $variable
$data = $cloner->cloneVar($variable);
$dumper = new HtmlDumper();
$dumper->setDisplayOptions([
'maxDepth' => $maxDepth,
]);
$output = fopen('php://memory', 'r+b');
$dumper->dump($data, $output);
// It returns the dump as a string. It contains some HTML, CSS, and JavaScript
return stream_get_contents($output, -1, 0);
}
VarCloner
VarCloner 有更加复杂的方法来处理特定的案例,不过主要的理念是将变量转换成数组:
class Foobar
{
public $public = 'foo';
protected $protected = 'bar';
private $private = 'baz';
}
var_dump((array) new Foobar());
结果是:
^ array:3 [
"public" => "foo"
"\x00*\x00protected" => "bar"
"\x00Foobar\x00private" => "baz"
]
Note: 要显示上述结果,我们实际使用了
dump()
而不是var_dump()
,因为它更有可读性,而且var_dump()
不会隐藏\x00
这样的字符。
VarCloner 有一系列 casters,用来从数组中添加或删除一些信息。比如 DateCaster
负责转换 DateTime
并可以用时区以人类可读的格式用添加日期。它也能转换 DateInterval
类型,以人类可读的格式添加时间间隔:
$php > dump(new DateTime());
^ DateTime @1679497380 {#30
date: 2023-03-22 16:03:00.646944 Europe/Paris (+01:00)
}
php > dump(new DateInterval('P1Y2M3DT4H5M6S'));
^ DateInterval {#30
interval: + 1y 2m 3d 04:05:06.0
+"y": 1
+"m": 2
+"d": 3
+"h": 4
+"i": 5
+"s": 6
+"f": 0.0
+"invert": 0
+"days": false
+"from_string": false
}
另一个好的示例是 DoctrineCaster
:它从 Doctrine 代理中,删除 __cloner__
和 __initializer__
属性。因此,这个 dump
更小。
因此,要想扩展 VarCloner,我们将使用 Caster。
Caster
空的 caster 长这样:
namespace App\Bridge\VarDumper\Caster;
use Stripe\StripeObject;
use Symfony\Component\VarDumper\Cloner\Stub;
class StripeObjectCaster
{
public static function castStripeData(
StripeObject $object,
array $a,
Stub $stub,
bool $isNested,
int $filter = 0,
) : array {
return $a;
}
}
castStripeData()
方法由 VarCloner 调用。我们可以按照我们所需的方式命名。它有 5 个参数:
$object
: 原始对象 (一个StripeObject
实例)$a
: 该对象的数组表示((array) $object
的转换结果)$stub
: VarDumper 用以显示变量的一个对象。我们可以添加一些元数据到其中,它会在dump
中显示$isNested
: 一个布尔值说明一个对象是否嵌套在另一个对象中$filter
: 位掩码说明那些信息该展示
该方法必须返回一个用来展示变量的数组。
现在,我们需要填充 castStripeData()
方法,移除内部属性。
删除一些属性
要移除这些属性,我们需要先知道他们的属性名。还记得吗?这些名字很奇怪。我们建议先用 dump()
找到所有属性:
dump(array_keys($a));
// StripeObjectCaster.php on line 15:
// array:57 [▼
// 0 => "\x00*\x00_opts"
// 1 => "\x00*\x00_originalValues"
// 2 => "\x00*\x00_values"
// 3 => "\x00*\x00_unsavedValues"
// 4 => "\x00*\x00_transientValues"
// 5 => "\x00*\x00_retrieveOptions"
// 6 => "\x00*\x00_lastResponse"
// 7 => "saveWithParent"
// 8 => "\x00~\x00id"
// ...
现在我们可以从数组中删除这些属性:
unset($a["\x00*\x00_opts"], $a["\x00*\x00_originalValues"], ...);
这么做也有个小缺点:我们不清楚有些值已经被删除了。要解决这个问题,我们可以让 VarDumper 显示删除属性的数量:
$stub->cut += 7; // 7 is the number of removed properties
添加一些属性
我们也可以添加一些属性到数组中。比如我们可以添加虚拟的(virtual) billing_cycle_anchor_as_date_time
属性:
if (isset($r->billing_cycle_anchor)) {
$a += [
Caster::PREFIX_VIRTUAL.'billing_cycle_anchor_as_date_time' => new \DateTime('@'.$r->billing_cycle_anchor),
];
}
组合
下面是最终的 caster 代码。它被稍微优化成不一定要维护属性列表:
class StripeObjectCaster
{
private static array $propertiesToRemove;
public static function castStripeData(StripeObject $r, array $a, Stub $stub, bool $isNested, int $filter = 0)
{
if (!isset(self::$propertiesToRemove)) {
self::$propertiesToRemove = array_keys((array) new StripeObject());
}
foreach (self::$propertiesToRemove as $property) {
if (array_key_exists($property, $a)) {
unset($a[$property]);
$stub->cut++;
}
}
if (isset($r->billing_cycle_anchor)) {
$a += [
Caster::PREFIX_VIRTUAL.'billing_cycle_anchor_as_date_time' => new \DateTime('@'.$r->billing_cycle_anchor),
];
}
return $a;
}
}
注册 caster
如果你失踪的不是全栈框架,你可以这样注册 caster :
$cloner = new VarCloner();
$cloner->addCasters([
StripeObject::class => StripeObjectCaster::castStripeData(...),
]);
这个数组中,key 是需要转换的类,value 是用于转换的可调用类型(callable)。
如果你用的是全栈框架,你也需要手动注册 caster。的确,VarCloner 没有在容器中注册。有一个这么做的好地方是,在 Kernel::boot()
方法中:
class AppKernel extends Kernel
{
public function boot()
{
parent::boot();
AbstractCloner::$defaultCasters += [
\Stripe\StripeObject::class => StripeObjectCaster::castStripeData(...),
];
}
}
结语
🎉 恭喜!现在我们学会了如何扩展 VarDumper 用更佳的展示对象。
顺便提一下,你知道你可以使用 PHP REPL(php -a) 中的 dump()
函数吗?要做到这一点,你应该克隆 VarDumper 组件到某个位置,安装依赖项,并在 php.ini
文件中添加下面这行:
auto_prepend_file = /path/to/var-dumper/vendor/autoload.php
现在,你可以在 PHP REPL 中使用 dump()
了:
$ php -a
Interactive shell
php > dump(new \DateTimeZone('Europe/Paris'));
^ DateTimeZone {#23
timezone: Europe/Paris (+01:00)
+"timezone_type": 3
+"timezone": "Europe/Paris"
}
另一个我们常用的小技巧是:Twig 扩展在生产环境中 dump 变量!只有管理员有充足的权限,所以别担心。有人说这是坏的实践,不过,我们发现,在生产环境中调试一些数据问题时,这对我们很有帮助。