01 · THE IDEAREAD → TRY → LEARN
Why null needs a contract
Many functions have to say "there is no value here". A user may not have a middle name, a stock count may not have been loaded yet, or a lookup may find nothing. PHP represents that state with null. A type declaration lets you state in the function signature that null is allowed, so callers do not have to guess.
Without a declaration, a type such as int or string rejects null for a parameter or a return value, and PHP throws a TypeError. Declaring a type nullable is an explicit choice: "this function can receive or produce no value, and callers must handle that case".
Writing nullable types
Prefix a single type with a question mark: ?string means a string or null. The manual describes ?T and T|null as identical, so use whichever reads better. The question-mark form is common for one base type, and the union form is needed when you combine several types, for example int|string|null.
Nullability applies to both sides of a function. A parameter can be ?string, and a return type can be ?string. The first example accepts a nullable argument and returns a nullable result.
function middleInitial(?string $middle): ?string
{
if ($middle === null || $middle === '') {
return null;
}
return strtoupper($middle[0]) . '.';
}
echo json_encode([middleInitial('grace'), middleInitial(''), middleInitial(null)]);["G.",null,null]
Here the function deliberately maps both null and an empty string to null. That is a decision about the contract, and it should be written down rather than happening by accident.
Absent is not the same as empty
The biggest mistake with nullable values is using a truthiness check. 0, 0.0, '', '0' and [] are all falsy, yet each one is a real value. If a stock count is 0, the item is known to be out of stock. If the count is null, it is unknown. Those are different facts.
To test for absence, compare strictly with === null or !== null. Avoid if (!$value) unless you truly want every falsy value to take the same branch.
function badLabel(?int $n): string
{
return $n ? "has $n" : 'missing';
}
function goodLabel(?int $n): string
{
return $n === null ? 'missing' : "has $n";
}
echo json_encode([badLabel(0), goodLabel(0), goodLabel(null)]);["missing","has 0","missing"]
The faulty version reports 0 as missing. The strict version keeps zero as data.
Optional is not the same as nullable
A parameter with a default value is optional: the caller may leave it out. A nullable parameter accepts null. These are separate ideas, and you can combine them. Write the nullable type explicitly when the default is null:
function greet(?string $name = null): string
{
return $name === null ? 'Hello, guest' : 'Hello, ' . $name;
}
echo json_encode([greet(), greet(''), greet('Ada')]);["Hello, guest","Hello, ","Hello, Ada"]
The manual notes that making a parameter nullable only through a null default, such as C $c = null, is deprecated as of PHP 8.4. Write ?C $c = null instead. In the example, an empty string is still a name, so greet('') does not become the guest greeting.
Designing a good contract
- Decide what
nullmeans: unknown, not found, not provided, or intentionally cleared. - Decide whether empty values are also treated as absent. If not, never use truthiness.
- Check
nullfirst, then handle real values in order of specificity. - Keep the return type as narrow as possible. Return
?stringonly when callers truly need to distinguish absence. - Test the boundaries:
null, zero or empty, the smallest normal value, an ordinary value, and an invalid one.
Also remember that type coercion in non-strict files can change scalar values before your function body runs. Passing JSON-style values such as null or an integer to ?int is straightforward, but you should still write your own checks for domain rules, like rejecting negative counts.
Exercise contract
Implement describeStock(?int $count): string:
nullreturnsunknown.- A negative number returns
invalid. 0returnsout of stock.1returns1 item.- Any larger number returns the number followed by
items, for example12 items.
The important part is the order. Check null with a strict comparison before anything else, and make sure 0 reaches its own branch.
Recap
?T means T|null, and it documents that absence is allowed. Use strict null checks so empty strings and zero stay meaningful. Make optional parameters explicitly nullable when they default to null, and define exactly what absence means for each function.
Functions · Arrays · Types
- Distinguish absent values from empty strings and zero
- Implement and test a deterministic PHP 8.4 function