01 · THE IDEAREAD → TRY → LEARN
Why immutable objects?
A value object represents a small piece of data, such as a point, a price or an address. Two things make such objects pleasant to work with: they are always complete after construction, and they never change afterwards. If a function receives one, it cannot silently alter the caller's data.
PHP lets you write this directly. Since PHP 8.2 a class can carry the readonly modifier. The manual describes the effect: every declared property becomes readonly, and the creation of dynamic properties is prevented. You no longer repeat readonly on each property.
A first readonly class
Combined with constructor property promotion, a readonly class is very compact. The constructor assigns each property exactly once, and after that the object is frozen.
readonly class Point
{
public function __construct(
public int $x,
public int $y,
) {}
}
$p = new Point(3, 4);
echo json_encode(['x' => $p->x, 'y' => $p->y]), "\n";{"x":3,"y":4}
Reading the properties works as usual because they are public. Only writing is restricted.
What happens on a write
Assigning to a readonly property after initialisation throws an Error. You can catch it to see the message, although in real code the right fix is to avoid the write.
readonly class Point
{
public function __construct(
public int $x,
public int $y,
) {}
}
$p = new Point(3, 4);
try {
$p->x = 10;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}Cannot modify readonly property Point::$x
The same protection applies to array properties. Writing $cart->items[] = 'x' on a readonly array property is also a modification of the property, so it fails too.
Rules to remember
- Every property in a readonly class must have a type. An untyped property is a compile-time error.
- Static properties are not allowed in a readonly class.
- You cannot re-enable dynamic properties with
#[\AllowDynamicProperties]; that combination is a compile-time error. - A readonly class can only be extended by another readonly class.
- Readonly is shallow. If a property holds an object, the property cannot be reassigned, but that inner object may still change its own state if it is mutable. Arrays are values, so they are safe here.
Changing data without mutation
If an object cannot change, how do you represent an updated value? You build a new object. A method that returns a modified copy is commonly called a "wither". It reads the current properties, combines them with the new input and calls the constructor again.
readonly class Cart
{
public function __construct(public array $items) {}
public function with(string $item): static
{
return new static([...$this->items, $item]);
}
}
$a = new Cart(['pen']);
$b = $a->with('book');
echo json_encode([$a->items, $b->items]), "\n";[["pen"],["pen","book"]]
The first cart still holds one item, while the second holds two. Using new static keeps the result the same class as the receiver, which matters if a readonly subclass inherits the method.
Worked reasoning for the exercise
The exercise asks for a function that builds an invoice object from a customer name and a list of integer prices, adds one more price through a wither, and reports both totals. The steps are:
- Define a readonly
Invoiceclass withcustomerandprices. - Add a
total()method that sums the prices witharray_sum(). - Add a
withPrice()method that returns a newInvoicewith the extra price appended. - In your function, create the original, derive the updated invoice, and return an associative array.
The key check is that the original total stays unchanged after the wither is called. If your code reports the same total for both, you have probably built the answer from the wrong object.
Exercise contract
Implement addPriceToInvoice(string $customer, array $prices, int $newPrice): array. Return an array with these keys, in this order: customer, original_total, new_total and new_count. The count is the number of prices in the updated invoice. Prices may be zero or negative, and the list may be empty.
Recap
readonly classmakes all declared properties write-once and blocks dynamic properties.- Properties need types, and static properties are not allowed.
- Modification attempts throw
Error. - Produce changed versions by returning new objects.
- Readonly is shallow for nested objects, but arrays held in properties cannot be altered in place.
Functions · Arrays · Types
- Construct readonly objects with scalar and array data
- Implement and test a deterministic PHP 8.4 function