DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
World desk6 min

The Readonly Trap: PHP Value Objects and DDD Aggregates

PHP readonly prevents certain property reassignment, but it does not guarantee deep immutability or define a DDD aggregate. Learn how to choose value and entity semantics.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PHP’s readonly feature can prevent a property from being reassigned, but it does not make an object deeply immutable or turn it into a sound domain-driven design (DDD) aggregate. Use it to protect values that should not change; use domain behavior and a carefully chosen aggregate boundary to protect business invariants.

What does readonly mean in PHP?

A PHP readonly property can be initialized once. After initialization, assigning to it again fails—even if the new value is identical. The property must have a declared type, cannot have an explicit default value, and must be initialized directly rather than through a reference. Readonly arrays cannot be changed by editing an offset or indirectly modifying the property.

As an Amazon Associate I earn from qualifying purchases.

Here is a small value type that uses readonly properties:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php

final class Money
{
    public function __construct(
        public readonly int $minorUnits,
        public readonly string $currency,
    ) {}

    public function equals(self $other): bool
    {
        return $this->minorUnits === $other->minorUnits
            && $this->currency === $other->currency;
    }
}

The example uses integer minor units—such as cents—to avoid representing a money amount with a binary floating-point number. The equals() method makes the intended comparison explicit: for this type, both the amount and currency define the value.

Readonly behavior has changed across PHP versions:

PHP version Relevant behavior
8.1 Readonly properties were introduced. They can be initialized once and cannot be reassigned afterward.
8.2 Readonly classes were introduced. All instance properties are readonly, and dynamic properties are disallowed.
8.3 A __clone() method may reinitialize readonly properties on the cloned object.
8.4 A readonly property’s default set visibility became protected(set), rather than the earlier private(set). Explicit visibility choices still matter.

A readonly class must use typed instance properties, cannot declare static properties, and can only extend a readonly parent. A non-readonly child cannot extend a readonly class. These are language rules, not guarantees about the correctness of a domain model.

Are PHP readonly objects immutable?

Not necessarily. Readonly is shallow: it prevents replacing the value held by a property, but it does not freeze an object referenced by that property. The reference is fixed; the referenced object’s internals may not be.

<?php

final readonly class Event
{
    public function __construct(public DateTime $createdAt) {}
}

$event = new Event(new DateTime('2026-10-09'));
$event->createdAt->modify('+1 day'); // The DateTime object can still change.

The property still refers to the same DateTime instance, so the assignment restriction is not broken. But the date inside that instance has changed. If a value object needs stable nested state, use immutable nested objects or otherwise control how they can be changed. For example, PHP’s DateTimeImmutable returns a new date object when modified instead of changing its existing instance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Readonly therefore helps prevent certain state changes; it is not deep or transitive immutability. That distinction matters whenever callers can share references to objects held inside a readonly object.

What is the difference between a value object and an entity?

The key distinction is how the domain recognizes the object. A value object is defined by its attributes: two instances with the same relevant values can be interchangeable. An entity is recognized by its identity and lifecycle, even as its attributes change. Martin Fowler uses points with matching x and y coordinates as a value-object example, in contrast to objects identified by a unique identifier.

Question Value object Entity
What makes it the same thing? The relevant attribute values A meaningful identity, such as an order number
What does a change mean? Usually a different value; create a replacement A state transition in the same object’s lifecycle
How should equality work? Compare the domain-defining values Compare identity, not every current attribute
Should separate references share a changing instance? Usually not; immutable values help prevent aliasing surprises Possibly, if the model’s lifecycle and consistency rules call for it

A useful test is: if two instances contain the same domain value, should the business treat them as interchangeable? If yes, value semantics may fit. If identity, history, or lifecycle matters, entity semantics may be more appropriate.

Examples of candidate value objects include money, points, ranges, and a validated telephone number. A domain type can make intent clearer and prevent irrelevant operations, but that does not mean every primitive must become a class. Nor does immutability alone make an object a value object: a sales order may be unchanged during a read operation and still be an entity because its order number and lifecycle define it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fowler recommends immutable value objects in part because immutability avoids aliasing bugs: one caller changing a shared object can unexpectedly affect another. In PHP, readonly can help implement that choice, as long as nested state is also safe and equality is defined in terms of the domain’s values.

Should DDD value objects be readonly?

Often, but not because DDD imposes a language-level rule. Readonly is a good fit when changing a value after construction would be a domain error. A changed amount, currency, date range, or validated identifier can then be represented by constructing a new value object rather than mutating the old one.

Choose value semantics first, then use readonly where it supports those semantics. Decide which fields define equality, validate the value when constructing it, and make sure any objects nested inside it cannot undermine the intended stability. PHP does not automatically give ordinary objects value-based equality; the example’s equals() method illustrates one explicit approach.

Do not choose readonly merely to make a class appear safer. If the object’s identity persists while its state changes, it may be an entity instead. Its design should explain which changes are allowed and how those changes preserve the rules of the domain.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Can an aggregate root be readonly?

It can be readonly when it represents a snapshot or read-only representation. But readonly syntax does not create an aggregate boundary or enforce the invariants that make a live aggregate coherent.

In DDD, an aggregate root is the controlled entry point for operations that must preserve rules across the entities and values in an aggregate. Microsoft Learn’s DDD-oriented guidance emphasizes the root’s role in performing rules and invariants for that group. The root’s job is about which operations are allowed and what they guarantee—not simply whether properties can be reassigned.

A live aggregate can be mutable and still be well designed when its state changes go through behavior that preserves its invariants. Conversely, an aggregate whose properties are readonly does not automatically have invariant-preserving operations. An aggregate can also contain immutable value objects while its root remains behaviorally mutable.

For example, an order might expose an operation to add a line only when the order is open, and ensure that the resulting total is consistent with its lines. Making the order’s properties readonly would not supply that business rule. The important design decision is that callers use the root’s operation rather than changing aggregate members in ways that bypass the rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How should you choose between readonly values and mutable aggregates?

Start with the domain’s meaning and consistency boundary, not a blanket rule that every domain object should be readonly. Use these questions to guide the design:

  • Identity: Is the object recognized by its values, or by a stable identity and lifecycle?
  • Equality: Should equal attribute values make two instances interchangeable, or does identity decide whether they are the same?
  • Change: Does a change create a new value, or is it a transition in an entity’s ongoing lifecycle?
  • Invariants: Do rules span multiple members that need one controlled entry point for change?
  • Nested state: Can an object stored in a readonly property still be mutated through another reference?
  • Representation: Is this a snapshot or read model, or the live consistency boundary for business operations?
  • Runtime and persistence: Which PHP version is deployed, and does the persistence approach support the chosen construction and hydration model? Check the documentation for the specific ORM and version before relying on compatibility; readonly syntax alone does not establish it.

Use readonly when it reinforces an intentionally stable value. Use an aggregate root when coordinated behavior must protect business rules. A well-chosen domain model may need both: immutable value objects inside an aggregate whose root controls meaningful state transitions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.