Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 003 / 100

PHP 8 features every developer should use for production

Language features that make PHP code safer and shorter.

Expona Admin6 min read

PHP 8 features every developer should use for production

PHP 8 arrived with a significant shift in language design: fewer characters, stronger types, and safer patterns that catch bugs before they hit your users. Over the past three years, these features have become standard in modern Laravel stacks and mature codebases handling millions of transactions daily. Learning them isn't optional anymore—they're the baseline for writing maintainable production code.

Constructor property promotion

Before PHP 8, declaring properties required boilerplate that cluttered your class definition. You'd declare the property, then accept it as a parameter, then assign it in the constructor body. For classes with five or six dependencies, this created noise that obscured intent.

Constructor property promotion collapses this into a single step. Visibility modifiers on constructor parameters automatically declare and assign properties:

php
final class Money {
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}
}

That final class Money now has two immutable properties without a single assignment statement. Compare this to the PHP 7 version:

php
final class Money {
    private int $amount;
    private string $currency;

    public function __construct(int $amount, string $currency) {
        $this->amount = $amount;
        $this->currency = $currency;
    }
}

In production systems handling payment data or inventory, immutability matters. The readonly modifier prevents accidental mutation after construction. Teams we've worked with report fewer bugs when value objects are truly immutable—properties can't drift into invalid states mid-request.

This pattern scales to service classes too. A repository or API client with three or four injected dependencies becomes cleaner:

php
final class OrderRepository {
    public function __construct(
        private DatabaseConnection $db,
        private Logger $logger,
        private Cache $cache,
    ) {}
}

The constructor signature itself documents dependencies. No hidden assignments to hunt through the method body.

Named arguments

Positional arguments work fine when you're calling a function once. But in large codebases, you might invoke the same function from twenty places with different argument orderings or optional parameters. Named arguments let you be explicit about which parameter you're passing:

php
// Old way: order matters, unclear at call site
sendEmail('user@example.com', 'Welcome', true, false);

// New way: self-documenting
sendEmail(
    to: 'user@example.com',
    subject: 'Welcome',
    isHtml: true,
    trackOpens: false,
);

function sendEmail(string $to, string $subject, bool $isHtml = false, bool $trackOpens = false): void {}

In payment processing or CRM systems where method signatures have evolved over years, named arguments save refactoring effort. You can add new optional parameters without breaking existing calls. Developers reading the code immediately understand what each argument means.

Named arguments also shine in fluent APIs and query builders. When you're chaining methods, clarity compounds:

php
$orders = Order::query()
    ->where(status: 'paid')
    ->whereBetween(createdAt: [$start, $end])
    ->orderBy(column: 'amount', direction: 'desc')
    ->get();

You're not guessing which parameter is which. The names anchor the logic to intent. In code reviews, ambiguity disappears.

Match expressions

The switch statement has been part of PHP forever, but it's verbose and error-prone. Fall-through bugs, missing breaks, and implicit type coercion have tripped up countless developers. PHP 8 introduced match, which is stricter and returns a value:

php
// Old switch way
$statusMessage = '';
switch ($order->status) {
    case 'pending':
        $statusMessage = 'Waiting for payment';
        break;
    case 'paid':
        $statusMessage = 'Processing shipment';
        break;
    case 'shipped':
        $statusMessage = 'In transit';
        break;
    default:
        $statusMessage = 'Unknown status';
}

// Modern match way
$statusMessage = match($order->status) {
    'pending' => 'Waiting for payment',
    'paid' => 'Processing shipment',
    'shipped' => 'In transit',
    default => 'Unknown status',
};

Match is an expression, not a statement. It returns directly to a variable or return statement. It uses strict comparison (===) by default, eliminating type coercion surprises. Every branch must be handled or a default case must exist, or an exception is thrown—no silent fallthrough.

In e-commerce systems, match makes payment status handling cleaner:

php
$action = match($paymentStatus) {
    'succeeded' => fn() => $order->markPaid(),
    'failed' => fn() => $order->notifyPaymentFailure(),
    'pending' => fn() => $order->queueForRetry(),
    'cancelled' => fn() => $order->cancel(),
};

You can even match against multiple conditions or return complex values, making state machines and workflow handlers far more readable.

Nullsafe operator

Null pointer exceptions (or in PHP's case, "trying to access property of null" warnings) are among the most common runtime errors in production. Before PHP 8, you had to chain checks or use null coalescing repeatedly:

php
// Old way: verbose null checks
$user = $order->getCustomer();
$city = null;
if ($user !== null && $user->getAddress() !== null) {
    $city = $user->getAddress()->getCity();
}

// With nullsafe operator
$city = $order->getCustomer()?->getAddress()?->getCity();

The ?-> operator short-circuits the chain if any step returns null, leaving the result as null. No exception, no warning, no defensive nesting. This is especially valuable in APIs where you're chaining through relationships you don't fully control:

php
// User might not have a profile, profile might not have a photo
$photoUrl = $user->profile?->photo?->url;

// Payment might not have a receipt
$receiptPdf = $payment->receipt?->generatePdf();

In ride-hailing or travel platforms where data structures are deeply nested and optional at multiple levels, the nullsafe operator reduces boilerplate by orders of magnitude. It also makes intent clearer—you're explicitly saying "this chain might be null somewhere," and that's fine.

Common mistakes

Overusing readonly. Promoted properties with readonly are great for value objects, but service classes shouldn't use them. A logger or cache object might need internal state changes. Only use readonly when the entire lifespan of the property should be immutable.

Mixing positional and named arguments carelessly. Once you use a named argument, all following arguments must be named too. This can break if you're not careful in large parameter lists. Stick to all positional or all named for clarity.

Assuming match exhaustively covers all cases. If you don't have a default case and the value doesn't match any branch, PHP throws an UnhandledMatchError. In production, this should be caught during testing, but it's a real exception, not a silent null.

Chaining nullsafe operators too deep. While $a?->b()?->c()?->d is valid, it can hide bugs. If you're going five levels deep through relationships, that's a sign your object graph is too tangled. Consider a method that encapsulates the navigation, or redesign your data model.

Key takeaways

  • Constructor property promotion with readonly eliminates boilerplate in value objects and makes immutability explicit.
  • Named arguments self-document call sites and reduce refactoring pain when function signatures evolve.
  • Match expressions are stricter and more readable than switch statements, with guaranteed exhaustiveness checking.
  • The nullsafe operator shortens defensive chains and makes intent clear without nested if checks.
  • These features aren't syntactic sugar—they reduce entire categories of bugs that have cost production teams real money.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles