PHP strict types, match, and the nullsafe operator

Use union types, a match expression, constructor promotion, and ?-> to write safer code.

PHP
<?php
declare(strict_types=1);

function describe(int|string|null $value): string
{
    return match (true) {
        $value === null => "nothing",
        is_int($value) && $value < 0 => "negative int",
        is_int($value) => "int $value",
        default => "string '$value'",
    };
}

echo describe(5), "\n", describe(-2), "\n", describe("hi"), "\n", describe(null), "\n";

class Address
{
    public function __construct(public ?string $city = null) {}
}

class User
{
    public function __construct(public string $name, public ?Address $address = null) {}
}

$users = [new User("Ada", new Address("London")), new User("Bo")];
foreach ($users as $user) {
    echo $user->name, ": ", $user->address?->city ?? "unknown", "\n";
}

try {
    echo strlen(123);
} catch (TypeError $e) {
    echo "TypeError caught\n";
}
Output
int 5
negative int
string 'hi'
nothing
Ada: London
Bo: unknown
TypeError caught

Explanation

declare(strict_types=1) tells PHP not to convert argument types silently. With it, passing an int where a string is required is a TypeError instead of a hidden conversion. Union types such as int|string|null list the types a parameter accepts. The match expression compares with strict equality, returns a value, and throws if nothing matches, unlike switch.

Constructor promotion declares and assigns properties directly in the constructor parameter list. The nullsafe operator ?-> stops and gives null if the object on its left is null, and ?? supplies a default. Together they handle optional data compactly.

Explore the dry run
Guided walkthroughReady
1<?php
2declare(strict_types=1);
3
4function describe(int|string|null $value): string
5{
6 return match (true) {
7 $value === null => "nothing",
8 is_int($value) && $value < 0 => "negative int",
9 is_int($value) => "int $value",
10 default => "string '$value'",
11 };
12}
13
14echo describe(5), "\n", describe(-2), "\n", describe("hi"), "\n", describe(null), "\n";
15
16class Address
17{
18 public function __construct(public ?string $city = null) {}
19}
20
21class User
22{
23 public function __construct(public string $name, public ?Address $address = null) {}
24}
25
26$users = [new User("Ada", new Address("London")), new User("Bo")];
27foreach ($users as $user) {
28 echo $user->name, ": ", $user->address?->city ?? "unknown", "\n";
29}
30
31try {
32 echo strlen(123);
33} catch (TypeError $e) {
34 echo "TypeError caught\n";
35}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
From here on, scalar arguments are not converted silently.—
—
A union type: the function accepts an int, a string, or null.—
—
match (true) tests each condition in order and returns the first arm that matches.—
—
Each call is routed to the right arm.—
int 5
negative int
string 'hi'
nothing
Constructor promotion: the parameters become properties automatically.—
int 5
negative int
string 'hi'
nothing
For Bo, address is null, so ?-> gives null and ?? supplies the default.—
int 5
negative int
string 'hi'
nothing
Ada: London
Bo: unknown
With strict types, passing an int to strlen is a TypeError instead of an automatic conversion.—
int 5
negative int
string 'hi'
nothing
Ada: London
Bo: unknown
The error is a normal exception that can be caught.—
int 5
negative int
string 'hi'
nothing
Ada: London
Bo: unknown
TypeError caught

Cost and common mistakes

Each call is O(1).

  • match throws UnhandledMatchError if no arm matches and there is no default. Always add default unless the cases are exhaustive.
  • strict_types applies to calls made from the file that declares it. It has to be the first statement in the file.
  • Without strict types, "5 apples" passed to an int parameter is converted with a warning or error depending on the version. Strict types avoid such surprises.

Try it yourself

How does match differ from switch?

Reveal answer and reasoning

match uses strict comparison, returns a value, needs no break statements, and throws an error when no arm matches.

Further reading