Closure::bindTo
(PHP 5 >= 5.4.0, PHP 7, PHP 8)
Closure::bindTo — 新しくバインドしたオブジェクトとクラスのスコープで、クロージャを複製する
説明
自身と同じ本体とバインド変数を持つ新しい 無名関数 を作って返します。しかし、バインドするオブジェクトは変わって新しいクラスのスコープとなります。
“バインドするオブジェクト” によって、$this
が関数本体で持つ値が決まり、“クラスのスコープ”
は、無名関数からどのクラスの private メンバーや protected メンバーにアクセスできるのかが決まります。
すなわち、無名関数から見えるメンバーは、
その無名関数が newScope
クラスのメソッドであった場合と同じものになります。
static なクロージャは何もオブジェクトをバインドできません
(newThis
の値は null
でなければなりません) が、
それでもこのメソッドを使ってクラスのスコープを変えることができます。
このメソッドが static でないクロージャに関して保証するのは、
バインドされたインスタンスを持っていればスコープ内にあるということです。
また、その逆も成り立ちます。そのため、static でないクロージャにスコープとして
null
インスタンスを渡すとそれはstaticとなり、staticでなくスコープにもないクロージャに
null
でないインスタンスを渡すと、特定されていない何らかのクラスのスコープに入ります。
注意:
単に無名関数を複製したいだけの場合は、 cloning を使うこともできます。
パラメータ
newThis
-
指定した無名関数をバインドするオブジェクト。クロージャのバインドを解除するには
null
を指定します。 newScope
-
クロージャを関連づけるクラススコープ、あるいは 'static' で現在のスコープを維持します。 オブジェクトを渡した場合は、そのオブジェクトの型をその代わりに使います。 これは、バインドしたオブジェクトの protected メソッドや private メソッドのアクセス権を決めます。 内部クラス (のオブジェクト) を をパラメータとして渡すことはできません。
例
例1 Closure::bindTo() の例
<?php
class A
{
private $val;
public function __construct($val)
{
$this->val = $val;
}
public function getClosure()
{
// このオブジェクトとスコープにバインドしたクロージャを返します。
return function() {
return $this->val;
};
}
}
$ob1 = new A(1);
$ob2 = new A(2);
$cl = $ob1->getClosure();
echo $cl(), "\n";
$cl = $cl->bindTo($ob2);
echo $cl(), "\n";
?>
上の例の出力は、 たとえば以下のようになります。
1 2
参考
- 無名関数
- Closure::bind() - バインドされたオブジェクトとクラスのスコープでクロージャを複製する
User Contributed Notes 8 notes
We can use the concept of bindTo to write a very small Template Engine:
#############
index.php
############
<?php
class Article{
private $title = "This is an article";
}
class Post{
private $title = "This is a post";
}
class Template{
function render($context, $tpl){
$closure = function($tpl){
ob_start();
include $tpl;
return ob_end_flush();
};
$closure = $closure->bindTo($context, $context);
$closure($tpl);
}
}
$art = new Article();
$post = new Post();
$template = new Template();
$template->render($art, 'tpl.php');
$template->render($post, 'tpl.php');
?>
#############
tpl.php
############
<h1><?php echo $this->title;?></h1>
You can do pretty Javascript-like things with objects using closure binding:
<?php
trait DynamicDefinition {
public function __call($name, $args) {
if (is_callable($this->$name)) {
return call_user_func($this->$name, $args);
}
else {
throw new \RuntimeException("Method {$name} does not exist");
}
}
public function __set($name, $value) {
$this->$name = is_callable($value)?
$value->bindTo($this, $this):
$value;
}
}
class Foo {
use DynamicDefinition;
private $privateValue = 'I am private';
}
$foo = new Foo;
$foo->bar = function() {
return $this->privateValue;
};
// prints 'I am private'
print $foo->bar();
?>
Private/protected members are accessible if you set the "newscope" argument (as the manual says).
<?php
$fn = function(){
return ++$this->foo; // increase the value
};
class Bar{
private $foo = 1; // initial value
}
$bar = new Bar();
$fn1 = $fn->bindTo($bar, 'Bar'); // specify class name
$fn2 = $fn->bindTo($bar, $bar); // or object
echo $fn1(); // 2
echo $fn2(); // 3
If you want to unbind completely the closure and the scope you need to set both to null:
<?php
class MyClass
{
public $foo = 'a';
protected $bar = 'b';
private $baz = 'c';
/**
* @return array
*/
public function toArray()
{
// Only public variables
return (function ($obj) {
return get_object_vars($obj);
})->bindTo(null, null)($this);
}
}
?>
In this example, only the public variables of the class are exported (foo).
If you use the default scope (->bindTo(null)) also protected and private variables are exported (foo, bar and baz).
It was hard to figure it out because there is nowhere mentioned in the documentation that you can use null as a scope.
Access private members of parent classes; playing with the scopes:
<?PHP
class Grandparents{ private $__status1 = 'married'; }
class Parents extends Grandparents{ private $__status2 = 'divorced'; }
class Me extends Parents{ private $__status3 = 'single'; }
$status1_3 = function()
{
$this->__status1 = 'happy';
$this->__status2 = 'happy';
$this->__status3 = 'happy';
};
$status1_2 = function()
{
$this->__status1 = 'happy';
$this->__status2 = 'happy';
};
// test 1:
$c = $status1_3->bindTo($R = new Me, Parents::class);
#$c(); // Fatal: Cannot access private property Me::$__status3
// test 2:
$d = $status1_2->bindTo($R = new Me, Parents::class);
$d();
var_dump($R);
/*
object(Me)#5 (4) {
["__status3":"Me":private]=>
string(6) "single"
["__status2":"Parents":private]=>
string(5) "happy"
["__status1":"Grandparents":private]=>
string(7) "married"
["__status1"]=>
string(5) "happy"
}
*/
// test 3:
$e = $status1_3->bindTo($R = new Me, Grandparents::class);
#$e(); // Fatal: Cannot access private property Me::$__status3
// test 4:
$f = $status1_2->bindTo($R = new Me, Grandparents::class);
$f();
var_dump($R);
/*
object(Me)#9 (4) {
["__status3":"Me":private]=>
string(6) "single"
["__status2":"Parents":private]=>
string(8) "divorced"
["__status1":"Grandparents":private]=>
string(5) "happy"
["__status2"]=>
string(5) "happy"
}
*/
?>
Clear the stack trace:
<?PHP
use Exception;
use ReflectionException;
$c = function()
{
$this->trace = [];
};
$c = $c->bindTo($R = new ReflectionException, Exception::class);
$c();
try
{
throw $R;
}
catch(ReflectionException $R)
{
var_dump($R->getTrace());
}
/*
array(0) {
}
*/
?>
With rebindable $this at hand it's possible to do evil stuff:
<?php
class A {
private $a = 12;
private function getA () {
return $this->a;
}
}
class B {
private $b = 34;
private function getB () {
return $this->b;
}
}
$a = new A();
$b = new B();
$c = function () {
if (property_exists($this, "a") && method_exists($this, "getA")) {
$this->a++;
return $this->getA();
}
if (property_exists($this, "b") && method_exists($this, "getB")) {
$this->b++;
return $this->getB();
}
};
$ca = $c->bindTo($a, $a);
$cb = $c->bindTo($b, $b);
echo $ca(), "\n"; // => 13
echo $cb(), "\n"; // => 35
?>
If you, like me, did not immediately understand what exactly "(an object of) an internal class" in the documentation about the 'newScope' parameter:
By an internal class, the documentation means any internal PHP class such as 'stdClass', 'Closure', 'WeakMap', and etc:
<?php
class A {}
$a = new A();
$closure = fn() => null;
$binded = $closure->bindTo($a, 'stdClass',); // Cannot bind closure to scope of internal class stdClass
$binded = $closure->bindTo($a, $closure,); // Warning: Cannot bind closure to scope of internal class Closure etc.
Get all object vars without using Reflection:
<?php
declare(strict_types=1);
class A
{
private $foo = 'foo';
protected $bar = 'bar';
public $buz = 'buz';
}
function get_object_vars_all($object): array
{
if (!\is_object($object)) {
throw new \InvalidArgumentException(sprintf('The argument should be an object, "%s" given.', get_debug_type($object)));
}
$closure = function () {
return get_object_vars($this);
};
return $closure->bindTo($object, $object)();
}
$a = new A();
var_dump(get_object_vars($a));
var_dump(get_object_vars_all($a));
?>
The output:
array(1) {
["buz"]=>
string(3) "buz"
}
array(3) {
["foo"]=>
string(3) "foo"
["bar"]=>
string(3) "bar"
["buz"]=>
string(3) "buz"
}