在PHP中序列化或哈希闭包函数

8

这必然会引起设计上的问题,但我想在PHP中对闭包进行序列化或哈希,以便我拥有该闭包的唯一标识符。

我不需要能够从中调用闭包,我只需要一个可以在闭包内外访问的唯一标识符,即一个接受闭包的方法需要为该闭包生成标识符,而闭包本身需要能够生成相同的标识符。

到目前为止我尝试过以下方法:

$someClass = new SomeClass();

$closure1 = $someClass->closure();

print $closure1();
// Outputs: I am a closure: {closure}

print $someClass->closure();
// Outputs: Catchable fatal error: Object of class Closure could not be converted to string

print serialize($closure1);
// Outputs: Fatal error: Uncaught exception 'Exception' with message 'Serialization of 'Closure' is not allowed'

class SomeClass
{
    function closure()
    {
        return function () { return 'I am a closure: ' . __FUNCTION__; };
    }
}

反射API似乎没有提供任何我可以使用来创建ID的东西。


好的,考虑你的设计受到质疑了 ;) “Closure” 在这里是一个匿名函数? - dualed
我需要能够从闭包内部以及接受闭包作为参数的方法内部获取哈希值。 - Toby
它只需要在实例的范围内是唯一的,无需持久化。 - Toby
3
如果您不需要序列化且所有内容都在一个请求中处理,可以尝试使用 spl_object_hash - hakre
@hakre,你指出了这一点,但重要的是强调这只在同一个请求中有效。哈希值不能被存储并在以后使用。从OP的问题中无法确定这是否是一个要求。 - theking2
显示剩余3条评论
10个回答

8

我的解决方案更通用,同时尊重闭包的静态参数。为了实现这个技巧,你可以在闭包内部传递对闭包的引用:

class ClosureHash
{
    /**
     * List of hashes
     *
     * @var SplObjectStorage
     */
    protected static $hashes = null;

    /**
     * Returns a hash for closure
     *
     * @param callable $closure
     *
     * @return string
     */
    public static function from(Closure $closure)
    {
        if (!self::$hashes) {
            self::$hashes = new SplObjectStorage();
        }

        if (!isset(self::$hashes[$closure])) {
            $ref  = new ReflectionFunction($closure);
            $file = new SplFileObject($ref->getFileName());
            $file->seek($ref->getStartLine()-1);
            $content = '';
            while ($file->key() < $ref->getEndLine()) {
                $content .= $file->current();
                $file->next();
            }
            self::$hashes[$closure] = md5(json_encode(array(
                $content,
                $ref->getStaticVariables()
            )));
        }
        return self::$hashes[$closure];
    }
}

class Test {

    public function hello($greeting)
    {
        $closure = function ($message) use ($greeting, &$closure) {
            echo "Inside: ", ClosureHash::from($closure), PHP_EOL, "<br>" ;
        };
        return $closure;
    }
}

$obj = new Test();

$closure = $obj->hello('Hello');
$closure('PHP');
echo "Outside: ", ClosureHash::from($closure), PHP_EOL, "<br>";

$another = $obj->hello('Bonjour');
$another('PHP');
echo "Outside: ", ClosureHash::from($another), PHP_EOL, "<br>";

6

您可以编写自己的闭包函数,拥有getId()getHash()等功能。

例如(演示):

1: Hello world
2: Hello world

第一个闭包(ID:1),在调用上下文中读取ID。 第二个闭包(ID:2),在闭包内部读取ID(其中包括自我引用)。

代码:

<?php
/**
 * @link https://dev59.com/FGzXa4cB1Zd3GeqPSl3m
 */

class IdClosure
{
    private $callback;
    private $id;

    private static $sequence = 0;

    final public function __construct(Callable $callback) {
        $this->callback = $callback;
        $this->id = ++IdClosure::$sequence;
    }

    public function __invoke() {
        return call_user_func_array($this->callback, func_get_args());
    }

    public function getId() {
        return $this->id;
    }
}

$hello = new IdClosure(function($text) { echo "Hello $text\n";});
echo $hello->getId(), ": ", $hello('world');

