php

PHP 8: the features I actually use

The PHP 8 features that changed my day-to-day code most, and the ones I still find less relevant in ordinary web applications.

Every language release gets two kinds of posts: the one that tours everything new, and the one that says which features actually changed how someone writes code after the excitement settled.

The second kind is more useful, so that is what this is. PHP 8 has been out long enough for me to have real opinions about which additions became part of my actual workflow and which ones are still mostly interesting in theory. I am writing from the context of Laravel-style production web applications — not library code, not CPU-intensive computation. Just the bread-and-butter backend development that takes up most of my time.

Named arguments

Named arguments changed my code the most, and I did not expect that. The value is not syntax novelty — it is call-site clarity.

In application code, especially around methods that take several options or constructors with multiple related parameters, named arguments make intent immediately visible without requiring the reader to count positions or navigate to the definition.

$service->buildReport(
    brand: 'Coleman',
    includeDrafts: false,
    locale: 'en_US',
    refreshCache: true
);

Before named arguments, buildReport('Coleman', false, 'en_US', true) was technically readable but required either trust in the method signature or a quick navigation to it. With named arguments, the call documents itself.

The improvement is felt in reading more than in writing. Code gets written once and read many times, so anything that makes the intent of a call clearer without adding verbosity earns its keep.

Match expressions

match felt like a small syntax improvement until I started using it in real branching code, and now I reach for it by default where switch used to go.

$statusLabel = match ($status) {
    'draft' => 'Draft',
    'published' => 'Published',
    'archived' => 'Archived',
    default => 'Unknown',
};

The practical advantages over switch are real: it is expression-oriented rather than statement-oriented, so it can appear inline in an assignment. It uses strict comparison by default, removing a category of bugs that switch’s loose comparison occasionally introduced. And it throws an UnhandledMatchError for unmatched values rather than silently falling through, which surfaces missing cases that switch would have hidden.

For status mapping, dispatch routing, and variant selection — patterns that appear constantly in web applications — match is consistently more expressive and less error-prone.

Constructor property promotion

Not dramatic, but it removed enough friction to be worth noting.

class BrandContext
{
    public function __construct(
        private string $brand,
        private string $locale,
        private bool $cacheEnabled = true,
    ) {}
}

Before promotion, this required declaring properties, listing constructor parameters, and assigning them individually — three separate things to keep in sync for a pattern that appears constantly. Constructor property promotion collapses that repetition to one place. The object is still the same thing; the code just stops requiring you to say it three times.

The impact is cleaner value objects, cleaner DTOs, and less visual noise in classes that have no logic beyond carrying data. Not transformative. A persistent daily quality-of-life improvement.

Nullsafe operator

One of those features that felt obvious in retrospect — the kind where you wonder how you lived without it once you start using it.

$country = $customer?->address?->country;

That replaces a pattern I had been writing for years: a nested conditional chain checking each object in the access path before dereferencing it. The result was technically correct but verbose, and the verbosity obscured what was actually simple intent — “give me the country if it exists, null otherwise.”

The nullsafe operator expresses that intent directly. The uncertainty is visible in the ?-> syntax rather than buried in guard clauses. Domain code that traverses optional relationships reads more directly.

Union types

Union types were helpful primarily because they let me make existing assumptions explicit in the type system.

If a method genuinely accepts either a string identifier or an integer ID because the codebase has historical inconsistency, or if a method returns either an object or null, saying that explicitly is better than documenting it or trusting discipline.

public function findBrand(string|int $identifier): Brand
{
    // ...
}

Not a feature I use everywhere. It fits specific cases: legacy call sites with mixed types, methods that legitimately need to accept multiple shapes, return types that are genuinely nullable but currently implicit about it. In those cases, making the union type explicit reduces the ambiguity that leads to defensive code sprinkled through callers.

The broader theme across the PHP 8 features I actually value: they help code say what it already means more directly. Named arguments, match, nullsafe, union types — all clarity improvements rather than capability additions.

JIT

JIT got the most attention in the PHP 8 announcement and has affected my day-to-day work the least.

That is not a criticism of JIT. It is a reflection of what most web application performance problems actually are. CPU-bound PHP execution is not typically where production web systems slow down. Database access patterns, cache strategy, network call overhead, and application architecture are where performance work has the most impact in the systems I work on. Raw PHP execution speed, improved by JIT, is not usually the binding constraint.

JIT matters for CPU-intensive workloads — machine learning in PHP, mathematical simulations, image processing, anything that does a lot of computation in pure PHP. For request-response web applications where most time is spent waiting for database or external service calls, the improvement is present but not the thing I optimize for.

What actually changed

The PHP 8 features that changed my code are all small improvements to expressiveness. Named arguments, match expressions, constructor property promotion, nullsafe operator, union types — none of those are architectural. None of them required rethinking how applications are structured.

That is probably why I trust them. They make the ordinary production code that I and my team read and modify every day easier to understand, which is where most of the value in day-to-day language improvements actually lives. The features that reduce friction in common cases earn compounding returns because those common cases happen constantly.