我相信在PHP中无法以可靠的方式对Closure
实例进行哈希处理,因为您无法访问函数体中大多数符号所属的AST。
据我所知,只有Closure
使用的外部作用域变量、函数体类型为T_VARIABLE
($a
、$b
等)的符号、类型信息和函数签名可以通过各种方式阐明。在缺乏有关函数体的重要信息的情况下,哈希函数在应用于Closure
实例时无法表现出幂等行为。
spl_object_hash
或spl_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_r
、var_export
、var_dump
、debug_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 嘛 ;)
spl_object_hash
。 - hakre