skip to content

In PHP, what does a nullable type such as ?Customer mean, and is it the same as Customer|null or an optional parameter?

level: juniorimportance: must knowfreq 62%

answer

  1. question mark before one type
  2. ?T and T|null are identical
  3. nullable is not optional
  4. return null; explicitly, not return;
  5. T $x = null deprecated in 8.4

basics

~20 s

?Customer accepts a Customer object or null, and is exactly the same type as Customer|null. Nullable does not mean optional: the argument must still be passed unless the parameter also has a default, written ?Customer $c = null.

solid answer

~40 s

A leading `?` (PHP 7.1) marks a **single** type as nullable: `?Customer` accepts a `Customer` or `null`, and the manual states that `?T` and `T|null` are identical. The `?` cannot prefix a union — `?int|string` is a syntax error, so you write `int|string|null`. **Nullable is not optional**: `function assign(?Customer $c)` still requires an argument, and calling it with none throws `ArgumentCountError`; `?Customer $c = null` is both. On a return type, `?Customer` requires an explicit `return null;` — a bare `return;` does not compile, and falling off the end throws `TypeError`. The old shortcut `Customer $c = null`, which made the type nullable implicitly, is deprecated since PHP 8.4.

code

php · 20 lines
php
<?php
declare(strict_types=1);

final class Customer
{
    public function __construct(public string $name) {}
}

function greet(?Customer $customer): string   // nullable, but required
{
    return 'Hello, ' . ($customer?->name ?? 'guest');
}

echo greet(null), PHP_EOL;                    // Hello, guest

try {
    echo greet();
} catch (ArgumentCountError $e) {
    echo 'missing argument', PHP_EOL;         // nullable is not optional
}

go deeper

for a junior

Recall that ?Customer accepts a Customer or null, that it equals Customer|null, and that you still must pass the argument unless there is a default.

for a middle

Explain why ? works on one type only, the explicit return null rule for nullable returns, and why PHP 8.4 deprecated the implicit T $x = null form.

for a senior

Decide where null is a legitimate result and where absence should throw, since every nullable return pushes a check onto every caller.

for a principal

Set a codebase convention for representing absence — nullable returns, exceptions or dedicated result objects — and keep it consistent across modules.

## What the question mark does In a PHP type declaration, a leading `?` means **"this type, or null"**. It was added in PHP 7.1, before PHP had general union types: ```php <?php declare(strict_types=1); function describe(?Customer $customer): string { return $customer === null ? 'guest' : $customer->name; } ``` `describe(new Customer(...))` and `describe(null)` both work; `describe('bob')` throws `TypeError`. ## ?T and T|null are the same type PHP 8.0 added union types, and with them a second spelling. The manual says it directly: `?T` and `T|null` are identical. Reflection, error messages and the engine treat them as one type. Which spelling to use is style: - `?Customer` is shorter and the common choice for one type plus null. - `int|string|null` is **required** when more than one non-null type is involved, because `?` applies only to a single type. `?int|string` does not parse. - For an intersection plus null, PHP 8.2+ needs the DNF form `(A&B)|null`; `?` cannot prefix an intersection either. Two spellings are rejected outright: `?mixed` (mixed already includes null) and `?null`. ## Nullable is not optional This is the part candidates most often blur. **Nullable** is about which values are accepted; **optional** is about whether the argument may be omitted. They are independent: | Declaration | `f(null)` | `f()` | |---|---|---| | `function f(?Customer $c)` | accepted | `ArgumentCountError` | | `function f(?Customer $c = null)` | accepted | accepted, `$c` is `null` | | `function f(Customer $c)` | `TypeError` | `ArgumentCountError` | | `function f(Customer $c = null)` | accepted (declaration deprecated since 8.4) | accepted, `$c` is `null` | The last row is the old implicit form: a `null` default silently widened the type to nullable. PHP 8.4 deprecated it because the declared type no longer says what the parameter accepts. The fix is to write the `?` explicitly. ## Nullable return types On a return type, `?Customer` means the function returns a `Customer` or `null` — but it must **say** which: 1. `return null;` is fine. 2. A bare `return;` is a compile error: `A function with return type must return a value (did you mean "return null;" instead of "return;"?)`. 3. Reaching the end of the body without any `return` throws `TypeError` at runtime (`Return value must be of type ?Customer, none returned`). PHP does not treat "no return" as "return null" for a declared return type. The rule exists to separate a deliberate "not found" from a forgotten return. ## When to use a nullable type - **Lookups that can miss**: `findCustomer(int $id): ?Customer` returns `null` for "no such customer". The caller must handle it, and the nullsafe operator `?->` or an explicit check makes that visible. - **Truly optional inputs**: `?string $note = null` for an argument that may be absent. - **Avoid it where absence is an error.** If a missing customer is a bug, return `Customer` and throw; a nullable return pushes a check onto every caller. ## Handling a nullable value Once a value may be `null`, the code that receives it has to deal with that before using it as an object: - an explicit check: `if ($customer === null) { ... }`; - the **nullsafe operator** (PHP 8.0): `$customer?->name` evaluates to `null` instead of throwing when `$customer` is `null`; - the **null coalescing operator**: `$customer?->name ?? 'guest'` supplies a fallback. Calling a method directly on a `null` value, `$customer->name()`, throws an `Error`. That is the practical cost of a nullable type: every consumer carries a branch, which is why it should mark genuine absence rather than serve as a convenient default. ## Common mistakes 1. Treating `?Customer $c` as optional and then calling the function without an argument. 2. Writing `?int|string` and getting a syntax error, instead of `int|string|null`. 3. Returning with a bare `return;` from a `?Customer` function, which does not compile. 4. Keeping `Customer $c = null` in new code, which raises a deprecation on PHP 8.4 and later. ## Quick reference - `?T` = `T|null`, since PHP 7.1; the union spelling since 8.0. - `?` works on one type only. - Nullable ≠ optional; add `= null` for optional. - `T $x = null` without `?` is deprecated since PHP 8.4. - A `?T` return needs an explicit `return null;`.

  • In PHP, how do you declare a parameter that accepts an int, a string or null?
    Write the union with `null` as a member: `int|string|null $key`. The `?` shorthand applies to one type only, so `?int|string` is a syntax error. If the argument may also be omitted, add a default: `int|string|null $key = null`.
  • In PHP, what happens when a function declared : ?Customer reaches the end of its body without a return statement?
    It throws `TypeError` saying the return value must be of type `?Customer` and none was returned. A declared return type is never satisfied by an implicit return, even when `null` is allowed. Write `return null;` for the not-found path; a bare `return;` would not compile.

saying these in an interview costs you the question

  • Says ?Customer makes the parameter optional, so it can be left out.
  • Thinks ?Customer and Customer|null behave differently at runtime.
  • Writes ?int|string for an int, a string or null.
  • Uses a bare return; to return null from a function declared ?Customer.
  • Recommends Customer $c = null as the normal way to declare a nullable parameter.