Enums, readonly and match: the PHP 8 features that made my juniors' code reviewable
if your idea of PHP is from 2012, this post is for you. Frank, Mehmet, I can see you reading
Contents
Code review in my agency used to be 40% "what values can this string have?" and "who changed this amount after the controller?". PHP 8 killed most of those comments. Here are the features that did it, with the code we actually write now.
1. Backed enums (8.1)
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Pending => 'Waiting for payment',
self::Paid => 'Paid',
self::Shipped => 'On the way',
self::Cancelled => 'Cancelled',
};
}
public function canTransitionTo(self $next): bool
{
return in_array($next, match ($this) {
self::Pending => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped, self::Cancelled => [],
}, true);
}
}
In the Eloquent model the column is cast straight to the enum, so $order->status is an OrderStatus, never a random string:
protected function casts(): array
{
return ['status' => OrderStatus::class];
}
// from user input: null instead of an exception
$status = OrderStatus::tryFrom($request->input('status'));
The status flow lives in one place. No more if ($order->status == 'payed'), yes, with the typo, in production, for a year.
2. match (8.0)
switch compares with ==, falls through if you forget a break, and silently does nothing if no case matches. match uses ===, returns a value, has no fallthrough, and throws UnhandledMatchError when nothing matches. If you add a new enum case and forget to handle it, you find out in your tests, not from a client.
3. Readonly classes and constructor promotion (8.0, 8.1, 8.2)
final readonly class Money
{
public function __construct(
public int $amount, // minor units, always. paisa, not rupees
public string $currency,
) {}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException('currency mismatch');
}
return new self($this->amount + $other->amount, $this->currency);
}
}
$price = new Money(150000, 'PKR');
$price->amount = 1; // Error: Cannot modify readonly property Money::$amount
Value objects that cannot be changed after creation. A controller cannot "adjust" an amount by accident three layers deep. If you want a different amount, you make a new Money.
4. The small ones that add up
- Nullsafe operator:
$order?->customer?->address?->cityinstead of four nested ifs. - Named arguments:
str_pad($ref, 10, pad_string: '0', pad_type: STR_PAD_LEFT). We use them for built-ins and booleans, sosendInvoice($order, true, false)becomes readable. - First-class callables:
array_map(strtoupper(...), $codes). - 8.4 gave us property hooks and asymmetric visibility (
public private(set)). I am still deciding how I feel about hooks. Asymmetric visibility I love already.
5. And the tool that enforces it
composer require --dev larastan/larastan, start at level 5, go up one level per sprint. PHPStan reads the enum types and the readonly properties and catches the mistakes before review does.
If you prefer a video, even ThePrimeagen has one called "PHP 8.4 Is Good", and he is professionally obligated to hate everything:
Our code reviews got shorter because the types now say what the comments used to beg for. That's it. That's the post. Now let the comments begin 🐘
Written by
Ayesha Siddiqui
Laravel dev turned agency owner, Karachi. 11 people, mostly PHP, all of them better than me now. PHP 8 is good and I will die on this hill. Founder of PHP Still Pays 🐘
Get the best of #php_still_pays weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to comment