I believe you can't hash Closure
instances in a reliable manner in PHP, because you cannot access most symbols in the AST belonging to the function body.
As far as I can tell only outer scope variables used by the Closure
, symbols in the function body of type T_VARIABLE
($a
, $b
, etc), type information and the function signature can be elucidated in a variety of ways. Absent vital information about the function body it is impossible for hash functions to behave in an idempotent manner when applied to instances of Closure
.
spl_object_hash
or spl_object_id
won't save you -- a possibly (almost constantly in real world applications) changing refcount complicates the matter so these functions aren't usually idempotent either.
The only situation where hashing a Closure
instance may be possible is when it has been defined in a PHP source file somewhere and your current instance of it uses no other Closure
instances from its outer scope. In that case you might have some succes by wrapping your Closure
instance in a ReflectionFunction
instance. Now you can try to get the filename and line numbers where the Closure
is declared. Then you could load the source file and extract the part between the line numbers, dump that part into a string and tokenize it with token_get_all()
. Next remove tokens that don't belong to the Closure
declaration and look at the outer scope of your Closure
instance to get the values of any outer scope variables it uses. Finally, put all this together in some way and hash that data. But of course when you like to pass functions to functions it won't be long until you start questioning "..but what if outer scope variables are also Closure
instances?" -- Well..
To test what happens in PHP I used the following a pair of functions:
$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);
}
);
};
The use of debug_zval_dump
was to avoid failure on circular refs and resources. The use of gzcompress
was to compress the input data to the hash function if it was a very large class. I tested this with a fully loaded Magento2 application as input to $zhash_algo_gz
, before I ran into the exact problems that lead me here in the first place (i.e. debug_zval_dump
contains refcount, not function body, leading to non-idempotent results from hashing functions).
On the the test.
We set a variable all the closures in this test use:
$b = 42;
In the first example the refcount stays the same for both calls as our two Closure
instances are not bound to variables and the code is executed in a fresh php -a
session:
$zhash_algo_gz(function ($a) use ($b) { return $a * $b + 5; });
$zhash_algo_gz(function ($a) use ($b) { return $a * $b + 6; });
Output:
a0cd0738ea01d667c9386d4d9fe085cbc81c0010f30d826106c44a884caf6184
a0cd0738ea01d667c9386d4d9fe085cbc81c0010f30d826106c44a884caf6184
Houston, we have a problem!
As mentioned earlier we cannot infer vital information about the function body of Closure
instances. The +5
and +6
tokens will not show up in any command output, print_r
, var_export
, var_dump
, debug_zval_dump
or otherwise.
This implies that hashing two anonymous functions that share the same signature, refcount, outer scope variables and parameters but a partially heterogeneous function body will yield identical hashes.
If we start a fresh php -a
session, but now bind our Closure
instances to variables first, it might look good at first glance:
$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);
Output:
085323126d01f3e04dacdbb6791f230d99f16fbf4189f98bf8d831185ef13b6c
18a9c0b26bf6f6546d08911d7268abba72e1d12ede2e9619d782deded922ab65
Hey, different hashes! But don't be fooled...
The change in hash is not due to the change in the function body, it is due to the changed refcount!. Not much use for that hash then, is there?
Apart from all this, like Brent says above, you probably want a Class
, not a Closure
.. this is still PHP ;)