$hello2 = new IdClosure(function($text) use (&$hello2) { echo $hello2->getId(), ": Hello $text\n";} );
$hello2('world');

我不确定那是否符合您的需求,也许它会给您一些想法。我建议使用spl_object_hash,但我并没有完全理解为什么它不能或最终能够起作用。


@dualed:是的,spl_object_hash 的这种行为是已知的。你需要保持对象的集合,然后利用哈希值,也可以参考:http://php.net/splobjectstorage。 - hakre
顺便提一下:这里的实现没有闭包的重复ID。 - hakre
是的,你说得对,它基本上遵循了我的$this注入想法的原则。不过我更喜欢你的方式,因为你可以使用类型提示。但我不明白为什么你要扩展这个类,毕竟你已经注入了回调函数。我只是看不出这里抽象类的好处。 - dualed
我扩展了这个类,以确保IdBaseClosure::$id是私有的,并且不能从外部修改。否则,ID可能会被污染,使其无用。这只是为了更加严格,宁愿安全也不要冒险。 - hakre
1
@dualed:啊,对了,我可能有点跑题了;回调函数也应该是私有的。我已经更新了答案来纠正这一点。感谢您的反馈,我可能会尝试一些不同的方法,所以才会那样做。但对于给出的示例来说,我必须承认它是多余的。 - hakre
显示剩余4条评论

5

好的,这是我能想到的唯一事情:

<?php
$f = function() {
};
$rf = new ReflectionFunction($f);
$pseudounique = $rf->getFileName().$rf->getEndLine();
?>

如果你想的话,可以使用md5或其他哈希算法。但如果该函数是由字符串生成的,则应使用uniqid()进行种子化。


它们是否在同一行中定义? - dualed
不用在意上一个评论(已删除)。这是目前为止最接近的解决方案,我正在尝试使用Reflection和@hakre的spl_object_hash()建议来完成类似的工作 - 我会继续尝试,如果没有结果,我就会采用这个方法,即使它看起来有点hacky。 - Toby
这有点hacky ;) 告诉我你的目标php版本,我有一个想法;或者告诉我是否可能使用php 5.4。 - dualed
我喜欢这个...`$someClass = new SomeClass();$closure = $someClass->closure(); $closure2 = $someClass->closure2();$rf = new ReflectionFunction($closure); $rf2 = new ReflectionFunction($closure);print spl_object_hash($rf); // 输出: 000000007ddc37c8000000003b230216 print spl_object_hash($rf2); // 输出: 000000007ddc37c9000000003b230216class SomeClass { function closure() { return function () { return '我是闭包:' . FUNCTION; }; } function closure2() { return function () { return '我是闭包:' . FUNCTION; }; } }` - Toby

3

PHP 匿名函数Closure 类 实例的形式暴露出来。由于它们基本上是对象,因此当您输入一个时,spl_object_hash 将返回唯一的标识符。从 PHP 交互提示符中:

php > $a = function() { echo "I am A!"; };
php > $b = function() { echo "I am B!"; };
php >
php >
php > echo spl_object_hash($a), "\n", spl_object_hash($b), "\n";
000000004f2ef15d000000003b2d5c60
000000004f2ef15c000000003b2d5c60

这些标识符看起来可能相同,但在中间有一个字母不同。

该标识符仅适用于该请求,因此请预计在调用之间更改,即使函数和任何使用的变量都不会更改。


@dualed,我也是这么想的,但我刚刚重新测试了没有函数体的闭包($x = function() {};),但无法复制。仍然得到一个唯一的哈希值,在中间递增字母。我在这里运行的是5.4.9... - Charles
嗯,我从一个函数中返回闭包,并且在我的系统上测试spl_object_hash(g()) == spl_object_hash(h())肯定会得到true的结果。 - dualed
1
我得到了与@Charles相同的结果 - spl_object_hash(function() {})和spl_object_hash(function() {})每次返回相同的哈希值,但$fn = function() {}; $fn2 = function() {};返回不同的哈希值。 - Toby
我有一个怀疑...当我把g()和h()的返回值分配给变量时,它们不再相等了。 - dualed
1
@Toby:是的,请阅读文档(http://php.net/manual/en/function.spl-object-hash.php)“注意:当对象被销毁时,它的哈希值可能会被重用于其他对象。”这也基本上适用于对象的地址。显然,您只应比较您知道仍然存在的对象的哈希值。 - newacct
显示剩余8条评论

2
Superclosure 提供了一个方便的类,允许您序列化/反序列化闭包等操作。

1

听起来你想要生成一个签名。如果闭包接受任何参数,从闭包外部创建签名几乎不可能复制。传递的数据将改变生成的签名。

$someClass = new SomeClass();
$closure1 = $someClass->closure();
$closure1_id = md5(print_r($closure1, true));

即使您的闭包不接受参数,您仍然需要解决在闭包内存储和持久化签名的问题。您可以尝试使用闭包内的静态变量,以便它只初始化一次并保留“签名”。但是,如何检索它会变得混乱。
实际上,这听起来像您想要一个类,而不是一个闭包。这将解决所有这些问题。您可以在实例化时传递“盐”,并使用该盐(即随机数)生成签名。这将使签名唯一。然后,您可以保留该盐,使用完全相同的构造函数参数(即盐)重新创建一个类,并将其与已创建类中的文件中的签名进行比较。

在这里使用var_dump可能比print_r更好。至少在我的本地副本中,print_r的输出总是“Closure Object()”,而var_dump包括对象编号。 - Charles
同意,var_dump会更好。 - Brent Baisley

0

根据您的需求,一个更简单的解决方案可能是:

function GetClosureUID(closure $closure)
{
    ob_start();
    var_dump($closure);
    return ob_get_clean();
}

这对我的用例有效,但我的要求是:

  • 每个闭包都有一个唯一的字符串
  • 在请求之间保持持久性,但仅限于单个端点文件内,并且仅在源代码被修改之前

可以通过在返回的字符串中还包括url /文件名来使其在不同的端点之间唯一。

在我特定的情况下,以下方法效果很好:

function GetClosureUID(closure $closure)
{
    ob_start();
    var_dump($closure);
    $full = ob_get_clean();
    return basename($_SERVER['PHP_SELF']) .':'. substr($full, 0, strpos($full, ' ('));
};

但是对于在修改源代码后仍然需要持久化存储的内容,或者需要更详细字符串描述的情况下,您需要采用基于反射的方法。


0

在@hakre和@dualed的帮助下,我们得出了可能的解决方案:

$someClass = new SomeClass();

$closure = $someClass->closure();
$closure2 = $someClass->closure2();

$rf = new ReflectionFunction($closure);
$rf2 = new ReflectionFunction($closure2);

print spl_object_hash($rf); // Outputs: 000000007ddc37c8000000003b230216
print spl_object_hash($rf2); // Outputs: 000000007ddc37c9000000003b230216

class SomeClass
{
    function closure()
    {
        return function () { return 'I am closure: ' . __FUNCTION__; };
    }

    function closure2()
    {
        return function () { return 'I am closure: ' . __FUNCTION__; };
    }
}

1
但是,当您从相同的闭包创建第二个reflectionfunction时,它是否返回相同的哈希值?并不是我想贬低您的解决方案。 - dualed

0

只需使用https://github.com/opis/closure,该库得到维护并利用SplObjectStorage创建闭包对象的包装器。您可以像这样使用serialize和unserialize:

use function Opis\Closure\{serialize as opisSerialize, unserialize as opisUnserialize};


    $serialized = opisSerialize(new SerializableClosure($closure));
    $wrapper = opisUnserialize($serialized);

0

我相信在PHP中无法以可靠的方式对Closure实例进行哈希处理,因为您无法访问函数体中大多数符号所属的AST。

据我所知,只有Closure使用的外部作用域变量、函数体类型为T_VARIABLE$a$b等)的符号、类型信息和函数签名可以通过各种方式阐明。在缺乏有关函数体的重要信息的情况下,哈希函数在应用于Closure实例时无法表现出幂等行为。

spl_object_hashspl_object_id都无法解决此问题--可能会更改refcount(在真实世界的应用程序中几乎总是如此),这使得这些函数通常也不是幂等的。

