brick / varexporter
是 var_export() 的强大替代品,可以导出闭包和对象而不需要 __set_state()
Requires
- php: ^7.4 || ^8.0
- nikic/php-parser: ^5.0
Requires (Dev)
- php-coveralls/php-coveralls: ^2.2
- phpunit/phpunit: ^9.3
- psalm/phar: 5.21.1
README
是 PHP 的 var_export()
的强大且美观的替代品。
介绍
PHP 的 var_export() 函数是一种方便的将变量导出为可执行 PHP 代码的方法。
它特别适用于存储可以像源代码一样被 OPCache 缓存的数据,稍后可以非常快速地检索,比使用 unserialize()
或 json_decode()
解析数据要快得多。
但它也有几个缺点
- 它不能导出没有实现
__set_state()
的自定义对象,并且__set_state()
在处理父类中的私有属性时表现不佳,这使得实现变得繁琐 - 它不支持闭包
此外,输出并不非常美观
- 它以
array()
表示法输出数组,而不是简短的[]
表示法 - 它以显式且不必要的
0 => ...
键 => 值语法输出数字数组
这个库旨在提供一个更美观、更安全、更强大的 var_export()
替代方案。
输出是 有效的独立 PHP 代码,不依赖于 brick/varexporter
库。
安装
此库可以通过 Composer 安装。
composer require brick/varexporter
要求
此库需要 PHP 7.4 或更高版本。
对于 PHP 7.2 & 7.3 兼容性,您可以使用版本 0.3
。对于 PHP 7.1,您可以使用版本 0.2
。请注意,这些 PHP 版本已结束生命周期 且不再受支持。如果您仍在使用这些 PHP 版本之一,您应尽快考虑升级。
项目状态 & 发布流程
虽然这个库仍在开发中,但它经过了良好的测试,应该足够稳定,可以在生产环境中使用。
当前版本号格式为 0.x.y
。当引入非破坏性更改(添加新方法、优化现有代码等)时,y
会递增。
当引入破坏性更改时,总是启动新的 0.x
版本周期。
因此,可以将您的项目锁定到给定的发布周期,例如 0.5.*
。
如果您需要升级到较新的发布周期,请查看 发布历史,了解每个后续 0.x.0
版本引入的更改列表。
快速入门
此库提供了一个方法 VarExporter::export()
,其工作方式与 var_export()
非常相似
use Brick\VarExporter\VarExporter; echo VarExporter::export([1, 2, ['foo' => 'bar', 'baz' => []]]);
此代码将输出
[ 1, 2, [ 'foo' => 'bar', 'baz' => [] ] ]
与 var_export()
的输出进行比较
array ( 0 => 1, 1 => 2, 2 => array ( 'foo' => 'bar', 'baz' => array ( ), ), )
注意:与 var_export()
不同,export()
总是返回导出的变量,而不会输出它。
导出自定义对象
var_export()
假设每个对象都有一个静态的 __set_state() 方法,该方法接受一个属性名到值的关联数组,并返回一个对象。
这意味着,如果您想导出您无法控制其类的类的实例,那么您会遇到麻烦。这也意味着您必须为您的类编写样板代码,如下所示
class Foo { public $a; public $b; public $c; public static function __set_state(array $array) : self { $object = new self; $object->a = $array['a']; $object->b = $array['b']; $object->c = $array['c']; return $object; } }
或者更动态、可重用且不太适合 IDE 的版本
public static function __set_state(array $array) : self { $object = new self; foreach ($array as $key => $value) { $object->{$key} = $value; } return $object; }
如果你的类有父类且父类有私有属性,你可能需要做一些技巧来写入值,如果你覆盖了父类的一个私有属性,那么你就没戏了,因为var_export()
会把所有属性放在同一个袋子里,输出一个具有重复键的数组。
那么VarExporter
会做什么呢?
它会确定导出你的对象的最合适方法,顺序如下
-
如果你的自定义类有
__set_state()
方法,VarExporter
会默认使用它,就像var_export()
会做的那样\My\CustomClass::__set_state([ 'foo' => 'Hello', 'bar' => 'World' ])
传递给
__set_state()
的数组将使用与var_export()
相同的语义构建;这个库在这个方面旨在实现100%的兼容性。唯一的区别是当你的类覆盖了私有属性时:var_export()
会输出一个包含重复键的数组(导致数据丢失),而VarExporter
会抛出一个ExportException
来确保你的安全。与
var_export()
不同,只有当类上实际实现了此方法时,才会使用此方法。你可以通过使用
NO_SET_STATE
选项来禁用这种方式导出对象,即使它们实现了__set_state()
。 -
如果你的类有
__serialize()
和__unserialize()
方法,VarExporter
将使用__serialize()
的输出来导出对象,并将其作为输入传递给__unserialize()
来重新构造对象(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); $object->__unserialize([ 'foo' => 'Test', 'bar' => 1234 ]); return $object; })()
这种方法推荐用于导出复杂的自定义对象:它与PHP 7.4中引入的新序列化机制向前兼容,灵活、安全,并且很好地支持继承。
如果出于任何原因你不想使用此方法导出实现了
__serialize()
和__unserialize()
的对象,你可以通过使用NO_SERIALIZE
选项来退出。 -
如果类不满足上述任何条件,它将通过直接属性访问来导出,其最简单形式如下
(static function() { $object = new \My\CustomClass; $object->publicProp = 'Foo'; $object->dynamicProp = 'Bar'; return $object; })()
如果类有构造函数,它将通过反射绕过
(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); ... })()
如果类有非公共属性,它们将通过绑定到对象的闭包来访问
(static function() { $class = new \ReflectionClass(\My\CustomClass::class); $object = $class->newInstanceWithoutConstructor(); $object->publicProp = 'Foo'; $object->dynamicProp = 'Bar'; (function() { $this->protectedProp = 'contents'; $this->privateProp = 'contents'; })->bindTo($object, \My\CustomClass::class)(); (function() { $this->privatePropInParent = 'contents'; })->bindTo($object, \My\ParentClass::class)(); return $object; })()
你可以使用
NOT_ANY_OBJECT
选项来禁用这种方式导出对象。
如果你尝试导出自定义对象,并且所有兼容的导出器都已禁用,将抛出ExportException
。
导出闭包
从版本0.2.0
开始,VarExporter
对闭包有实验性的支持
echo VarExporter::export([ 'callback' => function() { return 'Hello, world!'; } ]);
[ 'callback' => function () { return 'Hello, world!'; } ]
为了实现这个魔法,VarExporter
使用受SuperClosure启发的nikic/php-parser库,解析你的闭包定义所在的PHP源文件。
为了确保闭包在任何上下文中都能工作,它重新编写其源代码,将任何命名空间中的类/函数/常量名称替换为其完全限定的对应物
namespace My\App; use My\App\Model\Entity; use function My\App\Functions\imported_function; use const My\App\Constants\IMPORTED_CONSTANT; use Brick\VarExporter\VarExporter; echo VarExporter::export(function(Service $service) : Entity { strlen(NON_NAMESPACED_CONSTANT); imported_function(IMPORTED_CONSTANT); \My\App\Functions\explicitly_namespaced_function(\My\App\Constants\EXPLICITLY_NAMESPACED_CONSTANT); return new Entity(); });
function (\My\App\Service $service) : \My\App\Model\Entity { strlen(NON_NAMESPACED_CONSTANT); \My\App\Functions\imported_function(\My\App\Constants\IMPORTED_CONSTANT); \My\App\Functions\explicitly_namespaced_function(\My\App\Constants\EXPLICITLY_NAMESPACED_CONSTANT); return new \My\App\Model\Entity(); }
注意所有命名空间中的类,以及显式命名空间中的函数和常量都被重写,而未命名空间的功能strlen()
和未命名空间的常量保持不变。请参阅第一个注意。
使用声明
默认情况下,导出通过use()
绑定了变量的闭包将抛出ExportException
。这是故意的,因为导出的闭包可以在另一个上下文中执行,因此不能依赖于它们最初定义的上下文。
当使用 CLOSURE_SNAPSHOT_USES
选项时,VarExporter
将导出每个 use()
变量的当前值,而不是抛出异常。导出的变量将作为表达式添加到导出的闭包中。
$planet = 'world'; echo VarExporter::export([ 'callback' => function(string $greeting) use ($planet) { return $greeting . ', ' . $planet . '!'; } ], VarExporter::CLOSURE_SNAPSHOT_USE);
[ 'callback' => function (string $greeting) { $planet = 'world'; return $greeting . ', ' . $planet . '!'; } ]
箭头函数
PHP 支持闭包的简写语法(自 PHP 7.4 开始),也称为箭头函数。VarExporter
将这些作为正常闭包导出。
箭头函数可以隐式使用定义其上下文中的变量。如果箭头函数中使用了任何上下文变量,且没有使用 CLOSURE_SNAPSHOT_USES
选项,VarExporter
将抛出 ExportException
。
$planet = 'world'; echo VarExporter::export([ 'callback' => fn(string $greeting) => $greeting . ', ' . $planet . '!'; ], VarExporter::CLOSURE_SNAPSHOT_USES);
[ 'callback' => function (string $greeting) { $planet = 'world'; return $greeting . ', ' . $planet . '!'; } ]
注意事项
- 未显式命名空间定义的函数和常量,无论是直接定义还是通过
use function
或use const
语句,总是按原样导出。这是因为解析器没有运行时上下文来检查该函数或常量的定义是否存在于当前命名空间中,因此无法可靠地预测 PHP 的 全局函数/常量回退 的行为。如果你使用命名空间函数或常量,请务必谨慎:始终显式导入你的命名空间函数和常量。 - 闭包可以使用
$this
,但导出后 不会绑定到对象。如果你需要在导出代码后显式绑定,请通过bindTo()
进行操作。 - 源文件中同一行不能有 2 个闭包,否则将抛出
ExportException
。这是因为VarExporter
无法知道它遇到的\Closure
对象的定义是哪一个。 - 在 eval() 代码中定义的闭包无法导出,并抛出
ExportException
,因为没有源文件可以解析。
您可以通过使用 NO_CLOSURES
选项来禁用闭包导出。当设置此选项时,尝试导出闭包将抛出 ExportException
。
选项
VarExporter::export()
接受一个选项掩码作为第二个参数
VarExporter::export($var, VarExporter::ADD_RETURN | VarExporter::ADD_TYPE_HINTS);
可用选项
VarExporter::ADD_RETURN
将输出包装在返回语句中
return (...);
这使得代码可以在 PHP 文件中执行——或者 eval()
。
VarExporter::ADD_TYPE_HINTS
将类型提示添加到通过反射创建的对象中,以及绑定到对象的闭包中的 $this
。这允许外部工具和 IDE 对生成的代码进行静态分析。
/** @var \My\CustomClass $object */ $object = $class->newInstanceWithoutConstructor(); (function() { /** @var \My\CustomClass $this */ $this->privateProp = ...; })->bindTo($object, \My\CustomClass::class)();
VarExporter::SKIP_DYNAMIC_PROPERTIES
在输出中跳过自定义类上的动态属性。动态属性是那些不是类定义一部分的属性,而是在运行时添加到对象上的。默认情况下,任何设置在自定义类上的动态属性都会导出;如果使用此选项,则动态属性仅允许在 stdClass
对象上,并且忽略其他对象上的动态属性。
VarExporter::NO_SET_STATE
禁止通过 __set_state()
导出对象。
VarExporter::NO_SERIALIZE
禁止通过 __serialize()
和 __unserialize()
导出对象。
VarExporter::NOT_ANY_OBJECT
禁止使用直接属性访问和绑定闭包导出任何自定义对象。
VarExporter::NO_CLOSURES
禁止导出闭包。
VarExporter::INLINE_ARRAY
在单行上格式化数组
VarExporter::export([ 'one' => ['hello', 'world', 123, true, false, null, 7.5], 'two' => ['hello', 'world', [ 'one', 'two', 'three' ]] ], VarExporter::INLINE_ARRAY);
['one' => ['hello', 'world', 123, true, false, null, 7.5], 'two' => ['hello', 'world', ['one', 'two', 'three']]]
VarExporter::INLINE_SCALAR_LIST
在单行上格式化只包含标量值的数字数组
VarExporter::export([ 'one' => ['hello', 'world', 123, true, false, null, 7.5], 'two' => ['hello', 'world', ['one', 'two', 'three']] ], VarExporter::INLINE_SCALAR_LIST);
[ 'one' => ['hello', 'world', 123, true, false, null, 7.5], 'two' => [ 'hello', 'world', ['one', 'two', 'three'] ] ]
这里考虑为标量的类型是 int
、bool
、float
、string
和 null
。
此选项是 INLINE_ARRAY
的子集,当使用 INLINE_ARRAY
时没有效果。
VarExporter::TRAILING_COMMA_IN_ARRAY
在非内联数组的最后一个项目后添加一个尾逗号
VarExporter::export( ['hello', 'world', ['one', 'two', 'three']], VarExporter::TRAILING_COMMA_IN_ARRAY | VarExporter::INLINE_SCALAR_LIST );
[ 'hello', 'world', ['one', 'two', 'three'], ]
VarExporter::CLOSURE_SNAPSHOT_USES
将每个 use()
变量的当前值导出为导出闭包内的表达式。
缩进
您可以使用 VarExporter::export()
的第三个参数来控制缩进级别。这在您想使用生成的代码字符串替换用于生成代码文件的模板中的占位符时非常有用。
因此,在以下模板中使用 VarExporter::export(['foo' => 'bar'], indentLevel: 1)
的输出替换 {{exported}}
public foo()
{
$data = {{exported}};
}
将得到以下结果
public foo() { $data = [ 'foo' => 'bar' ]; }
请注意,第一行永远不会缩进,正如上面示例中所示。
错误处理
在 export()
中发生的任何错误都会抛出 ExportException
use Brick\VarExporter\VarExporter; use Brick\VarExporter\ExportException; try { VarExporter::export(fopen('php://memory', 'r')); } catch (ExportException $e) { // Type "resource" is not supported. }
限制
-
目前不支持导出除了
stdClass
和Closure
之外的内部类,以及实现__set_state()
的类(最显著的是 DateTime 类)。如果VarExporter
发现此类,将抛出ExportException
。为了避免遇到这个障碍,您可以在包含内部对象引用的类中实现
__serialize()
和__unserialize()
。如果您认为某个内部类可以被/应该导出,请随意提出问题或拉取请求。
-
目前不支持导出匿名类。欢迎提出想法或拉取请求。
-
与
var_export()
类似,VarExporter
目前无法保持对象身份(同一对象的两个实例,一旦导出,将创建两个相等的(==
)但不同的(!==
)对象)。 -
与
var_export()
类似,它目前无法处理循环引用,例如对象A
指向B
,而B
又指向A
。
在几乎所有其他情况下,它都提供了优雅且非常高效地将数据缓存到 PHP 文件中的方法,以及序列化的可靠替代方案。