Take the State of PHP 2026 Survey →

Readonly Property Defaults View as Markdown

Implemented (PHP 8.6)
Authors
Nick Sdot, php@nicksdot.dev
Date
2026-07-04
Version
1.0
Implementation
https://github.com/php/php-src/commit/a2640aebc914b3c6fc997c379b1f91a2c4db8c55
Source
wiki.php.net/rfc/readonly_property_defaults

Vote

Allow readonly properties to have default values as outlined in the RFC? closed
Yes
24
No
0
Abstain
5
29 voters
alcaeus Yes 2026-07-31 09:25
alec Yes 2026-07-24 11:37
alexandredaubois Yes 2026-07-29 06:56
brzuchal Yes 2026-07-29 05:48
bwoebi Yes 2026-07-25 14:18
crell Abstain 2026-07-24 16:14
daniels Yes 2026-07-30 21:06
derick Abstain 2026-07-24 07:33
duncan3dc Yes 2026-07-24 08:36
ericmann Yes 2026-07-27 17:02
galvao Yes 2026-07-27 01:58
iliaa Yes 2026-07-24 14:32
jbnahan Yes 2026-08-04 08:05
jordikroon Yes 2026-07-24 12:29
josh Yes 2026-08-06 09:36
kalle Yes 2026-07-24 05:40
kguest Yes 2026-08-05 14:49
marandall Yes 2026-07-24 11:22
mbeccati Abstain 2026-07-24 10:24
ocramius Yes 2026-07-24 08:38
ramsey Yes 2026-07-25 01:04
sergey Yes 2026-07-27 11:03
shivam Yes 2026-07-31 14:29
svpernova09 Abstain 2026-07-24 13:50
theodorejb Yes 2026-07-24 18:48
timwolla Abstain 2026-07-24 07:04
trowski Yes 2026-07-30 16:43
weierophinney Yes 2026-07-24 11:45
weilindu Yes 2026-07-24 18:04

Discussions on the mailing list

[RFC] Readonly Property Defaults
by Nick Sdot · 14 messages
[VOTE] Readonly Property Defaults
by Nick Sdot · 2 messages

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:50 UTC

Introduction

This RFC seeks to allow default values for instance readonly properties. Historically, default values on readonly properties were not disallowed for technical reasons, but because they were considered “not particularly useful”. With interface properties, introduced in PHP 8.4, this changed. Allowing default values on readonly class properties enables creating a strict contract for implementation property values that will not change at runtime -- as in, constant-like behaviour with contract.

<?php
 
interface Rule
{
    public string $className { get; }
}
 
final class RuleA implements Rule
{
    public readonly string $className = SomeParser::class;
}
 
 
final readonly class RuleB implements Rule
{
    public string $className = SomeParser::class;
}
 
?>

Proposal

This RFC allows instance readonly properties to declare default values. The same applies to properties of readonly classes, because properties declared in a readonly class are implicitly readonly. No new syntax is introduced. This RFC only removes the current compile-time restriction that rejects an explicit default value on a readonly property. The default value follows the same rules as default values for non-readonly properties. Type checks, constant expression restrictions, inheritance checks, and trait composition are unchanged.

Why now?

The reason for not allowing readonly properties was usefulness, not technical. Quote from the original “Readonly Properties” RFC:

As the default value counts as an initialising assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion could become more useful in the future if new expressions are allowed as property default values.

This means the original RFC actively highlighted to look out for future language additions as a reason that would make allowing readonly property default values worthwhile. The interface properties landed in PHP 8.4 are such a reason.

General

The readonly defaults are useful for classes that implement get-only interface properties. Such classes can provide contractable, fixed metadata or blueprint values without boilerplate constructor assignments or getter methods.

<?php
 
interface IngestorBlueprint
{
    public string $name { get; }
    public string $stub { get; }
    public string $path { get; }
    public array $steps { get; }
}
 
final readonly class SourceOneChangelogIngestor implements IngestorBlueprint
{
    public string $name = 'Source One';
    public string $stub = 'stubs/output.md';
    public string $path = 'source-one/changelog/%s/%s';
 
    public array $steps = [
        ParserShapeA::class,
        HandleShapeA::class,
    ];
}
 
?>

This is intentionally not equivalent to a { get; set; } interface property. A readonly property, with or without a default value, satisfies a { get; } requirement but not a { get; set; } requirement.

<?php
 
interface MutableName
{
    public string $name { get; set; }
}
 
final readonly class Example implements MutableName
{
    public string $name = 'Example';
    //  Invalid: a readonly property, even with default value, still only satisfies { get; }, but not { get; set; }.
}
 
?>

A default value counts as the initialising assignment of the readonly property. This means the property is already initialised when the object is created, before the constructor body runs. Any later assignment, including from the constructor, is a modification and fails.

<?php
 
final readonly class Rule
{
    public string $className = SomeParser::class;
 
    public function __construct(string $className) {
        $this->className = $className; // Error: Cannot modify readonly property Rule::$className
    }
}
 
?>

Interaction with inheritance

Readonly properties with defaults follow the existing inheritance rules for properties. A child class may inherit the default value. If a property is redeclared, the normal property compatibility checks apply.

<?php
 
abstract class ParentRule
{
    public readonly int $priority = 1;
}
 
final class ChildRule extends ParentRule
{
    public readonly int $priority = 2;
}
 
