Home
Fun with PHP: Changing Readonly Properties and Other Shenanigans
Liked 3 times

Did you know that PHP’s userland lets you change a readonly property multiple times? Or that you can modify an enum’s value? Or even change internal object properties that should never be changed? Let me show you some truly cursed PHP tricks.

Changing a Readonly Property

So, you know how readonly properties are, well… read-only? Turns out they’re not!

I stumbled upon this mechanism just as PHP started deprecating it — but hey, if you ignore the deprecation warnings, you can still use it up until PHP 9!

A bit of theory first: readonly properties can only be assigned inside a class constructor. After that, they’re supposed to be immutable.

Readonly properties can actually be set anywhere in the class and since 8.4 from anywhere. 

final readonly class ReadonlyClass
{
    public string $someProp;

    public function __construct()
    {
        $this->someProp = 'unchangeable!';
    }
}

The only official way to set such a property outside the class (pre 8.4) is via reflection — but even then, only if the property hasn’t been initialized yet:

final readonly class ReadonlyClass
{
    public string $someProp;
}
$test = new ReadonlyClass();
$reflection = new ReflectionClass(ReadonlyClass::class)->getProperty('someProp');
$reflection->setValue($test, 'changed once!');
var_dump($test->someProp);
$reflection->setValue($test, 'changed twice?');

This produces the predictable result:

string(13) "changed once!"

Fatal error: Uncaught Error: Cannot modify readonly property ReadonlyClass::$someProp

You get the same error no matter whether you do it in a constructor, set it in a different method or simply use reflection. As soon as you change the value in any official way more than once, you get an error.

Changing It Multiple Times

Enough stalling — let’s dive in! The magical object that can modify a readonly property (and much more) is ArrayObject.

Normally, you’d use ArrayObject to wrap an array. But it also accepts any object as the backing value — and that’s where the fun begins. Once you know how PHP stores properties internally (which is actually pretty simple), chaos follows.

Let’s start with this class:

final readonly class ReadonlyClass
{
    public string $someProp;
    private string $somePrivateProp;
    protected string $someProtectedProp;

    public function __construct()
    {
        $this->someProp = 'unchangeable?';
        $this->somePrivateProp = 'unchangeable?';
        $this->someProtectedProp = 'unchangeable?';
    }

    public function getSomePrivateProp(): string
    {
        return $this->somePrivateProp;
    }

    public function getSomeProtectedProp(): string
    {
        return $this->someProtectedProp;
    }
}

Now we create an instance and wrap it in an ArrayObject:

$instance = new ReadonlyClass();
$arrayObj = new ArrayObject($instance);

And now comes the fun part:

// simply use the property name for public properties
$arrayObj['someProp'] = 'changeable public!';
// use "\0[FQN]\0[Property name]" for private properties
$arrayObj["\0ReadonlyClass\0somePrivateProp"] = 'changeable private!';
// use "\0*\0[Property name]" for protected properties
$arrayObj["\0*\0someProtectedProp"] = 'changeable protected!';

var_dump($instance->someProp, $instance->getSomePrivateProp(), $instance->getSomeProtectedProp());

This prints:

string(18) "changeable public!"
string(19) "changeable private!"
string(21) "changeable protected!"

And just like that, you’ve changed an unchangeable property. You can modify it as many times as you want. So… what other arcane tricks are possible?

Changing an Enum Value

Enums are basically fancy objects that represent a specific named instance — optionally with a value. The key difference from old userland implementations is that PHP guarantees every enum case is a unique instance that’s always equal to itself, no matter where it’s referenced from.

In other words, an enum is really just an object, and ->value or ->name are plain properties.

enum MyEnum: string {
    case A = 'a';
    case B = 'b';
}

$arrayObj = new ArrayObject(MyEnum::A);
$arrayObj['value'] = 'b';
$arrayObj['name'] = 'C';

var_dump(MyEnum::A->value);
var_dump(MyEnum::A->name);

This prints exactly what you’d expect after reading the previous example:

string(1) "b"
string(1) "C"

Even more amusing: Running var_dump(MyEnum::A); now prints enum(MyEnum::C).

It won’t actually make it equal to another enum case, but if you use the value somewhere and reconstruct it using MyEnum::from(), you’ll get back MyEnum::B.

If you try to serialize and deserialize it, you’ll get an error — because MyEnum::C doesn’t exist:

var_dump(MyEnum::from(MyEnum::A->value));
var_dump(unserialize(serialize(MyEnum::A)));

The first prints enum(MyEnum::B), while the second throws a warning: Undefined constant MyEnum::C.

Breaking Types

ArrayObject is so powerful that even the type system trembles before it. Types? Mere suggestions!

final class TestTypedClass
{
    public string $str = 'test';
    public bool $bool = true;
    public int $int = 42;
}

$instance = new TestTypedClass();
$arrayObj = new ArrayObject($instance);

$arrayObj['str'] = 5;
$arrayObj['bool'] = 'hello';
$arrayObj['int'] = new stdClass();

var_dump($instance->str, $instance->bool, $instance->int);

Output:

int(5)
string(5) "hello"
object(stdClass)#3 (0) {
}

So if you ever thought “Hmm, this boolean could really use more than two possible values” — now you know how!

Dynamic Properties Everywhere

Some internal classes like Closure, Generator, and DateTime disallow dynamic properties. Nevermore!

$closure = fn () => true;
$arrayObject = new ArrayObject($closure);
$arrayObject['test'] = 'hello';

var_dump($closure->test);
// prints string(5) "hello"

Crashing PHP

And finally — my favourite one! Ever wanted to cause a segmentation fault? Try this:

$exception = new Exception("Hello there!");
$arrayObject = new ArrayObject($exception);
$arrayObject["\0Exception\0trace"] = -1;

var_dump($exception->getTraceAsString());

That gave me one beautiful Segmentation fault (core dumped)!

So, how did you like these all-powerful ArrayObject shenanigans?


Tip: To comment on or like this post, open it on your favourite Fediverse instance, such as Mastodon or Lemmy.
© 2024 Dominik Chrástecký. All rights reserved.