Friday, November 11, 2016

Bypassing private and protected visibility in PHP

Members declared protected can be accessed only within the class itself and by inherited classes. Members declared as private may only be accessed by the class that defines the member.

This is true only in an academic sense: code outside the object can still get and set private and protected members. As usual in PHP, all it takes is a little magic.


Let's see how. Start with a completely sealed object, then use some magic to modify it from "outside":

<?php
class Sealed { private $value = 'foo'; }

$sealed = new Sealed;
var_dump($sealed); // private $value => string(3) "foo"

call_user_func(\Closure::bind(
    function () use ($sealed) { $sealed->value = 'BAZ'; },
    null,
    $sealed
));

var_dump($sealed); // private $value => string(3) "BAZ"

The magic lay in \Closure::bind, which allows an anonymous function to bind to a particular class scope. The documentation says:

If an object is given, the type of the object will be used instead. This determines the visibility of protected and private methods of the bound object.

So, effectively, we're adding a run-time setter to $sealed, then calling that setter. This can be elaborated to generic functions that can force set and force get object members:

<?php
function force_set($object, $property, $value) {
    call_user_func(\Closure::bind(
        function () use ($object, $property, $value) {
            $object->{$property} = $value;
        },
        null,
        $object
    ));
}

function force_get($object, $property) {
    return call_user_func(\Closure::bind(
        function () use ($object, $property) {
            return $object->{$property};
        },
        null,
        $object
    ));
}

force_set($sealed, 'value', 'quux');
var_dump(force_get($sealed, 'value')); // 'quux'

I personally use this in testing and debugging, where I need a quick monkey-patch to accomplish a goal without having the formal architecture to support it. But you should probably not rely on this ability for production quality code. Then again, Composer uses this technique so maybe you can, too.


Updated November 12, 2016: A commenter pointed out @ocramius blogged on this in 2013. To differentiate the two articles, I'll add that one can also use this technique to call private or protected methods:

<?php
function force_call($object, $method, ...$args) {
    return call_user_func(\Closure::bind(
        function () use ($object, $method, $args) {
            return call_user_func_array([ $object, $method ], $args);
        },
        null,
        $object
    ));
}

class Sealed {
    private function init($value) {
        return strtoupper($value);
    }
}
$sealed = new Sealed;
echo force_call($sealed, 'init', 'foo'); // string(3) 'FOO'

Updated January 5, 2017: I stumbled upon a similar blog post by Kazuyuki Hayashi today.

0 comments:

Post a Comment

Share your thoughts!