var_dump(new ParentRule()->priority); // int(1)
var_dump(new ChildRule()->priority);  // int(2)
 
?>

Interaction with magic methods and unset

Readonly properties without default values keep the existing behaviour: They may be unset before initialisation from the declaring scope. This can make the property visible to magic methods and enables the usual lazy initialisation pattern through __get(). Readonly properties with default values are already initialised when the object is created -- therefore, unset($this->prop) is treated as unsetting an initialised readonly property and throws an error.

<?php
 
final class LazyValue
{
    public readonly int $value = 1;
 
    public function __construct() {
        unset($this->value); // Error: Cannot unset readonly property LazyValue::$value
    }
 
    public function __get(string $name): mixed {
        echo "__get\n";
        return 2;
    }
}
 
$object = new LazyValue();
var_dump($object->value); // int(1), __get() is not called
 
?>

Interaction with clone

Readonly properties with default values behave the same as other initialised readonly properties during cloning. If a class has a __clone() method, the cloned readonly property may be reinitialised once during that method.

<?php
 
final class Counter
{
    public readonly int $value = 1;
 
    public function __clone() {
        $this->value++;
    }
}
 
var_dump((clone new Counter())->value); // int(2)
 
?>

Likewise, clone-with may reinitialise a readonly property with a default value.

<?php
 
final class Counter
{
    public readonly int $value = 1;
 
    public function withValue(int $value): self {
        return clone($this, ['value' => $value]);
    }
}
 
$counter = new Counter();
 
var_dump($counter->value); // int(1)
var_dump($counter->withValue(2)->value); // int(2)
 
?>

Interaction with serialisation

A readonly property with a default value is initialised, hence included in serialised data. During unserialisation, the readonly property value may be restored from the serialised data. This applies both to native object hydration and to assignments via __unserialize(). After unserialisation completes the property is readonly again.

<?php
 
final class Counter
{
  public readonly int $value = 1;
}
 
var_dump(serialize(new Counter)); // string(34) "O:7:"Counter":1:{s:5:"value";i:1;}"
 
var_dump(unserialize('O:7:"Counter":0:{}')->value); // int(1)
var_dump(unserialize('O:7:"Counter":1:{s:5:"value";i:2;}')->value); // int(2)
 
?>
<?php
 
final class Counter
{
    public readonly int $value = 1;
 
    public function __unserialize(array $data): void {
        foreach ($data as $key => $value) {
            $this->{$key} = $value;
        }
    }
}
 
var_dump(serialize(new Counter)); // string(34) "O:7:"Counter":1:{s:5:"value";i:1;}"
 
var_dump(unserialize('O:7:"Counter":0:{}')->value); // int(1)
var_dump(unserialize('O:7:"Counter":1:{s:5:"value";i:2;}')->value); // int(2)
 
?>

Interaction with asymmetric visibility

Readonly properties may already be combined with asymmetric visibility. This RFC does not change those rules. A default value initialises the property immediately, so later writes fail as readonly modifications regardless of set visibility.

<?php
 
final class Example
{
    public public(set) readonly int $id = 1;
}
 
$example = new Example();
$example->id = 2; // Error: Cannot modify readonly property Example::$id
 
?>

Interaction with traits

Traits may declare readonly properties with default values. Existing trait property compatibility rules apply. If two imported trait properties define the same readonly property with the same default value, composition is allowed. If the default values differ, the composition is incompatible.

<?php
 
trait T1
{
    public readonly int $prop = 1;
}
 
trait T2 { 
    public readonly int $prop = 2;
}
 
class Foo 
{
    use T1, T2; // Fatal error: T1 and T2 define the same property ($prop) in the composition of Foo. However, the definition differs...
}
 
?>

Interaction with reflection

Reflection reports readonly properties with default values in the same way as other properties with default values. ReflectionProperty::isReadOnly() returns true, ReflectionProperty::hasDefaultValue() returns true, and ReflectionProperty::getDefaultValue() returns the declared default value.

Interaction with property hooks

None. Readonly properties cannot declare hooks. This remains unchanged. A readonly property with a default value may satisfy a get-only interface property. It does not satisfy an interface property that requires set access.

Non-goals

This RFC does not change:

  • the behaviour of readonly properties without default values
  • how readonly interacts with property hooks
  • how readonly interacts with static properties
  • that a default value on a promoted constructor parameter is a parameter default only

Backward Incompatible Changes

None.

Proposed PHP Version(s)

PHP 8.6

RFC Impact

The change only relaxes a compile-time validation rule for property declarations, the internal representation remains unchanged.

To the Ecosystem

IDEs, language servers, static analysers, and other tools that currently report defaults on readonly properties as disallowed must be updated to allow them.

To Extensions & SAPIs

None.

Future Scope

None.

Voting Choices

Primary Vote requiring a 2/3 majority to accept the RFC:

Vote: Allow readonly properties to have default values as outlined in the RFC? — Yes: 24 · No: 0 · Abstain: 5 (closed)

Patches and Tests

https://github.com/php/php-src/pull/22588

Implementation

https://github.com/php/php-src/commit/a2640aebc914b3c6fc997c379b1f91a2c4db8c55

References

Rejected Features

None.

Changelog

  • 2026-07-04: Initial version.
  • 2026-07-09: Improved examples & formatting.
  • 2026-07-09: Improved un-/serialisation section.
  • 2026-07-14: Added constructor property promotion to non-goals.