PHP 8.1: Readonly Properties

Version8.1
TypeNew Feature

PHP 8.1 brings support for read-only class properties. A class property declared read-only is only allowed to be initialized once, and further changes to the property is not allowed.

Read-only class properties are declared with the readonly keyword* in a typed property.

class User {
    public readonly int $uid;

    public function __construct(int $uid) {
        $this->uid = $uid;
    }
}

$user = new User(42);

Read-only property values can only be set from within the class itself, either from the constructor or another method. Once set, no further modifications are allowed to that property, even from within the class.

class User {
    public readonly int $uid;

    public function __construct(int $uid) {
        $this->uid = $uid;
    }
}
$user = new User(42);
$user->uid = 56;
Error: Cannot modify readonly property User::$uid in ...:...

readonly as a function name

From PHP 8.1 and later, readonly is a reserved PHP keyword. However, it explicitly allows declaring functions with name readonly to accommodate existing PHP applications that declare their own readonly functions, such as WordPress.

Constructor readonly properties

Constructor properties, introduced in PHP 8.0 also support the new readonly flag in the constructor.

class User {
    public function __construct(public readonly int $uid) {}
}

The declaration above is equivalent to:

class User {
    public readonly int $uid;

    public function __construct(int $uid) {
        $this->uid = $uid;
    }
}

Property initialization

A property declared readonly can be initialized from anywhere inside the class. It makes more sense to initialize read-only properties from the constructor, but it is not required to do so.

class User {
    public readonly int $uid;

    public function fetch(int $uid) {
        $this->uid = $uid;
    }
}

$user = new User();
$user->fetch(42);

The snippet above is valid, because the readonly property $uid is only written once, from the fetch() method. However, a second call to the fetch() function throws an Error, because it involves overwriting the $uid property.

$user = new User();
$user->fetch(42);
$user->fetch(16);
Error: Cannot modify readonly property User::$uid in ...:...

Initializing a readonly property outside the scope of the class is not allowed:

class User {
    public readonly int $uid;
}
$user = new User();
$user->uid = 9;
Error: Cannot initialize readonly property User::$uid from global scope in ...:...

Read-only enforcement

PHP actively refuses to modify a readonly property. This includes directly setting a value, incrementing it, references, and array operations.

class User {
    public readonly int $uid;

    public function __construct(int $uid) {
        $this->uid = $uid;
    }
}

$user = new User(42);

None of the following non-exhausting list of changes are allowed:

$user->uid = 16;
$user->uid++;
$uid_ref =& $user->uid;

Unsetting a readonly property

Further, unsetting a readonly property that is already initialized is not allowed either:

unset($user->uid);
Error: Cannot unset readonly property User::$uid in ...:...

Limitations

readonly properties can greatly reduce boilerplate code that are often used to limit write access to class properties, which can now be simplified with a public readonly property. However, the readonly flag may not be an ideal fit in certain use cases.

Only supported on Typed Properties

Read-only properties are only supported on typed property. This is because properties declared without a type are implicitly null until a value is set, and is not compatible with the read-only restriction. Typed properties are "uninitialized" until a value is set.

Attempting to declare a readonly property without a type results in an error:

class User {
    public readonly $uid;
}
Fatal error: Readonly property User::$uid must have type in ... on line ...

Mutability

A readonly property does not provide immutability for objects. For example, if a readonly property holds an object, that object itself can change:

class User {
    public string $username;
}

class Post {
    public readonly User $author;
    public function __construct(User $author) {
        $this->author = $author;
    }
}

$user = new User();
$user->username = 'Foo';

$post = new Post($user);
$user->username = 'Bar';

In the snippet above, Post class stored a readonly property author. The $user object itself can change, and the readonly property does not prevent modifications to the object itself.

If the immutability is required, make sure to clone the object prior to assigning:

class Post {
    public readonly User $author;
    public function __construct(User $author) {
-       $this->author = $author;
+       $this->author = clone $author;
    }
}

Backwards Compatibility Impact

readonly is a reserved keyword in PHP 8.1. Any existing classes or other symbols that use the name readonly (case insensitive) will result in a syntax error in PHP 8.1.

It is not possible to port the readonly functionality to older PHP versions. However, the @readonly / @property-read annotations is understood by static analyzers such as Psalm.

class User {
    /** @readonly */
    public string $username;
}

RFC Discussion Implementation