Functions, control flow and type declarations

match, loops, typed parameters and returns, nullable and union types, named arguments, variadics and first-class callables.

Control flow worth using

<?php
declare(strict_types=1);

$label = match ($order->status) {
    "paid", "shipped" => "In progress",
    "cancelled"       => "Closed",
    default           => "Unknown",
};

foreach ($rows as $index => $row) {
    if ($row->isEmpty()) {
        continue;
    }
    if ($row->total() > 1000) {
        break;
    }
}
  • match is an expression, so it can be assigned. It compares with === and throws UnhandledMatchError when nothing matches and there is no default.
  • Unlike switch, match never falls through: each arm is one expression and needs no break.
  • break 2; and continue 2; exit or skip an outer level when loops are nested.
  • Iterating an empty array is safe; iterating null is not. Cast with (array) $maybeNull or reject the value earlier.

Typed functions

function total(array $lines, float $tax = 0.2): float
{
    $net = array_sum(array_map(fn($l) => $l->net(), $lines));
    return round($net * (1 + $tax), 2);
}

function find(int $id): ?User          // nullable return
{
    return $this->users[$id] ?? null;
}

function label(int|string $key): string   // union parameter
{
    return is_int($key) ? "#$key" : $key;
}

function sum(float ...$values): float     // variadic
{
    return array_sum($values);
}

$lengths = array_map(strlen(...), $names);   // first-class callable syntax
echo total(tax: 0.0, lines: $lines);         // named arguments, any order
DeclarationMeaning
int $xRequired integer; a wrong type is an error
?int $xInteger or null
int|string $xUnion of allowed types
: voidReturns nothing; the caller cannot use the result
: neverNever returns normally - it throws or exits
int ...$xVariadic: collects the remaining arguments into an array
⚠️
declare(strict_types=1) applies per file, to calls made from that file. Without it, "5" is quietly coerced to 5 and a typo becomes a value that half works. The declaration must be the very first statement in the file.

Worked example: validate and normalise

/**
 * @return array{email: string, age: int}
 * @throws InvalidArgumentException
 */
function parseSignup(array $input): array
{
    $email = trim((string) ($input["email"] ?? ""));
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
        throw new InvalidArgumentException("Invalid email");
    }

    $age = filter_var($input["age"] ?? null, FILTER_VALIDATE_INT);
    if ($age === false || $age < 13) {
        throw new InvalidArgumentException("Invalid age");
    }

    return ["email" => mb_strtolower($email), "age" => $age];
}

One function, one job: it returns a value the rest of the program can trust, or it throws with a reason. Every later layer can then treat the data as valid, which removes defensive checks from the whole codebase.

FAQ

Named arguments or positional?
Positional for one or two obvious parameters; named for booleans and for anything after the third argument, where a reader cannot guess the meaning from the call site.
Can default values contain expressions?
Only constant expressions: literals, constants and enum cases. When the default needs a function call, use ?int $x = null and assign inside the body.

Object-oriented PHP A modern PHP toolchain with Composer

Last refreshed 2026-09-18.