antto1/superclosure

序列化闭包对象,包括它们的上下文和绑定

v2.4.1 2020-12-02 08:29 UTC

README

一个用于序列化闭包和匿名函数的PHP库。

不再维护

此软件不再维护。请考虑使用opis/closure

README的其余部分将保持软件被弃用之前的状态。

简介

曾经,我尝试序列化一个PHP Closure对象。正如你可能猜到的,它根本不起作用。事实上,你会从PHP运行时得到一个非常具体的错误信息

未捕获的异常'Exception',消息为'Serialization of 'Closure' is not allowed'

尽管PHP不允许序列化闭包,但SuperClosure库使得这成为可能。以下是使用方法

use SuperClosure\Serializer;

$serializer = new Serializer();

$greeting = 'Hello';
$hello = function ($name = 'World') use ($greeting) {
    echo "{$greeting}, {$name}!\n";
};

$hello('Jeremy');
//> Hello, Jeremy!

$serialized = $serializer->serialize($hello);
// ...
$unserialized = $serializer->unserialize($serialized);

$unserialized('Jeremy');
//> Hello, Jeremy!

是的,相当酷,对吧?

特性

SuperClosure提供了两种不同的Closure Analyzers,每个都支持不同方面的闭包序列化功能。《TokenAnalyzer》不如《AstAnalyzer》健壮,但大约快20-25倍。使用下面的表格,并考虑到你的闭包代码的样子,你应该选择支持你所需功能的最快分析器

注意事项

  1. 对于任何通过引用使用的变量(例如,function () use (&$vars, &$like, &$these) {…}),序列化后引用不会被保留。唯一的例外是递归闭包引用。
  2. 如果你在一行中定义了两个闭包(你为什么要这样做呢?),你将无法序列化任何一个,因为不清楚应该解析哪个闭包的代码(它们毕竟是匿名函数)。
  3. 警告:需要eval()函数来反序列化闭包。这个函数被认为很危险,所以你必须评估在使用此库时可能需要采取哪些预防措施。你应该只从可信来源反序列化闭包,否则你将使自己面临代码注入攻击的风险。如果你计划存储或传输序列化的闭包,签名它们是个好主意。请参阅下面的《签名闭包》部分以了解如何进行此操作。
  4. 无法序列化在eval()中定义的闭包。这包括重新序列化已经反序列化的闭包。

分析器

当你实例化Serializer时,可以选择你想要使用的分析器。如果你没有指定,默认使用AstAnalyzer,因为它具有最强大的功能。

use SuperClosure\Serializer;
use SuperClosure\Analyzer\AstAnalyzer;
use SuperClosure\Analyzer\TokenAnalyzer;

// Use the default analyzer.
$serializer = new Serializer();

// Explicitly choose an analyzer.
$serializer = new Serializer(new AstAnalyzer());
// OR
$serializer = new Serializer(new TokenAnalyzer());

分析器在你只是想要对闭包对象进行一些内省时也很有用。查看使用AstAnalyzer时返回的内容

use SuperClosure\Analyzer\AstAnalyzer;

class Calculator
{
    public function getAdder($operand)
    {
        return function ($number) use ($operand) {
            return $number + $operand;
        };
    }
}

$closure = (new Calculator)->getAdder(5);
$analyzer = new AstAnalyzer();

var_dump($analyzer->analyze($closure));
// array(10) {
//   'reflection' => class ReflectionFunction#5 (1) {...}
//   'code' => string(68) "function ($number) use($operand) {
//     return $number + $operand;
// };"
//   'hasThis' => bool(false)
//   'context' => array(1) {
//     'operand' => int(5)
//   }
//   'hasRefs' => bool(false)
//   'binding' => class Calculator#2 (0) {...}
//   'scope' => string(10) "Calculator"
//   'isStatic' => bool(false)
//   'ast' => class PhpParser\Node\Expr\Closure#13 (2) {...}
//   'location' => array(8) {
//     'class' => string(11) "\Calculator"
//     'directory' => string(47) "/Users/lindblom/Projects/{...}/SuperClosureTest"
//     'file' => string(58) "/Users/lindblom/Projects/{...}/SuperClosureTest/simple.php"
//     'function' => string(9) "{closure}"
//     'line' => int(11)
//     'method' => string(22) "\Calculator::{closure}"
//     'namespace' => NULL
//     'trait' => NULL
//   }
// }

签名闭包

SuperClosure 2.1+版本允许你在实例化Serializer时指定一个签名密钥。这样做将配置你的Serializer来签名你序列化的任何闭包,并验证你反序列化的任何闭包的签名。这样做可以帮助保护你免受可能通过篡改序列化的闭包而发生的代码注入攻击。请记住,你的签名密钥必须保密

$serializer1 = new SuperClosure\Serializer(null, $yourSecretSigningKey);
$data = $serializer1->serialize(function () {echo "Hello!\n";});
echo $data . "\n";
// %rv9zNtTArySx/1803fgk3rPS1RO4uOPPaoZfTRWp554=C:32:"SuperClosure\Serializa...

$serializer2 = new SuperClosure\Serializer(null, $incorrectKey);
try {
    $fn = $serializer2->unserialize($data);
} catch (SuperClosure\Exception\ClosureUnserializationException $e) {
    echo $e->getMessage() . "\n";
}
// The signature of the closure's data is invalid, which means the serialized
// closure has been modified and is unsafe to unserialize.

安装

要使用Composer在你的项目中安装Super Closure库,只需使用Composer引入项目

$ composer require antto1/superclosure

当然,你也可以手动更新你的require块

{
    "require": {
        "antto1/superclosure": "^2.0"
    }
}

请访问Composer首页获取有关如何使用Composer的更多信息。

为什么我需要序列化闭包?

嗯,既然您在这里查看这个README文件,您可能已经有了心中的用例。尽管这个概念最初是一个实验,但在实际应用中已经出现了一些用例。

例如,在UserScape(http://www.userscape.com)制作的关于Laravel和IronMQ的视频(http://vimeo.com/64703617)中,大约在7分50秒时,他们展示了如何将闭包推送到队列中作为一个作业,以便由工作进程执行。这样做的好处是您不需要为可能非常简单的作业创建整个类。

或者...您可能有一个由闭包编写的依赖注入容器或路由对象。如果您想缓存它,您需要能够序列化它。

然而,一般来说,可能应该避免序列化闭包。

请告诉我这个项目是如何开始的

这一切始于2010年初,当时PHP 5.3开始流行起来。我着手证明尽管PHP不允许我这样做,但序列化闭包是可以实现的。我在前雇主的博客HTMList(http://www.htmlist.com/development/extending-php-5-3-closures-with-serialization-and-reflection/)上写了一篇名为《使用序列化和反射扩展PHP 5.3闭包》的博客文章,展示了如何实现这一点。我还把代码发布在GitHub上。

从那时起,我对代码进行了一些迭代,最近的迭代由于使用了出色的nikic/php-parser库而变得更加健壮。

替代方案

今年,引入了Opis Closure库,该库也提供了序列化闭包的功能。您也应该检查它并看看哪个最适合您的需求。

如果您希望将闭包作为可执行PHP代码导出,可以查看brick/varexporter库。