Accessing Private Properties in PHP without Reflection
I was discussing this technique during a code review recently, and I realized this cool technique might not be well known. I discovered it quite by accident a few years ago.
Sometimes, strictly for testing purposes, we might need to access a private or protected property or method. Our usual inclination is to use Reflection to do this. Reflection is a bit cumbersome, though, because there’s a lot of extra boilerplate to set it up.
But closures actually provide us with a cool way to do that much more easily. If you’ve ever used JavaScript’s bind
method, we can do the same thing in PHP.
Let’s start with a sample class:
1 | class Person |
Non-static properties
We know how to read and change $me->name
. It’s public, so there’s no problem there.
Now, what if we need, somewhere in a test, to set the exact age. That’s a protected property, so we can’t just directly change it, but Closures can give us a way around that.
1 | (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 |
So, what did we do here? Let’s break it down a bit.
1 | $changeAge = (fn (int $newAge) => $this->age = $newAge); |
First, we create a closure that changes $this->age
to whatever is passed to it. If we tried to do $changeAge(30)
, we’d get an error, though, because we defined this outside of the class. $this
doesn’t actually exist here, or, if we put this code in a unit test, $this
would refer to the unit test class itself.
That’s where call
comes in. Closure::call
binds a closure to a new object, and calls it with whatever args you passed to it. In other words, the first argument passed to call
become the $this
within the closure. Any other arguments are passed to the function itself.
1 | $changeAge->call($me, 30); |
We can also simply access the $age
and $cool
properties without changing them. So, I can do:
1 | $amICool = (fn () => $this->cool)->call($me); // true |
Static properties
That works great for the non-static properties or methods. What about the static ones?
1 | $mySpecies = (fn () => static::$species)->bindTo(null, Person::class)(); // homo sapien |
So, this is a little different. First, we’re using Closure::bindTo
which returns a new closure, which has been re-bound to the specified object or class. With bindTo
, you can pass an object (like call
), or you can leave that null and pass a class instead, which changes the static binding. That’s what we’ve done here. And since it returns a new closure, you then have to call it, which is why we have an extra ()
after.
So, let’s break this down step-by-step as well.
1 | $getSpecies = (fn () => static::$species); |
A closure which returns that $species
of the current scope.
1 | $boundGetSpecies = $getSpecies->bindTo(null, Person::class); |
A new closure, which changes the scope to Person
, meaning any references to static
refer to Person
.
1 | $mySpecies = $boundGetSpecies(); |
Or, if we to change the species to an arbitrary value, as we did in the third one:
1 | // Create species changing closure, initially bound to the current scope |
Using bindTo
with an object
You can use bindTo
in a similar way to call
.
1 | (fn () => $this->birthday())->bindTo($me)(); |
Let’s break it down.
1 | $haveABirthday = (fn () => $this->birthday()); |
Closure which calls birthday()
on the object within the current scope.
1 | $haveMyBirthday = $haveABirthday->bindTo($me); |
Create a new closure with $me
as the scope. This way, any reference to $this
refers to $me
.
1 | $haveMyBirthday(); |
Since $haveMyBirthday
is a closure, we have to actually call it.
Conclusion
Sure, we can use ReflectionClass
and ReflectionObject
to do all of this, but this technique greatly simplifies it, since calling a single private method is a single line of code.
I actually use this in a package I recently wrote that should make this even easier.
And to reiterate, I do not recommend doing this in production code. There’s a reason visibility exists. We shouldn’t circumvent it like this in code on our server. But, if we need to fiddle around with some objects for testing, this technique can simplify that for us.