Skip to content
article

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

Ayesha Siddiqui@laravel_ayesha

August 7, 2026 · 3 min read

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?->city instead 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, so sendInvoice($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

14

Sign in to join the conversation.

Sign in to comment
Mehmet Aydoğan@malloc_mehmet#8 in PHP Still Pays
Enum in 2021. C has enum since 1972. Congratulations PHP, you reached 1972. yazık
4 net votes
Ayesha Siddiqui@laravel_ayesha#1 in PHP Still Pays
C enums are ints with names, Mehmet. you can put 47 in a C enum variable and the compiler smiles at you. these carry methods and refuse invalid values. so we skipped 1972 and went somewhere better 😊
2 net votes
Mehmet Aydoğan@malloc_mehmet#8 in PHP Still Pays
In C you do not put 47 because you are engineer and you know what you do. Language should not babysit.
5 net votes
Elif Korkmaz@elif_ships2 rep in PHP Still Pays
hocam 'you know what you do' is how every CVE starts. please update your opinions past 1998, there is çay here 🙃
4 net votes
Frank Ostrowski@frank_cpp98#10 in PHP Still Pays
Readonly classes. Immutable value objects. C++ has had const since before most of your team was born. Regards.
4 net votes
Karthik Subramaniam@karthi_rustacean2 rep in PHP Still Pays
And C++ const can be cast away with const_cast simply. PHP readonly cannot be. For once the scripting language is the stricter one only.
6 net votes
Frank Ostrowski@frank_cpp98#10 in PHP Still Pays
Casting away const on an object defined const is undefined behaviour. Which is my point. You are not supposed to. Regards.
2 net votes
Guillaume Mercier@guillaume_rale#10 in PHP Still Pays
Bon. In Symfony we use enums with Doctrine since 8.1 and it removed half our status string bugs. The match exhaustiveness is the real gift. I am agree with everything except named arguments, which make renaming a parameter a breaking change. Bref.
1 net votes
Ayesha Siddiqui@laravel_ayesha#1 in PHP Still Pays
that is a fair point actually, it is why we only use named args for built-ins and bools, never across our own package boundaries
4 net votes
Bayu Setiawan@bayu_laravel#2 in PHP Still Pays
The canTransitionTo method is very nice mbak. We have loan approval flow in koperasi with 40 if-else, I will copy this pattern 🙏
4 net votes
Oluwatobi Adeyemi@tobi_freelance#6 in PHP Still Pays
Mehmet came to a PHP topic to talk about 1972 again 😂 my brother, nobody is forcing you to read
5 net votes
Mehmet Aydoğan@malloc_mehmet#8 in PHP Still Pays
I joined this topic to keep you honest.
1 net votes
Sam Whitaker@segfault_sam4 rep in PHP Still Pays
mehmet in a php thread is the most reliable service on this site. 100% uptime
8 net votes
Faizan Qureshi@faizan_php#4 in PHP Still Pays
Ayesha baji, Money class wala example humare school fees module mein laga diya. readonly ki wajah se ek bug pakda gaya jahan controller fee amount badal raha tha 😅 Mehmet sahab ke liye arz hai: enum mein jo likha hai janab, wahi chalega, C mein 47 daal ke, kaun sambhalega 🙂
7 net votes