Modern versions of PHP have introduced tools that profoundly change the way we design and write code. Among these, readonly properties, enums, and the match expression stand as three fundamental pillars for more expressive, safer, and maintainable code. This guide explores each feature in detail, providing real examples, advanced use cases, and strategies for combining them into robust applications.
Readonly Properties: Declarative Immutability
Introduced in PHP 8.1, readonly properties allow you to declare that a class property can be assigned only once, typically in the constructor. After initialization, any attempt to modify the property throws an Error exception. This mechanism enforces immutability at the individual property level, eliminating the need for manual getters or write-once logic.
Syntax and Behavior
class User {
public readonly string $name;
public readonly int $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
}
$user = new User('Alice', 30);
// $user->name = 'Bob'; // Error: Cannot modify readonly property User::$name
Readonly properties can be typed (including union and intersection types) and can be constructor-promoted, making code even more concise:
class Product {
public function __construct(
public readonly string $sku,
public readonly float $price,
) {}
}
It is not allowed to declare a readonly property without a type, nor to use static or abstract. Also, default values must be constants; the property must be initialized in the constructor or via a constructor call.
Benefits and Use Cases
Readonly properties are ideal for Value Objects, DTOs (Data Transfer Objects), immutable configurations, and any entity that should not change after creation. They remove the need for getters (if no further encapsulation is needed) and prevent accidental modifications. When combined with enums and the match expression, they enable declarative, predictable code.
Enums: A Discrete and Safe Data Type
Enums were introduced in PHP 8.1 as a true data type. They allow you to define a finite set of possible values, improving readability and safety compared to using constants or arbitrary integers.
Pure Enums and Backed Enums
A pure enum has no associated value:
enum Status {
case Pending;
case Approved;
case Rejected;
}
A backed enum associates each case with a scalar value (int or string):
enum OrderStatus: string {
case New = 'new';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
Enums can implement interfaces, have their own methods (including static ones), and support pattern matching via match expressions.
Methods and Interfaces in Enums
enum Color: int {
case Red = 1;
case Green = 2;
case Blue = 3;
public function hexCode(): string {
return match($this) {
Color::Red => '#FF0000',
Color::Green => '#00FF00',
Color::Blue => '#0000FF',
};
}
}
This integration makes enums far more powerful than simple constants: each case is an object with behavior.
Match Expression: An Improved, Expressive Switch
Introduced in PHP 8.0, the match expression combines the capabilities of switch with the safety of strict comparison (===) and the ability to return a value. Unlike switch, match is an expression that can be assigned.
Basic Syntax
$result = match($value) {
1 => 'One',
2, 3 => 'Two or three',
default => 'Other',
};
Each arm is separated by a comma. No break is needed; the first matching arm executes and its expression is returned. If no arm matches and there is no default, an UnhandledMatchError exception is thrown.
Advantages Over Switch
- Strict comparison (===) instead of ==, preventing type coercion bugs.
- Returns a value, enabling more concise, functional code.
- Supports multiple pattern arms (e.g.,
2, 3 => ...). - No fall-through: each arm is independent.
The match expression is especially powerful when combined with enums and readonly properties, creating clear and immutable control flows.
Combining All Three Features: Practical Examples
Immutable DTO with Enum and Match
enum OrderAction: string {
case Create = 'create';
case Update = 'update';
case Delete = 'delete';
}
readonly class OrderDTO {
public function __construct(
public string $orderId,
public OrderAction $action,
public array $data = [],
) {}
}
function processOrder(OrderDTO $dto): string {
return match($dto->action) {
OrderAction::Create => createOrder($dto->data),
OrderAction::Update => updateOrder($dto->orderId, $dto->data),
OrderAction::Delete => deleteOrder($dto->orderId),
};
}
Here, OrderDTO is immutable thanks to readonly properties; the action is represented by an enum, and the routing logic is handled by the match expression. The result is error-proof, easily extendable code.
Simple State Machine
enum PaymentStatus: string {
case Pending = 'pending';
case Completed = 'completed';
case Failed = 'failed';
case Refunded = 'refunded';
public function canTransitionTo(self $newStatus): bool {
return match($this) {
self::Pending => in_array($newStatus, [self::Completed, self::Failed], true),
self::Completed => $newStatus === self::Refunded,
self::Failed, self::Refunded => false,
};
}
}
State transitions are expressed with match, and the immutability of readonly properties ensures that the state is not altered except through a controlled process.
Best Practices and Caveats
Readonly properties should not be used in classes requiring lazy loading or frequent reassignments. They shine for DTOs, configurations, and value objects.
Enums do not support inheritance (you cannot extend an enum). Use interfaces to share common behavior.
Prefer match over switch for operations involving discrete values (enums, predictable strings). For complex boolean conditions or ranges, if/elseif blocks may be more readable.
Finally, combining readonly, enums, and match leads to declarative and functional style, reducing side effects and improving testability.
Conclusion and Next Steps
Readonly properties, enums, and the match expression are not just syntactic additions; they represent a paradigm shift toward safer, more expressive programming. Integrating them into your workflow yields code that is easier to reason about, maintain, and test.
For further reading, check the official PHP documentation on readonly properties and the enum guide. To compare with the latest PHP 8.3 and 8.4 features, read our dedicated article: PHP 8.3 and 8.4 New Features: Typed Class Constants, Property Hooks, and json_validate.
Sponsored Protocol