Readonly Properties
Readonly Properties What Is a Readonly Property? In simple terms: A readonly property can be given a value once and can never be changed afterwards. It is like writing an order number on an invoice in
What Is a Readonly Property?
In simple terms: A readonly property can be given a value once and can never be changed afterwards. It is like writing an order number on an invoice in permanent ink: anyone can read it, but nobody can alter it.
Technically: Since PHP 8.1, a property declared with the readonly modifier can be initialized only once, and only from inside the class (normally in the constructor). Any later attempt to modify, increment, append to, or unset it throws an Error. Readonly properties must have a type and cannot have a default value.
Why Readonly Properties Exist
Many values should never change after an object is created: an order's ID, a product's SKU, the timestamp of a payment, the amount of a transaction. Before PHP 8.1, protecting such values required private properties plus getter methods. With readonly, a property can be public for reading while remaining unchangeable:
// Before PHP 8.1
private string $sku;
public function getSku(): string { return $this->sku; }
// PHP 8.1+
public readonly string $sku;
Declaring and Using Readonly Properties
Example
<?php
final class Product
{
public readonly string $sku;
public readonly string $name;
public readonly int $pricePaise;
public function __construct(string $sku, string $name, int $pricePaise)
{
if ($pricePaise <= 0) {
throw new InvalidArgumentException('Price must be positive.');
}
$this->sku = strtoupper($sku);
$this->name = $name;
$this->pricePaise = $pricePaise;
}
}
$pen = new Product('pen-01', 'Gel Pen', 2000);
echo "{$pen->sku}: {$pen->name}, " . $pen->pricePaise / 100 . " rupees\n";
try {
$pen->pricePaise = 1;
} catch (Error $e) {
echo $e->getMessage() . "\n";
}
Output
PEN-01: Gel Pen, 20 rupees
Cannot modify readonly property Product::$pricePaise
How It Works
- The constructor initializes each readonly property exactly once — after validating and normalizing the values.
- Reading the properties works from anywhere, because they are
public. - Any attempt to assign a new value throws an
Error, even from inside the class.
With Constructor Promotion
Readonly properties are most often declared through constructor property promotion:
final class Product
{
public function __construct(
public readonly string $sku,
public readonly string $name,
public readonly int $pricePaise,
) {}
}
The Rules of Readonly
| Rule | Error If Violated |
|---|---|
| Must have a declared type | Readonly property Product::$x must have type |
| Cannot have a default value | Readonly property Product::$x cannot have default value |
| Can be initialized only once | Cannot modify readonly property Product::$x |
| Must be initialized from inside the class scope | Cannot initialize readonly property Product::$x from global scope |
| Cannot be unset after initialization | Cannot unset readonly property Product::$x |
| Cannot be static | Compile error |
Initialization Only From Inside the Class
<?php
final class Invoice
{
public readonly int $number;
}
$invoice = new Invoice();
try {
$invoice->number = 1001;
} catch (Error $e) {
echo $e->getMessage() . "\n";
}
Output
Cannot initialize readonly property Invoice::$number from global scope
Even the first assignment is rejected when it comes from outside the class. This guarantees that the class itself controls the value. (Since PHP 8.4, readonly properties behave like protected(set), so child classes may also initialize them.)
Arrays Are Fully Protected
<?php
final class Course
{
public function __construct(public readonly array $topics) {}
public function addTopic(string $topic): void
{
$this->topics[] = $topic;
}
}
$course = new Course(['Variables', 'Arrays']);
try {
$course->addTopic('Functions');
} catch (Error $e) {
echo $e->getMessage() . "\n";
}
Output
Cannot modify readonly property Course::$topics
Appending to, or changing an element of, a readonly array counts as modifying the property.
Interior Mutability: Readonly Is Shallow
A readonly property that holds an object cannot be replaced with another object — but the object itself may still be changed through its own methods:
<?php
final class Event
{
public function __construct(public readonly DateTime $startsAt) {}
}
$event = new Event(new DateTime('2026-10-20 10:00'));
$event->startsAt->modify('+1 day');
echo $event->startsAt->format('Y-m-d H:i') . "\n";
Output
2026-10-21 10:00
readonly protects the reference, not the referenced object. To make the event's start time truly unchangeable, use an immutable type:
public readonly DateTimeImmutable $startsAt;
DateTimeImmutable::modify() returns a new object instead of changing the existing one, so the event's value can no longer change. The same applies to your own classes: readonly objects are only fully immutable if the objects they contain are immutable too.
Changing Values: The "Wither" Pattern
Immutable objects are not changed; instead, a method returns a new object with the modified value. Such methods are commonly named with...():
<?php
final class Product
{
public function __construct(
public readonly string $sku,
public readonly string $name,
public readonly int $pricePaise,
) {}
public function withPrice(int $pricePaise): self
{
if ($pricePaise <= 0) {
throw new InvalidArgumentException('Price must be positive.');
}
return new self($this->sku, $this->name, $pricePaise);
}
public function formattedPrice(): string
{
return '₹' . number_format($this->pricePaise / 100, 2);
}
}
$original = new Product('PEN-01', 'Gel Pen', 2000);
$updated = $original->withPrice(2500);
echo "Original: " . $original->formattedPrice() . "\n";
echo "Updated: " . $updated->formattedPrice() . "\n";
var_dump($original === $updated);
Output
Original: ₹20.00
Updated: ₹25.00
bool(false)
Why This Is Useful
- Code holding a reference to
$originalcan rely on it never changing — for example, an order that captured the product's price at purchase time. - Every "change" produces a separate object, so history and comparisons are easy.
DateTimeImmutable, PSR-7 HTTP messages ($request->withHeader(...)), and many value objects use this pattern.
Cloning in Newer PHP Versions
For classes with many properties, calling the constructor in every wither becomes repetitive. Newer PHP versions offer alternatives:
| Version | Feature |
|---|---|
| PHP 8.3 | Readonly properties may be re-initialized inside __clone(), enabling deep cloning |
| PHP 8.5 | "Clone with": clone($this, ['pricePaise' => $pricePaise]) creates a copy with changed properties |
// PHP 8.5+
public function withPrice(int $pricePaise): static
{
return clone($this, ['pricePaise' => $pricePaise]);
}
Note that "clone with" bypasses the constructor, so validation must be performed in the wither itself (or in a property hook).
When to Use Readonly Properties
| Good Fit | Poor Fit |
|---|---|
| Identifiers (IDs, SKUs, order numbers) | Values that legitimately change (stock levels, status) |
Value objects (Money, EmailAddress, Coordinates) | Objects that are built step by step |
| Data transfer objects and request/response objects | Entities whose state changes through their lifecycle |
| Configuration loaded once at startup | Caches and counters |
When all properties of a class are readonly, the whole class can be declared readonly (PHP 8.2+); see Readonly Classes.
Common Mistakes
Giving a Readonly Property a Default Value
Effect: Compile error.
Fix: Assign the value in the constructor (or use a class constant for fixed values).
Initializing From Outside the Class
Effect: Cannot initialize readonly property ... from global scope.
Fix: Initialize in the constructor or a named constructor.
Assuming Readonly Makes Contained Objects Immutable
Effect: Mutable objects (such as DateTime) inside readonly properties can still change.
Fix: Use immutable types for readonly properties.
Trying to Modify Readonly Arrays
Effect: Cannot modify readonly property.
Fix: Return a new object with a new array (wither pattern), or do not make the property readonly.
Best Practices
- Use
public readonlyfor data that must be readable but never changed. - Initialize all readonly properties in the constructor, after validation.
- Store immutable types (such as
DateTimeImmutable) in readonly properties. - Implement changes as
with...()methods that return new objects. - Reserve mutable properties for state that genuinely changes over an object's lifetime.
Official reference: https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties