The nullsafe operator turns a fatal error into a null, which is sometimes exactly right and sometimes the removal of the only signal that something upstream was wrong.
$city = $order?->customer?->address?->city;
// if $order is null this is null, and so is the case where
// the customer genuinely has no address. two very different
// situations, one result.
// the version that keeps the distinction:
if ($order === null) {
throw new OrderNotFound($id);
}
$city = $order->customer->address?->city;
The rule that has held up is one nullsafe per chain, at the link that is genuinely optional, and a real check at the links that are not. A chain of three is a statement that any of the three may legitimately be absent, which is rarely true and is usually a way of not thinking about which one. Debugging a null that came from four possible places is materially harder than a fatal error naming the exact property.