Kodokon kodokon.com

Modern PHP: strict types, enums, match

Adopt today's PHP: strict types, arrow functions, the nullsafe operator, enums, and the match expression.

9 min · 3 questions

Open this lesson in Kodokon

Modern PHP embraces typing. declare(strict_types=1), placed as the first statement of the file, disables implicit conversions: passing the string '10' to a float parameter throws a TypeError instead of being silently converted. Type parameters and return values: your errors surface early, in the right place, rather than three files later.

PHP
<?php

declare(strict_types=1);

function applyDiscount(
    float $price,
    float $rate = 0.1
): float {
    return round($price * (1 - $rate), 2);
}

echo applyDiscount(49.99);
echo PHP_EOL;
echo applyDiscount(100.0, 0.25);
A typed signature: the function's contract is explicit.

Arrow functions fn condense anonymous functions of a single expression. A major advantage over classic closures: they automatically capture the variables of the parent scope, without a use clause. They shine with array_map() and array_filter().

PHP
<?php

declare(strict_types=1);

$prices = [12.5, 49.9, 8.0, 120.0];
$rate = 0.2;

$discounted = array_map(
    fn (float $p): float => round($p * (1 - $rate), 2),
    $prices
);

$affordable = array_filter(
    $discounted,
    fn (float $p): bool => $p < 50
);

print_r($affordable);
The rate variable is captured without a use clause.

The nullsafe operator ?-> short-circuits a call as soon as a link in the chain is null, instead of causing a fatal error. Combined with ??, it replaces pyramids of if statements with a single readable line.

PHP
<?php

declare(strict_types=1);

class Address
{
    public function __construct(
        public readonly string $city
    ) {
    }
}

class User
{
    public function __construct(
        public readonly ?Address $address = null
    ) {
    }
}

$alice = new User(new Address('Lyon'));
$bob = new User();

echo $alice->address?->city ?? 'Unknown city';
echo PHP_EOL;
echo $bob->address?->city ?? 'Unknown city';
No error for Bob: the chain stops at the first null.

Enums give a real type to closed sets of values (statuses, roles): it becomes impossible to create a nonexistent status. And match is a clear improvement over switch: strict comparison, a result returned as a value, and an UnhandledMatchError if no case matches - a safeguard against oversights.

PHP
<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';

    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Pending',
            self::Shipped => 'Shipped',
            self::Delivered => 'Delivered',
        };
    }
}

$status = OrderStatus::from('shipped');
echo $status->label();
An enum backed by strings: ideal for a database.

Knowledge check

Make sure you remember the key points of this lesson.

  1. With declare(strict_types=1), what happens if you pass the string '10' to a float-typed parameter?
    • PHP silently converts the string to 10.0
    • PHP emits a mere warning and continues
    • PHP throws a TypeError
  2. What distinguishes an arrow function fn() from a classic closure function()?
    • It can contain several statements
    • It automatically captures the variables of the parent scope, without a use clause
    • It runs faster
    • It cannot declare types
  3. What advantage does the match expression have over switch?
    • It compares strictly (===), returns a value, and throws an error if no case matches
    • It accepts loose comparisons (==) for more flexibility
    • It allows fall-through from one case to the next