PHP binding method to another class

Viewed 1909

Can i bind method of class Foo to class Bar? And why the code below throws a warning "Cannot bind method Foo::say() to object of class Bar"? With function instead of method code works fine.

P.S. I know about extending) it is not practical question, just want to know is it real to bind non-static method to another class

class Foo {

    public $text = 'Hello World!';

    public function say() {
        echo $this->text;
    }

}

class Bar {

    public $text = 'Bye World!';

    public function __call($name, $arguments) {
        $test = Closure::fromCallable(array(new Foo, 'say'));
        $res = Closure::bind($test, $this);
        return $res();
    }

}

$bar = new Bar();
$bar->say();

Code below works fine

 function say(){
    echo $this->text;
 }
 class Bar {

    public $text = 'Bye World!';

    public function __call($name, $arguments) {
        $test = Closure::fromCallable('say');
        $res = Closure::bind($test, $this);
        return $res();
    }

}

$bar = new Bar();
$bar->say();
2 Answers

This is currently not supported. If you want to bind a closure to a new object, it must not be a fake closure, or the new object must be compatible with the old one (source).

So, what is a fake closure: A fake closure is a closure created from Closure::fromCallable.

This means, you have two options to fix your problem:

  1. Bar must be compatible with the type of Foo - so just make Bar extend from Foo, if possible.

  2. Use unbound functions, like annonymous, static or functions outside of classes.

It is not supported by PHP. However in PHP 7.0 it was kinda possible. Consider this example:

class Foo {
    private $baz = 1;
    
    public function baz() {
        var_dump('Foo');
        var_dump($this->baz);
    }
}


class Bar {
    public $baz = 2;
    
    public function baz() {
        var_dump('Bar');
        var_dump($this->baz);
    }
}


$fooClass = new ReflectionClass('Foo');
$method = $fooClass->getMethod('baz');

$foo = new Foo;
$bar = new Bar;

$closure = $method->getClosure($foo);

$closure2 = $closure->bindTo($bar);

$closure2();

The output of foregoing code is:

string(3) "Foo"
int(2)

And this means method Foo::baz was called on object $bar and has accessed its $baz property rather than Foo::$baz.

Also, please note that Bar::$baz is public property. If it was private, php would throw Fatal error cannot access private property. This could've be fixed up by changing the scope of closure $closure->bindTo($bar, $bar);, however doing this was already prohibited in php7.0 and would lead to a following warning: Cannot rebind scope of closure created by ReflectionFunctionAbstract::getClosure().

There's workaround however, which will work for latest versions of php. Laravel created a splendid package called laravel/serializable-closure. What it does is simple - it reads source code of closure in order to serialize it and be able to unserialize it later with all necessary context.

So basically the functionality of unserialized closure remains the same, however it is different from PHP's perspective. PHP doesn't know it was created from class method and therefore allows to bind any $this.

Final variant will look like this:

$callback = [$foo, 'baz'];

$closure = unserialize(
    serialize(new SerializableClosure(Closure::fromCallable($callback)))
)->getClosure();

$closure->call($bar);

Please, note that serialization and unserialization are expensive operations, so do not use provided solution unless there's no other solution.

Related