Lesson 70 of 100
Types15 min5 checksPHP 8.5

Design Nullable Function Contracts

Learn how ?T and T|null model an absent value, and how to keep null separate from empty strings and zero in PHP 8.4 functions.

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.

Example
<?php
function middleInitial(?string $middle): ?string
{
    if ($middle === null || $middle === '') {
        return null;
    }
    return strtoupper($middle[0]) . '.';
}

echo json_encode([middleInitial('grace'), middleInitial(''), middleInitial(null)]);
Output
["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.

Example
<?php
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)]);
Output
["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:

Example
<?php
function greet(?string $name = null): string
{
    return $name === null ? 'Hello, guest' : 'Hello, ' . $name;
}

echo json_encode([greet(), greet(''), greet('Ada')]);
Output
["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

  1. Decide what null means: unknown, not found, not provided, or intentionally cleared.
  2. Decide whether empty values are also treated as absent. If not, never use truthiness.
  3. Check null first, then handle real values in order of specificity.
  4. Keep the return type as narrow as possible. Return ?string only when callers truly need to distinguish absence.
  5. 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:

  • null returns unknown.
  • A negative number returns invalid.
  • 0 returns out of stock.
  • 1 returns 1 item.
  • Any larger number returns the number followed by items, for example 12 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.

Before you start

Functions · Arrays · Types

You’ll practice
  • Distinguish absent values from empty strings and zero
  • Implement and test a deterministic PHP 8.4 function
Your task

Make describeStock() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.