唯一可能哈希闭包实例的情况是它在某个 PHP 源文件中定义,并且您当前的实例没有使用来自其外部作用域的其他闭包实例。在这种情况下,您可以通过将您的闭包实例包装在 ReflectionFunction 实例中来尝试获取声明闭包的文件名和行号。然后,您可以加载源文件并提取行号之间的部分,将该部分转储到字符串中并使用 token_get_all() 进行标记化。接下来,删除不属于闭包声明的令牌,并查看您的闭包实例的外部作用域以获取其使用的任何外部作用域变量的值。最后,以某种方式将所有这些内容组合在一起并对数据进行哈希。但当然,当您想将函数传递给函数时,很快就会开始质疑“..但如果外部作用域变量也是闭包实例呢?”--好吧...
为了测试 PHP 中发生的情况,我使用了以下一对函数:
$zhash = function ($input, callable $hash = null, callable $ob_callback = null) {
    if (\is_scalar($input)) {
        return \is_callable($hash) ? $hash($input) : \hash('md5', $input);
    }

    \ob_start(
        \is_callable($ob_callback) ? $ob_callback : null,
        4096,
        PHP_OUTPUT_HANDLER_STDFLAGS
    );
    \debug_zval_dump($input);
    $dump = \ob_get_clean();

    return \is_callable($hash) ? $hash($dump) : \hash('md5', $dump);
};

$zhash_algo_gz = function ($input, string $algo = 'sha256', int $compress = -1) use ($zhash) {
    return $zhash(
        $input,
        function ($data) use ($algo) {
            return \hash($algo, $data);
        },
        function ($data) use ($compress) {
            return \gzcompress($data, $compress, ZLIB_ENCODING_GZIP);
        }
    );
};

debug_zval_dump 的使用是为了避免循环引用和资源失败。而 gzcompress 的使用是将输入数据压缩到哈希函数中,如果它是一个非常大的类。我使用一个完全加载的 Magento2 应用程序作为 $zhash_algo_gz 的输入进行了测试,然后遇到了导致我首次来到这里的确切问题(即 debug_zval_dump 包含 refcount,而不是函数体,从哈希函数得到的结果不是幂等的)。

关于测试

我们设置了一个变量,所有在此测试中使用的闭包都使用它:

$b = 42;

在第一个例子中,引用计数对于两个调用保持不变,因为我们的两个Closure实例未绑定到变量,并且代码在新的php -a会话中执行:
$zhash_algo_gz(function ($a) use ($b) { return $a * $b + 5; });
$zhash_algo_gz(function ($a) use ($b) { return $a * $b + 6; });

输出:

a0cd0738ea01d667c9386d4d9fe085cbc81c0010f30d826106c44a884caf6184
a0cd0738ea01d667c9386d4d9fe085cbc81c0010f30d826106c44a884caf6184

休斯敦,我们有问题!

正如之前提到的,我们无法推断出 Closure 实例的函数体中的关键信息。 +5+6 标记不会出现在任何命令输出、print_rvar_exportvar_dumpdebug_zval_dump 或其他输出中。

这意味着,哈希两个匿名函数,它们共享相同的签名、引用计数、外部作用域变量和参数,但具有部分异构的函数体,将产生相同的哈希值。

如果我们启动一个新的 php -a 会话,但现在首先将我们的 Closure 实例绑定到变量上,乍一看可能看起来不错:

$f1 = function ($a) use ($b) { return $a * $b + 5; });
$f2 = function ($a) use ($b) { return $a * $b + 6; });
$zhash_algo_gz($f1);
$zhash_algo_gz($f2);

输出:

085323126d01f3e04dacdbb6791f230d99f16fbf4189f98bf8d831185ef13b6c
18a9c0b26bf6f6546d08911d7268abba72e1d12ede2e9619d782deded922ab65

嘿,不同的哈希值!但不要被欺骗……

哈希值的变化是由于引用计数的改变,而不是函数体的改变,所以那个哈希值并没有什么用处,对吧?

除了这些,就像 Brent 在上面说的那样,你可能想要一个 Class,而不是一个 Closure……毕竟这还是 PHP 嘛 ;)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接