Enums and immutability remove whole categories of invalid-state bugs.
Enums (PHP 8.1) define a type with a fixed set of cases. A pure enum has only names; a backed enum maps each case to an int or string, which is what you store in a database or JSON:
enum Status: string {
case Draft = 'draft';
case Published = 'published';
}Status::from('draft') converts a value to a case and throws a ValueError for unknown values; Status::tryFrom('x') returns null instead. Status::cases() lists all cases, and $case->value and $case->name read them. Cases are singletons, so compare with ===. Enums can have methods, constants and implement interfaces, which pairs nicely with match. They cannot hold mutable state.
Readonly properties (8.1) can be assigned once, from inside the class, and never changed. A readonly class (8.2) makes every declared property readonly. Together with constructor promotion they make compact value objects:
final readonly class Money {
public function __construct(public int $cents, public string $currency) {}
}To "change" an immutable object, return a new one from a with...() method. Since PHP 8.3 readonly properties can be reinitialised inside __clone, which makes clone-based withers possible.
Readonly is shallow: a readonly property holding an object prevents replacing the object, not mutating it.
Example
<?php
enum Status: string {
case Draft = 'draft';
case Published = 'published';
public function label(): string {
return match ($this) {
Status::Draft => 'Not live yet',
Status::Published => 'Live',
};
}
}
final readonly class Money {
public function __construct(public int $cents, public string $currency) {}
public function add(Money $o): static { return new static($this->cents + $o->cents, $this->currency); }
}
$s = Status::from('published');
echo $s->name, ' / ', $s->label(), "\n"; // Published / Live
var_dump(Status::tryFrom('archived')); // NULL
$total = (new Money(500, 'EUR'))->add(new Money(250, 'EUR'));
echo $total->cents, "\n"; // 750
try { $total->cents = 1; } catch (Error $e) { echo $e->getMessage(), "\n"; }
// Cannot modify readonly property Money::$centsRemember
- Backed enums map cases to int or string; use from/tryFrom to parse
- Enums plus match give exhaustive, typo-proof branching
- readonly classes make concise immutable value objects
Watch out for
- Using Status::from() on untrusted input without handling ValueError; use tryFrom and validate.