cross-posted from: https://chrastecky.dev/post/24
Changing a Readonly Property
So, you know how
readonlyproperties 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:
readonlyproperties can only be assigned inside a class constructor. After that, they’re supposed to be immutable.final readonly class ReadonlyClass { public string $someProp; public function __construct() { $this->someProp = 'unchangeable!'; } }The only official way to set such a property outside the constructor 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::$somePropChanging It Multiple Times
Enough stalling — let’s dive in! The magical object that can modify a
readonlyproperty (and much more) isArrayObject.Normally, you’d use
ArrayObjectto 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 a class, and
->valueor->nameare 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 examples:
string(1) "b" string(1) "C"Even more amusing: Running
var_dump(MyEnum::A);now printsenum(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 backMyEnum::B.If you try to serialize and deserialize it, you’ll get an error — because
MyEnum::Cdoesn’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
ArrayObjectis 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, andDateTimedisallow 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
ArrayObjectshenanigans?