PHP strict types, match, and the nullsafe operator
Use union types, a match expression, constructor promotion, and ?-> to write safer code.
On this page
PHP functions and classes.
- Use union types, a match expression, constructor promotion, and ?-> to write safer code.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
<?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";
}int 5 negative int string 'hi' nothing Ada: London Bo: unknown TypeError caught
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
<?phpdeclare(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";}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
| Step / line | What happens | State after step | Output 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 |
Behind the scenes
How PHP processes this code
The PHP runtime executes the script. echo writes to the response or terminal output.
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.