Immutability
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches' (d5), phpstan/psalm can flag mutation of readonly properties and detect public setters on value objects, but identifying conceptually-mutable-but-not-marked code requires the specialist tooling configured for it.
Closest to 'touches multiple files / significant refactor in one component' (e5), the quick_fix suggests adding readonly or private setters returning new instances — but converting an existing mutable value object to immutable typically ripples through call sites that mutated it, requiring 'with*' methods and updated consumers.
Closest to 'persistent productivity tax' (b5), immutability decisions apply across web/cli/queue contexts and shape how every value object and DTO is constructed and passed; affects many work streams but isn't quite system-defining (b7).
Closest to 'notable trap' (t5), the misconception that readonly properties make the whole object immutable (when they only freeze the reference, not nested mutable state) is a documented gotcha most devs eventually learn — listed explicitly in common_mistakes.
Also Known As
TL;DR
Explanation
Immutable objects prevent a class of bugs caused by shared mutable state: race conditions, unexpected side effects, and defensive copying. In PHP, immutability is implemented by returning new instances from mutating operations (like DateTimeImmutable), using readonly properties (PHP 8.1+), or by convention (no setters). Value objects are natural candidates for immutability. Immutability trades memory (new instances vs. mutation) for safety — an excellent trade in most PHP web request contexts.
Common Misconception
Why It Matters
Common Mistakes
- Declaring a property readonly but storing a mutable object in it — the reference is immutable, not the object's internal state.
- Creating "immutable" objects with public setters — naming something immutable does not make it so.
- Avoiding immutability in performance-critical code without profiling — modern PHP's copy-on-write makes immutability cheaper than it looks.
- Making every object immutable regardless of context — value objects benefit most; entities that track change over time need mutability.
Code Examples
class DateRange {
public \DateTime $start;
public \DateTime $end;
}
$range->start = new \DateTime('tomorrow'); // surprise mutation
// PHP 8.2 readonly class — all properties are readonly
readonly class DateRange {
public function __construct(
public \DateTimeImmutable $start,
public \DateTimeImmutable $end,
) {
if ($end <= $start) throw new \InvalidArgumentException('end must be after start');
}
public function withEnd(\DateTimeImmutable $end): self {
return new self($this->start, $end); // return new, don't mutate
}
}