Lesson 75 of 100
Classes15 min5 checksPHP 8.5

Create Readonly Value Objects with Readonly Classes

Learn how PHP 8.4 readonly classes make every property write-once, and how to build small immutable value objects that return updated copies.

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.

Example
<?php
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";
Output
{"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.

Example
<?php
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";
}
Output
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.

Example
<?php
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";
Output
[["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:

  1. Define a readonly Invoice class with customer and prices.
  2. Add a total() method that sums the prices with array_sum().
  3. Add a withPrice() method that returns a new Invoice with the extra price appended.
  4. 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 class makes 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.
Before you start

Functions · Arrays · Types

You’ll practice
  • Construct readonly objects with scalar and array data
  • Implement and test a deterministic PHP 8.4 function
Your task

Make addPriceToInvoice() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.