ReadOnly Gotchas – A few more limitations with PHP 8.1 ReadOnly Properties

Last month I wrote about the new readonly properties in PHP 8.1, and the difficulties if you want to clone an object that uses them (together with a couple of potential solutions to that problem). The inability to clone an object with readonly properties isn’t the only limitation that you might encounter though; and while the other limitations are mentioned in the PHP documentation, they could easily be overlooked.

You cannot apply readonly to static properties.


class Test {
    public static readonly int $property;
}

results in a Fatal Error when the class is loaded


Fatal error: Static property Test::$property cannot be readonly

Your static analysis tools or unit tests should pick up this error before it gets deployed to a production environment.


You can only use readonly with typed properties.


class Test {
    public readonly $property;
}

results in a Fatal Error when the class is loaded


Fatal error: Readonly property Test::$property must have type

Your static analysis tools or unit tests should pick up this error too.

If you really don’t know what datatype might be passed in for the value of this property, then explicitly specify a type of mixed.


class Test {
    public readonly mixed $property;
}

You can’t specify a default value for a readonly property.


class Test {
    public readonly int $property = 42;
}

As the documentation says, this would initialise the property with a fixed value that couldn’t then be changed (even by constructor arguments); so making it effectively a class constant; and it will generate a Fatal Error when the class definition is loaded.


Fatal error: Readonly property Test::$property cannot have default value

This error should also be picked up by your static analysis tools or unit tests. Hopefully, IDE’s like PHPStorm will also pick up on this and the above mentioned limitations as invalid code, but the latest PHPStorm (2021.2.3) doesn’t yet warn against them.

However, if you’re using constructor property promotion, then the use of a default value in the following example is perfectly sensible


class Test {
    public function __construct(
        public readonly int $property = 42
    ) {}
}

$x = new Test();
$y = new Test(12);

Here, we’re providing a default argument value, not a default property value.


Readonly properties can only be initialised from the scope of the class in which the are defined, no matter what their visibility, so be careful when working with inheritance.


abstract class Greeting {
    public readonly string $recipient;
}

class Seasonal extends Greeting {
    public readonly string $greeting;

    public function __construct(string $greeting, string $recipient) {
        $this->recipient = $recipient;
        $this->greeting = $greeting;
    }
}


$x = new Seasonal('Blessed Yule', 'Mark');

We might think that the code in the constructor will set/initialise both the recipient and the greeting properties, and that would be the case if those properties weren’t readonly. But because they are, we get a fatal runtime error when we try to instantiate the Seasonal class.


Fatal error: Cannot initialize readonly property Greeting::$recipient from scope Seasonal

We need to initialise the recipient property inside the scope of the abstract Greeting class, because the property is defined in that class.


abstract class Greeting {
    public readonly string $recipient;

    public function __construct(string $recipient) {
        $this->recipient = $recipient;
    }
}

class Seasonal extends Greeting {
    readonly string $greeting;

    public function __construct(string $greeting, string $recipient) {
        parent::__construct($recipient);
        $this->greeting = $greeting;
    }
}

and then we won’t get that Fatal runtime error.


As I summarised in my original post on the subject last month: on the surface readonly properties seem really useful, but there are some potential problems with using them. I do believe that the benefits outweigh the drawbacks; but we need to be aware of their limitations, and understand how to work with them. Hopefully this article has helped provide some of that understanding.

This entry was posted in PHP and tagged , , . Bookmark the permalink.

2 Responses to ReadOnly Gotchas – A few more limitations with PHP 8.1 ReadOnly Properties

  1. thookerov says:

    This is a quirk in readonly properties that baffles me: the RFC (https://wiki.php.net/rfc/readonly_properties_v2) specifies readonly objects still permit interior mutability, but i don’t understand why.

    readonly class RO {
    public function __construct(public mixed $prop){}
    }

    $ro1 = new RO([‘foo’ => (object)[‘bar’ => ‘goo’]]); // nest an object in an array
    $ro1->prop[‘foo’]->bar = ‘baz’; // error – yes!

    $ro2 = new RO((object)[‘foo’ = ‘goo’]); // or new stdClass()
    $ro2->prop->foo = ‘bar’; // OK – what??

    All other vanilla types are properly locked, but not object/stdClass. If it’s any higher level class, the readonly modifier on that object applies, but this one exception exists that makes no sense to me. It’s more consistent if stdClass takes the property modifier and applies it internally. Every other type respects readonly – including arrays, which implicitly prevents modifying any internal data AND mutating internal objects.

    Like

  2. Pingback: PHP 8.2 – The Release of Deprecations | Mark Baker's Blog

Leave a comment