rfc:readonly_property_defaults

PHP RFC: Readonly Property Defaults

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 interfaces 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 in PHP 8.4 landed interface properties 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)
 
?>

Interaction with serialisation

Serialisation follows existing readonly property behaviour. A readonly property with a default value is initialised, so it is included in normal object serialisation. Unserialisation continues to use the existing object hydration semantics and is not changed by this RFC.

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 private(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.

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

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:

Allow readonly properties to have default values as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

Implementation

tbd

References

Rejected Features

None.

Changelog

  • 2026-07-04: Initial version.
rfc/readonly_property_defaults.txt · Last modified: by nicksdot