francislavoie/varexporter

此包已废弃且不再维护。未建议替代包。

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

0.2.3 2019-12-03 15:50 UTC

This package is auto-updated.

Last update: 2020-10-18 19:02:03 UTC


README

A powerful and pretty replacement for PHP's var_export().

Build Status Coverage Status Latest Stable Version License

注意

This is just a quick fork of https://github.com/brick/varexporter to add PHP 7.0 support for some legacy apps.

简介

PHP的var_export()函数是导出变量为可执行PHP代码的便捷方式。

特别有用,可以将数据存储在OPCache中,就像您的源代码一样,稍后可以非常快速地检索,比使用unserialize()json_decode()反序列化数据快得多。

但它也有一些缺点

  • 它为stdClass对象输出无效的PHP代码,使用不存在的stdClass::__set_state()(PHP < 7.3)
  • 它不能导出自定义对象,这些对象没有实现__set_state(),并且__set_state()与父类中的私有属性不兼容,这使得实现变得繁琐
  • 它不支持闭包

此外,输出也不太美观

  • 它将数组输出为array()表示法,而不是简短的[]表示法
  • 它以显式且不必要的0 => ...键值对语法输出数值数组

此库旨在提供比var_export()更美观、更安全、更强大的替代方案。

安装

此库可以通过Composer安装。

composer require vectorface/varexporter-legacy

要求

此库需要PHP 7.0或更高版本。

项目状态 & 发布流程

虽然这个库仍在开发中,但它经过充分测试,应该足够稳定,可以在生产环境中使用。

当前版本编号为0.x.y。当引入非破坏性更改时(添加新方法、优化现有代码等),y会增加。

当引入破坏性更改时,总是开始新的0.x版本周期。

因此,可以将您的项目锁定到给定的发布周期,例如0.2.*

如果您需要升级到较新的发布周期,请查看发布历史,以获取每个进一步的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()始终返回导出的变量,而不会输出它。

导出stdClass对象

每次将数组转换为对象或使用带有第二个参数设置为falsejson_decode()时,您都会遇到stdClass对象(这是默认值)。

虽然var_export()stdClass的输出是语法上有效的PHP代码

var_export(json_decode('
    {
        "foo": "bar",
        "baz": {
            "hello": "world"
        }
    }
'));
stdClass::__set_state(array(
   'foo' => 'bar',
   'baz' => 
  stdClass::__set_state(array(
     'hello' => 'world',
  )),
))

但它完全无用,因为它假设stdClass有一个静态的__set_state()方法,但实际上并没有

错误:调用未定义的方法stdClass::__set_state()

VarExporter做了什么?

它输出一个到对象转换的数组,这在语法上有效,可读性强,并且可执行

echo VarExporter::export(json_decode('
    {
        "foo": "bar",
        "baz": {
            "hello": "world"
        }
    }
'));
(object) [
    'foo' => 'bar',
    'baz' => (object) [
        'hello' => 'world'
    ]
]

注意:自PHP 7.3以来,var_export()现在导出一个数组到对象转换,就像VarExporter::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;
    }
}

或者更动态、可重用且不太友好的版本

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()方法(在PHP 7.4中引入,但这个库接受它们在PHP的早期版本中!),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 functionuse const语句,总是以原样导出。这是因为解析器没有运行时上下文来检查当前命名空间中是否存在此函数或常量的定义,因此无法可靠地预测PHP的全局函数/常量回退的行为。如果您正在使用命名空间函数或常量,请务必谨慎:始终显式导入您的命名空间函数和常量,如果有的话。
  • 通过use()绑定变量的闭包不能导出,并且将抛出ExportException。这是故意的,因为导出的闭包可以在另一个上下文中执行,因此不应依赖于它们最初定义的上下文。
  • 闭包可以使用$this,但在导出后不会绑定到对象。如果需要,您必须在运行导出代码后显式通过bindTo()绑定它们。
  • 在源文件中同一行上不能有两个闭包,否则将抛出ExportException。这是因为VarExporter无法知道哪个包含了它遇到的\Closure对象的定义。
  • 在eval()代码中定义的闭包不能导出并抛出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_NUMERIC_SCALAR_ARRAY

格式化只包含标量值的数字数组,使其在一行内显示

VarExporter::export([
    'one' => ['hello', 'world', 123, true, false, null, 7.5],
    'two' => ['hello', 'world', ['one', 'two', 'three']]
], VarExporter::INLINE_NUMERIC_SCALAR_ARRAY);
[
    'one' => ['hello', 'world', 123, true, false, null, 7.5],
    'two' => [
        'hello',
        'world',
        ['one', 'two', 'three']
    ]
]

在此处视为标量的类型有 intboolfloatstringnull

错误处理

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 之外的其他内部类。VarExporter 如果发现则会抛出 ExportException

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

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

  • 尚未支持匿名类的导出。欢迎提出想法或拉取请求。

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

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

在几乎每种其他情况下,它都提供了一个优雅且非常高效的将数据缓存到 PHP 文件的方法,以及序列化的可靠替代方案。