Lesson 72 of 100
Classes15 min5 checksPHP 8.5

Navigate Optional Objects with Nullsafe Calls

Use the ?-> operator to read properties and call methods through objects that may be null, and combine it with ?? for a scalar fallback.

01 · THE IDEAREAD → TRY → LEARN

The problem: optional objects

Real object graphs often have gaps. A user may have no address, a team may have no lead, and a lead may have no email. Reading $user->address->city when address is null produces a warning and gives you null anyway. Calling a method on null throws an Error. Before the nullsafe operator you had to wrap each step in an if or an isset() check.

The nullsafe operator ?-> is written like normal property or method access, with a question mark in front of the arrow. If the value on its left is null, the access yields null instead of failing. If the value is an object, it behaves like ->.

A first example

The next example has a user with an optional address, and an address with an optional city.

Example
<?php
class Address
{
    public function __construct(public ?string $city) {}
}

class User
{
    public function __construct(public ?Address $address) {}
}

$withCity = new User(new Address('Lisbon'));
$withoutAddress = new User(null);

echo json_encode($withCity->address?->city), "\n";
echo json_encode($withoutAddress->address?->city), "\n";
Output
"Lisbon"
null

The first chain reaches the city and returns "Lisbon". In the second chain address is null, so ?->city produces null and no warning appears.

Short-circuiting

The nullsafe operator does more than return null for one step. When the left side is null, PHP skips the rest of the chain. That includes the evaluation of method arguments, so any side effects in those arguments do not happen.

Example
<?php
class Profile
{
    public function label(string $prefix): string
    {
        return $prefix . ':ready';
    }
}

function prefix(): string
{
    echo "prefix evaluated\n";
    return 'profile';
}

$profile = null;
$result = $profile?->label(prefix());
echo json_encode($result), "\n";

$profile = new Profile();
$result = $profile?->label(prefix());
echo json_encode($result), "\n";
Output
null
prefix evaluated
"profile:ready"

With null, the helper never runs. With a real object, it runs and the method result is returned.

Adding a scalar fallback

A nullsafe chain ends in either a real value or null. Callers usually want a plain default instead, which is what the null coalescing operator ?? provides. Put it at the end of the chain.

Example
<?php
class Person
{
    public function __construct(private ?string $email) {}

    public function email(): ?string
    {
        return $this->email;
    }
}

class Team
{
    public function __construct(private ?Person $lead) {}

    public function lead(): ?Person
    {
        return $this->lead;
    }
}

function leadEmail(?Team $team): string
{
    return $team?->lead()?->email() ?? 'unknown';
}

echo json_encode([
    leadEmail(new Team(new Person('ana@example.com'))),
    leadEmail(new Team(null)),
    leadEmail(new Team(new Person(null))),
    leadEmail(null),
]), "\n";
Output
["ana@example.com","unknown","unknown","unknown"]

Each ?-> guards the step that precedes it. A missing team, a missing lead and a missing email all end up as 'unknown'.

Things to remember

  • ?-> only checks for null. If the left side is some other non-object value such as an integer, you still get the usual warning or error.
  • Use ?? rather than ?: for the fallback when falsy real values such as '' or 0 must be kept. ?: replaces them, while ?? replaces only null.
  • Nullsafe access is for reading. You cannot use it as the target of an assignment such as $a?->b = 1.
  • Do not put ?-> everywhere. If a value should never be null, a plain -> will reveal the bug sooner.

Exercise contract

Write shippingCity(array $order): string. The starter already converts the nested array to local objects with json_decode(json_encode($order), false), so each nested level can be read with ->. Return the city at customer, then address, then city. If the customer, the address or the city is null, return 'unknown'. An empty string city is a real value and must be returned unchanged.

The tests check the returned string for a full record, a null customer, a null address, a null city and an empty city.

Recap

Use ?-> to stop a chain safely at the first null, rely on short-circuiting to skip later calls and their arguments, and finish with ?? when you need a scalar default.

Before you start

Functions · Arrays · Types

You’ll practice
  • Use ?-> on local object chains with a scalar fallback
  • Implement and test a deterministic PHP 8.4 function
Your task

Make shippingCity() 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.