Если вы уже использовали оператор объединения с null (null coalescing operator), вы, вероятно, замечали его недостатки: он не работает с вызовами методов. Вместо этого приходится делать промежуточные проверки или полагаться на хелперы вроде optional , которые есть в некоторых фреймворках:

$startDate = $booking->getStartDate();

$dateAsString = $startDate ? $startDate->asDateTimeString() : null;

Оператор nullsafe предоставляет схожую с null coalescing функциональность, но при этом поддерживает вызовы методов. Вместо того чтобы писать так:

$country =  null;
 
if ($session !== null) {
    $user = $session->user;
 
    if ($user !== null) {
        $address = $user->getAddress();
 
        if ($address !== null) {
            $country = $address->country;
        }
    }
}


В PHP 8 можно написать следующее:

$country = $session?->user?->getAddress()?->country;

Давайте посмотрим, на что способен этот новый оператор, а на что — нет!

Оператор nullsafe в деталях

Давайте начнём с самого главного вопроса: в чём же именно разница между оператором объединения с null и оператором nullsafe?

Взглянем на этот пример:

class Order
{
    public ?Invoice $invoice = null;
}

$order = new Order();

Здесь у нас есть объект Order , который имеет опциональную связь с объектом Invoice . Теперь представьте, что мы хотим получить номер счёта (если он не null). Это можно сделать как с помощью оператора объединения с null, так и с помощью оператора nullsafe:

var_dump($order->invoice?->number);
var_dump($order->invoice->number ?? null);

Так в чём же разница? Хотя в этом примере оба оператора дают одинаковый результат, у каждого из них есть свои сценарии использования. Например, оператор объединения с null можно использовать с ключами массивов, тогда как оператор nullsafe с ними работать не умеет:

$array = [];

var_dump($array['key']->foo ?? null);var_dump($array['key']?->foo);

Warning: Undefined array key "key"

С другой стороны, оператор nullsafe может работать с вызовами методов, в отличие от оператора объединения с null. Представьте себе такой объект Invoice :

class Invoice
{
    public function getDate(): ?DateTime { /* … */ }
    
    // …
}

$invoice = new Invoice();

Вы можете использовать оператор nullsafe, чтобы вызвать format  для даты счёта, даже если она равна null :

var_dump($invoice->getDate()?->format('Y-m-d'));

// null

Тогда как оператор объединения с null завершится ошибкой:

var_dump($invoice->getDate()->format('Y-m-d') ?? null);

Fatal error: Uncaught Error: Call to a member function format() on null

 

Короткое замыкание (short-circuiting)

Иногда можно использовать любой из двух операторов, а иногда подойдёт только один конкретный. Разница в том, что оператор nullsafe использует форму короткого замыкания (short-circuiting): при записи ?->  PHP проверяет левую часть выражения, и если она равна null , правая часть попросту отбрасывается. Оператор объединения с null на самом деле представляет собой замаскированный вызов isset  для левого операнда и не поддерживает короткое замыкание.

Короткое замыкание также означает, что при записи такого вида:

$foo?->bar(expensive_function());

expensive_function  выполнится только в том случае, если $foo  действительно не равен null .

Вложенные операторы nullsafe

Вызовы с оператором nullsafe можно выстраивать в цепочки:

$foo?->bar?->baz()?->boo?->baa();

Только для чтения данных

Оператор nullsafe нельзя использовать для записи данных в объекты:

$offer?->invoice?->date = new DateTime(); 

Оператор nullsafe — это определённо недостающий элемент пазла, который наконец-то появился в PHP. Учитывая динамическую природу языка, приятно иметь удобный способ работы с null . Разница и пересечения между оператором nullsafe и оператором объединения с null поначалу могут сбивать с толку, но я уверен, что мы к этому быстро привыкнем.