While writing my slides for the PHP UK Conference I looked much more closely at the changes to closures in PHP 5.4.
The biggest change that people have been asking for, is access to $this
when the closure is defined inside of an object method. This has been added:
Simple enough. However, it didn’t stop there; there was also the addition of Closure::bind()
and Closure->bindTo()
. These methods are identical except one is a static method into which the closure is passed, the second an instance method on the closure itself.
These methods both take two arguments (on top of the closure for the static version): $newthis
and $newscope
.
What this means is that unlike the regular object model the concept of $this
and lexical scope (what is in scope for the function with regards to private/protected methods inside objects) are completely separated.
You can change $this
to be a different object, while leaving the scope as the current object which if you then pass the current object in gives the different object access to the private/protected methods of the current method. That sounds complicated right? It is. A more likely scenario is to keep $this
the same while changing the scope to another object (or static) such that you can pretend you’re accessing it from that other object and not hit private/protected methods.
Either of these scenarios is great for unit testing — I’m sure we’ll see this creep into PHPUnit sooner rather than later (I’m pretty sure Sebastian was promoting access to private/protected methods in reflection at some point — this makes that moot).
However, this can be seriously confusing, consider the following code that intentionally busts the OO model we know and love:
We have two classes, Bar
with two methods, public function A() { }
and private B() { }
, we then have Bat
which uses the __call()
magic method to act as a proxy for Bar
, as well as public function C()
and private function D()
.
Inside __call()
we instantiate an instance of Bar
as $bar
and then if __call()
is able to call the requested function on $bar
it simply does so, otherwise it creates a closure, passing in both the requested function name and $bar
. Then using Closure::bind()
we leave $this
as the current object, but change the lexical scope to be that of $bar
. The result of this is a new closure that is assigned to $bustOO
What this means is that inside the context of the new closure, any call on $this
is referencing the object in which it was originally instantiated, but any calls on the passed in $bar
will be the same as calling $this
from inside $bar
itself.
This means that the call to the private $bar->B()
will now work. Also, the call to $this->C()
works (calling Bat->C()
), but things get tricky when we call $this->D()
.
When this happens, because the lexical scope is $bar
, it cannot access Bat->D()
, which means it calls Bat->__call()
. Inside __call()
the is_callable(array($bar, $function))
check fails (the method doesn’t exist on then tries to call $bar->D()
through the callback, resulting in Fatal error: Call to undefined method Bar::D()
.
The full output looks like this:
Talk about head spinning!
Comments
Matthew Weier O'Phinney
As a succinct explanation: In your example, you’re binding $this to Bat, but the _scope_ is Bar — which means that the closure can only access methods in Bat that are visible to Bar. Since Bar does not extend Bat, that means only public visibility, which means calls to $this->D() will fail.
Lee Davis
Despite the explaination of what its doing, am I the only one thinking _why_ is it doing this? Is this really a good feature? What benefits does binding closures to a new scope really give? And do the benefits really outweight the risks?
When managing a highly layered application its comforting to know that solid API’s (which may contain final / private methods) can encapsulate behaviours within themselves. I know the Protected vs Private debate has been iterated more times that i’d like to mention, but I still feel there is a place for private scope and seeing more features that allow you to break it concerns me.
I can appreciate the common retort of “well if someone has access to the code they can do what they want”. This is true, but as release manager you may take comfort in the fact that certain core layers of the application aren’t touched on a commit, and so fully encapsulated behaviours that should never be executed or become visible from outside of that layer.
This along with the reflection API allowing you to break scope fuels the burning question, how relaxed to fundemental OO principles is PHP going to become in the future. Is it even worth using the visibility keywords for > 5.4?
Comments are closed.