vectorface/varexporter-legacy

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

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

0.2.3 2019-12-03 15:50 UTC

This package is auto-updated.

Last update: 2020-10-03 17:39:49 UTC


README

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

Build Status Coverage Status Latest Stable Version License

注意

这只是https://github.com/brick/varexporter的一个快速分支,用于为一些旧应用添加PHP 7.0支持。

简介

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对象

每次将数组转换为对象,或者使用带有第二个参数设置为 false(默认值)的 json_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 是如何替代 var_export() 的?

它输出一个数组到对象转换,这是语法正确、可读性强且可执行的。

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;
    }
}

或者更动态、可重用且不太适合 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 是如何替代 var_export() 的?

它将确定导出您的对象的最合适方法,顺序如下

  • 如果您的自定义类有一个 __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会解析定义闭包的PHP源文件,使用受nikic/php-parser库启发的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 functionuse const语句),将按原样导出。这是因为解析器没有运行时上下文来检查该函数或常量定义是否存在于当前命名空间中,因此无法可靠地预测PHP的全局函数/常量回退行为。如果您使用命名空间化的函数或常量,请务必非常小心:始终显式导入您的命名空间化的函数和常量(如果有)。
  • 通过use()绑定变量的闭包无法导出,并将抛出ExportException异常。这是故意的,因为导出的闭包可以在另一个上下文中执行,因此不能依赖于它们最初被定义的上下文。
  • 闭包可以使用$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_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 文件中,并且是序列化的可靠替代方案。