PHP Readonly Classes

PHP readonly classes provide a concise way to declare data-focused objects whose properties cannot be reassigned after initialization. PHP 8.2 introduced the readonly class modifier, which applies readonly behavior to every declared instance property and prevents dynamic properties.

Readonly limits property reassignment; it does not guarantee deep immutability. If a readonly property contains an object, that object can still change internally unless its own design prevents changes.

Use readonly classes for value objects, configuration data, command objects, and immutable-style data transfer objects. Do not use them when the object's normal responsibility requires changing its properties over time.

Declare a Readonly Class

Place readonly before class. Every instance property must have a type, and you normally initialize properties in the constructor.

Example:

<?php
// Represent a price that should not be reassigned after creation
readonly class Price
{
    public function __construct(
        public int $amountInPaise,
        public string $currency
    ) {
        if ($amountInPaise < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
    }
}

$price = new Price(149900, 'INR');
echo $price->currency; // INR

Constructor property promotion works well because it declares and initializes each property in one place.

What Readonly Prevents

After a property is initialized, PHP rejects reassignment, incrementing, reference binding, and unsetting. Array elements stored directly in a readonly property also cannot be changed because that operation modifies the property value.

<?php
// Each operation below attempts to modify readonly state
$price->currency = 'USD';          // Error
$price->amountInPaise++;           // Error
unset($price->currency);            // Error

Readonly Class Rules

Rule Reason
Properties must be typed Untyped properties have an implicit null default
No static properties Readonly applies to instance state
No dynamic properties Undeclared state would bypass the class contract
No property defaults A default would initialize the property before runtime assignment
Promoted constructor defaults are allowed The default is a constructor parameter default

A readonly class cannot opt into dynamic properties with #[AllowDynamicProperties]. Attempting to create an undeclared property raises an error.

Initialize Properties Once

A property can be initialized from class scope. In PHP 8.2 and 8.3, initialization is limited to the scope that declares the property. From PHP 8.4, readonly properties are implicitly protected(set), so child scope can initialize them unless narrower set visibility is declared. Constructor initialization remains the clearest cross-version pattern.

<?php
readonly class UserId
{
    public string $value;

    public function __construct(string $value)
    {
        // Initialize the property exactly once
        $this->value = trim($value);
    }
}

Readonly Is Not Deep Immutability

A readonly property holding an object cannot point to a different object, but methods can still change the referenced object's internal state.

<?php
final class Address
{
    public function __construct(public string $city) {}
}

readonly class Customer
{
    public function __construct(public Address $address) {}
}

$customer = new Customer(new Address('London'));

// Legal: the property still references the same Address object
$customer->address->city = 'Manchester';

// Illegal: this replaces the readonly property value
$customer->address = new Address('Delhi');

Use immutable nested objects or defensive cloning when the complete object graph must remain unchanged.

Readonly Class Inheritance

Readonly status is part of the class contract. A readonly class can extend only a readonly parent, and a child of a readonly parent must also be readonly.

<?php
readonly class Identifier
{
    public function __construct(public string $value) {}
}

// Valid because both parent and child are readonly
readonly class OrderId extends Identifier {}

Traits used by a readonly class must not introduce incompatible untyped, static, or mutable properties.

Clone Readonly Objects

Cloning creates another object with the same property values. PHP 8.3 added controlled reinitialization of readonly properties inside __clone(), which allows deep-cloning of nested objects.

<?php
readonly class Profile
{
    public function __construct(public Address $address) {}

    public function __clone(): void
    {
        // PHP 8.3+: reinitialize once during cloning
        $this->address = clone $this->address;
    }
}

$original = new Profile(new Address('Bristol'));
$copy = clone $original;
$copy->address->city = 'Bath';

// The original nested object remains unchanged
echo $original->address->city; // Bristol

Readonly Classes and Serialization

Serialization, hydration, reflection, and framework mappers must respect initialization rules. Test the exact library and PHP version you use. Prefer constructor-based hydration so validation and invariants run consistently.

Inspect Readonly Status with Reflection

<?php
// Detect readonly classes in tooling or framework code
$reflection = new ReflectionClass(Price::class);

var_dump($reflection->isReadOnly());
var_dump(($reflection->getModifiers() & ReflectionClass::IS_READONLY) !== 0);

Readonly Class vs Readonly Property

Approach Use It When
Readonly property Only selected properties must resist reassignment
Readonly class Every instance property follows the readonly contract
Regular class The object intentionally changes state

Best Practices

  • Validate constructor arguments before completing initialization.
  • Use small, focused readonly value objects.
  • Use immutable nested types when deep immutability matters.
  • Document the minimum supported PHP version, especially for cloning behavior.
  • Return new objects for changed values instead of mutating existing ones.
  • Test framework hydration and serialization paths.

Conclusion

PHP readonly classes make an all-properties-readonly contract clear and concise. They prevent reassignment and dynamic properties, work naturally with promoted constructor properties, and support controlled deep cloning in current PHP versions. Their limit is equally important: referenced objects may still mutate. Design the full object graph deliberately when you need genuine immutability.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram