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
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
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
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
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
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
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
Countryclass with a publicname - an
Addressclass with acountry(): ?Countrymethod - a
Customerclass with anaddress(): ?Addressmethod - 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
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.