m-derakhshi/varexporter

var_export()的强大替代品,能够导出闭包和对象而不需要__set_state()

资助包维护!
BenMorel

dev-master 2024-05-24 17:47 UTC

This package is auto-updated.

Last update: 2024-09-17 13:57:43 UTC


README

PHP的var_export()的强大且美观的替代品。

Build Status Coverage Status Latest Stable Version Total Downloads License

简介

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版本已达到EOL(已停止支持)。如果您仍在使用这些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使用基于nikic/php-parser库的PHP源文件,该库受到SuperClosure的启发,解析你的闭包定义所在的文件。

为了确保闭包在任何上下文中都能工作,它将重写其源代码,将任何命名空间内的类/函数/常量名称替换为其完全限定形式

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 functionuse const语句),将始终以原样导出。这是因为解析器没有运行时上下文来检查当前命名空间中是否存在该函数或常量的定义,因此无法可靠地预测 PHP 的 全局函数/常量回退 的行为。如果您正在使用命名空间函数或常量,请务必小心:始终明确导入您的命名空间函数和常量。
  • 闭包可以使用$this,但在导出后不会绑定到对象。如果需要,您必须在运行导出代码后通过 bindTo() 显式绑定。
  • 在源文件的同一条线上不能有两个闭包,否则将抛出ExportException。这是因为《VarExporter》无法知道它遇到的 \Closure 对象的定义是哪一个。
  • 在 eval()'d 代码中定义的闭包无法导出,并抛出 ExportException,因为没有源文件可以解析。

您可以使用 NO_CLOSURES 选项禁用闭包导出。当此选项设置时,在尝试导出闭包时将抛出 ExportException

选项

VarExporter::export() 接受一个作为第二个参数的选项掩码

VarExporter::export($var, VarExporter::ADD_RETURN | VarExporter::ADD_TYPE_HINTS);

可用选项

VarExporter::ADD_RETURN

将输出包装在 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']
    ]
]

这里被视为标量的类型是 intboolfloatstringnull

此选项是 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.
}

限制

  • 目前不支持导出除 stdClassClosure 以外的内部类,以及实现 __set_state() 的类(尤其是 DateTime 类)。如果 VarExporter 发现这类类,将抛出 ExportException

    为了避免遇到这个障碍,您可以在包含内部对象引用的类中实现 __serialize()__unserialize()

    如果您认为某个内部类可以被/应该被导出,请随时提出问题或拉取请求。

  • 目前不支持导出匿名类。欢迎提出想法或拉取请求。

  • var_export() 一样,VarExporter 目前无法保持对象身份(导出后,同一对象的两个实例将创建两个相等(==)但不同的(!==)对象)。

  • var_export() 一样,它目前无法处理循环引用,例如对象 A 指向 B,而 B 又指回 A

在几乎所有其他情况下,它都提供了优雅且非常高效地将数据缓存到 PHP 文件中的方法,并且是序列化的良好替代方案。