8

This is bound to beg design questions, but I want to serialize or hash a closure in PHP such that I have a unique identifier for that closure.

I don't need to be able to call the closure from that, I just need a unique identifier for it that is accessible from inside and outside of the closure itself, i.e. a method that accepts a closer will need to generate an id for that closure, and the closure itself will need to be able to generate that same id

Things I've tried so far:

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

The Reflection API doesn't seem to offer anything I might be able to use to create an ID either.

4

9 回答 9

8

My solution is more general and respects static parameters for closure. To make the trick, you can pass a reference to the closure inside the closure:

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>";
于 2013-01-31T07:33:52.803 回答
6

You could all that you need write your own, your own closures having a getId() or getHash() or whatever.

Example (Demo):

1: Hello world
2: Hello world

First closure (ID: 1), ID read in calling context. Second closure (ID: 2), ID read from within the closure (where self-reference).

Code:

<?php
/**
 * @link http://stackoverflow.com/questions/13983714/serialize-or-hash-a-closure-in-php
 */

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');

I have no clue if that suits your needs, maybe it gives you some ideas. I suggested spl_object_hash but didn't understood the discussion much why it does not or in the end then does work.

于 2012-12-21T05:43:52.060 回答
5

Ok, here is the only thing I can think of:

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

If you like, you can hash it with md5 or whatnot. If the function is generated from a string however, you should seed that with a uniqid()

于 2012-12-21T03:56:49.597 回答
3

PHP anonymous functions are exposed as instances of the Closure class. As they're basically objects, spl_object_hash will return a unique identifier when handed one. From the PHP interactive prompt:

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

Those identifiers might look the same, but they differ by one letter in the middle.

The identifier is good only for that request, so expect it to change between calls, even if the function and any use'd variables don't change.

于 2012-12-21T03:50:04.600 回答
2

Superclosure provides a convenience class that allows you to serialize/unserialize closures, among other things.

于 2014-04-25T17:05:57.490 回答
1

It sounds like you want to generate a signature. Creating a signature from outside the closure will be nearly impossible to reproduce if the closure accepts any parameters. The data passed in will change the generated signature.

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

Even if your closure doesn't accept parameters, you still have the problem of storing and persisting the signature inside the closure. You might be able to do something with a static variable inside the closure so it only initializes once and retains the "signature". But that gets messy on how to retrieve it.

It really sounds like you want a class, not a closure. It would solve all of these problems. You could pass in a "salt" on instantiation and have it generate a signature using the salt (i.e. a random number). That would make the signature unique. You could then retain that salt, recreate a class using the exact same constructor parameters (i.e. salt) and compare that with the signature on file in the already created class.

于 2012-12-21T03:35:00.223 回答
0

Possible solution arrived at with the help of @hakre and @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__; };
    }
}
于 2012-12-21T04:30:14.820 回答
0

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

于 2020-09-19T13:43:37.157 回答
0

Just use https://github.com/opis/closure ,the library is maintained and it makes use of SplObjectStorage to create an wrapper for the closure object. You can use serialize and unserialize like this:

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


    $serialized = opisSerialize(new SerializableClosure($closure));
    $wrapper = opisUnserialize($serialized);
于 2020-11-22T20:20:24.570 回答