PHP 8.0 feature focus: nullsafe methods

Larry Garfield
Larry Garfield
Director of Developer Experience
08 Oct 2020

Last week we used Weak Maps to solve one edge case. Today, we simplify the most common and annoying edge case: That of nothingness.

Let’s be honest, null values suck. They’ve been called the billion dollar mistake, but programmers in most languages still have to deal with it. (Except for those working in Rust.) They especially suck when you have a variable that could be an object or null. If it’s an object, presumably you know it’s type and what it can do; if it’s null, you can’t do much of anything with it.

The standard way to deal with that situation is to branch using a conditional check, the eponymous “if null” pattern:

$user = get_user($id);
if (!is_null($user)) {
    $address = $user->getAddress();
    if (!is_null($address)) {
        $state = $address->state;
        If (!is_null($state)) {
            // And so on.

This is, of course, gross and hard to read, but necessary if you want to avoid the dreaded Call to a member function on null error.

In an ideal world, we could structure our code so that null is impossible. Sadly, we’re not living in an ideal world, especially when we have to deal with someone else’s code. For that reason, a number of languages have what is called a “nullsafe operator.” And, as of version 8.0, PHP is one of them.

The nullsafe operator is really a modifier on object access, either properties or method calls. If either of those are preceded by a ?, it has the same effect as wrapping the access in an if (!is_null)) check. The example above, for instance, becomes:

$state = get_user($id)?->getAddress()?->state;

In this example, the value returned by get_user() is either a User object or null. The value returned by User::getAddress() is either an Address object or null. If get_user() returns null, the ? catches that and returns null, ignoring the entire rest of the line. The same happens for getAddress(). At the end, $state is either a valid state value pulled from the state property of the address, or it’s null.

The nullsafe operator short-circuits the rest of the line the first time a null value is encountered. That means even dynamic calls or erroring calls later in the process don’t happen. For example:


$bestSaleItem = $catalog?->getProducts(get_seasonal_type())?->mostPopular(10)[5];

If $catalog is null, get_seasonable_type() won’t be called at all. The entire chain stops after detecting that $catalog is null and sets $bestSaleItem to null. Similarly, if getProducts() returns null, then mostPopular() is never called. However, that does not extend to arrays. If mostPopular() is still called and it returns null, you’ll still get a Trying to access array offset on value of type null error.

There are a few limitations to the nullsafe operator, of course. It only works for reading properties or methods, not for writing to values. It also doesn’t deal with references, because references require real values. (This is consistent with other languages that have a nullsafe operator.)

It also provides no indication of which step in the chain returned null.; If $bestSaleItem is null, it could be because $catalog was null, or getProducts() returned null, or mostPopular() returned null. There’s no way to tell. Sometimes that’s fine. If you do need to tell the difference, though, nullsafe won’t help you. For that you really need an Either monad, but that’s a topic for a very different time…

Nonetheless, nullsafe can help reduce the amount of boilerplate code floating around your object-oriented method chains. We have Ilija Tovilo to thank for the nullsafe RFC.

Stick around for our next installment, when we cover a series of smaller improvements that will make PHP 8.0 just generally more pleasant to live in.

You can try out pre-release copies of PHP 8.0 today on, with just a one-line change. Give it a whirl, and let us know what your favorite features are.