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;
}
}matchis an expression, so it can be assigned. It compares with===and throwsUnhandledMatchErrorwhen nothing matches and there is nodefault.- Unlike
switch,matchnever falls through: each arm is one expression and needs nobreak. break 2;andcontinue 2;exit or skip an outer level when loops are nested.- Iterating an empty array is safe; iterating
nullis not. Cast with(array) $maybeNullor 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| Declaration | Meaning |
|---|---|
int $x | Required integer; a wrong type is an error |
?int $x | Integer or null |
int|string $x | Union of allowed types |
: void | Returns nothing; the caller cannot use the result |
: never | Never returns normally - it throws or exits |
int ...$x | Variadic: 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.Related
Object-oriented PHP A modern PHP toolchain with Composer
Last refreshed 2026-09-18.