advanced php language

Nullsafe Operator

The nullsafe operator ?-> lets PHP stop an object access chain when the value on the left is null. Instead of throwing an error, the whole chain returns null.

This is useful when a relationship is genuinely optional: a user may not have a profile yet, an order may not have a delivery address, or a customer may not have an account manager.

The Problem It Solves

Without the nullsafe operator, optional object chains often require repeated checks.

PHP example
<?php

declare(strict_types=1);

final class Profile
{
    public function __construct(
        public string $displayName,
    ) {
    }
}

final class User
{
    public function __construct(
        private ?Profile $profile,
    ) {
    }

    public function profile(): ?Profile
    {
        return $this->profile;
    }
}

$user = new User(null);

if ($user->profile() === null) {
    echo 'Anonymous' . PHP_EOL;
} else {
    echo $user->profile()->displayName . PHP_EOL;
}

// Prints:
// Anonymous

The check is correct, but it is noisy. It also calls profile() twice.

Using ?->

The nullsafe operator keeps the optional access close to the value that may be missing.

PHP example
<?php

declare(strict_types=1);

final class Profile
{
    public function __construct(
        public string $displayName,
    ) {
    }
}

final class User
{
    public function __construct(
        private ?Profile $profile,
    ) {
    }

    public function profile(): ?Profile
    {
        return $this->profile;
    }
}

$user = new User(null);

$displayName = $user->profile()?->displayName ?? 'Anonymous';

echo $displayName . PHP_EOL;

// Prints:
// Anonymous

$user->profile()?->displayName returns null if profile() returns null. The null coalescing operator ?? then provides the fallback.

Chaining Through Several Optional Objects

You can use ?-> more than once in a chain.

PHP example
<?php

declare(strict_types=1);

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

final class Address
{
    public function __construct(
        private ?Country $country,
    ) {
    }

    public function country(): ?Country
    {
        return $this->country;
    }
}

final class Customer
{
    public function __construct(
        private ?Address $address,
    ) {
    }

    public function address(): ?Address
    {
        return $this->address;
    }
}

$customer = new Customer(new Address(null));

$country = $customer->address()?->country()?->name ?? 'Unknown country';

echo $country . PHP_EOL;

// Prints:
// Unknown country

Each ?-> protects only the access immediately after it. If a later part of the chain can also be null, that later access needs its own ?->.

It Only Handles null

The nullsafe operator does not protect against false, empty strings, missing array keys, or invalid types.

PHP example
<?php

declare(strict_types=1);

final class ApiResponse
{
    public function __construct(
        public ?string $requestId,
    ) {
    }
}

function latestResponse(bool $available): ?ApiResponse
{
    return $available ? new ApiResponse('req-123') : null;
}

echo latestResponse(false)?->requestId ?? 'no response';
echo PHP_EOL;

// Prints:
// no response

If a function returns ApiResponse|false, ?-> is the wrong tool until you handle the false case. That usually means the function should be redesigned or wrapped with clearer return values.

Read-Only Access

The nullsafe operator is for reading through an optional chain. You cannot assign through it.

PHP example
<?php

declare(strict_types=1);

final class Profile
{
    public function __construct(
        public string $displayName,
    ) {
    }
}

$profile = new Profile('Ada');

$profile?->displayName;

echo $profile->displayName . PHP_EOL;

// Prints:
// Ada

If you need to update an optional object, check it explicitly first. That makes the missing-value branch visible.

Do Not Hide Required Data Problems

Use ?-> when missing data is normal. Do not use it to silence a bug where the data should exist.

PHP example
<?php

declare(strict_types=1);

final class Order
{
    public function __construct(
        public ?string $paymentReference,
    ) {
    }
}

function receiptReference(Order $order): string
{
    if ($order->paymentReference === null) {
        throw new RuntimeException('Paid orders must have a payment reference.');
    }

    return $order->paymentReference;
}

echo receiptReference(new Order('pay_123')) . PHP_EOL;

// Prints:
// pay_123

For required data, an explicit exception or validation error is usually better than quietly returning a fallback string.

What You Should Be Able To Do

After this lesson, you should be able to use ?-> for optional object access, combine it with ?? for clear fallbacks, and recognise when an explicit guard is safer.

For junior PHP work, this matters because null-related errors are common in real applications. The skill is knowing when null is an expected state and when it signals broken data.

Practice

Practice: Read Optional Customer Data

Create a small customer summary example that uses the nullsafe operator only for genuinely optional data.

Task

Build:

  • a Country class with a public name
  • an Address class with a country(): ?Country method
  • a Customer class with an address(): ?Address method
  • a customerCountry() function that returns the country name or 'Unknown country'

Use strict types. Show one customer with a country and one customer without an address. Keep the expected output inside the PHP code block as printed lines or comments.

Afterward, add a short note explaining why ?-> is appropriate here.

Show solution

This solution uses ?-> because both the address and country are optional relationships.

PHP example
<?php

declare(strict_types=1);

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

final class Address
{
    public function __construct(
        private ?Country $country,
    ) {
    }

    public function country(): ?Country
    {
        return $this->country;
    }
}

final class Customer
{
    public function __construct(
        private ?Address $address,
    ) {
    }

    public function address(): ?Address
    {
        return $this->address;
    }
}

function customerCountry(Customer $customer): string
{
    return $customer->address()?->country()?->name ?? 'Unknown country';
}

echo customerCountry(new Customer(new Address(new Country('France')))) . PHP_EOL;
echo customerCountry(new Customer(null)) . PHP_EOL;

// Prints:
// France
// Unknown country

The nullsafe operator is appropriate because a missing address is an expected state, not a broken invariant. If every customer were required to have an address, an explicit guard or validation error would be clearer.