From PHP 8, you are able to use the null safe operator which combined with the null coalescing operator allows you to write code like:
echo $data->getMyObject()?->getName() ?? '';
By using ?-> instead of -> the chain of operators is terminated and the result will be null.
The operators that "look inside an object" are considered part of the chain.
- Array access ([])
- Property access (->)
- Nullsafe property access (?->)
- Static property access (::)
- Method call (->)
- Nullsafe method call (?->)
- Static method call (::)
e.g. for the code:
$string = $data?->getObject()->getName() . " after";
if $data is null, that code would be equivalent to:
$string = null . " after";
As the string concatenation operator is not part of the 'chain' and so isn't short-circuited.
Answer from Danack on Stack OverflowIs there a "nullsafe operator" in PHP? - Stack Overflow
Null Safe Operator for PHP 7.x or Earlier - Stack Overflow
The null safe operator in PHP 8
PHP 8: the nullsafe operator
Videos
From PHP 8, you are able to use the null safe operator which combined with the null coalescing operator allows you to write code like:
echo $data->getMyObject()?->getName() ?? '';
By using ?-> instead of -> the chain of operators is terminated and the result will be null.
The operators that "look inside an object" are considered part of the chain.
- Array access ([])
- Property access (->)
- Nullsafe property access (?->)
- Static property access (::)
- Method call (->)
- Nullsafe method call (?->)
- Static method call (::)
e.g. for the code:
$string = $data?->getObject()->getName() . " after";
if $data is null, that code would be equivalent to:
$string = null . " after";
As the string concatenation operator is not part of the 'chain' and so isn't short-circuited.
Nullsafe operator allows you to chain the calls avoiding checking whether every part of chain is not null (methods or properties of null variables).
PHP 8.0
$city = $user?->getAddress()?->city
Before PHP 8.0
$city = null;
if($user !== null) {
$address = $user->getAddress();
if($address !== null) {
$city = $address->city;
}
}
With null coalescing operator (it doesn't work with methods):
$city = null;
if($user !== null) {
$city = $user->getAddress()->city ?? null;
}
Nullsafe operator suppresses errors:
Warning: Attempt to read property "city" on null in Fatal error:
Uncaught Error: Call to a member function getAddress() on null
However it doesn't work with array keys:
$user['admin']?->getAddress()?->city //Warning: Trying to access array offset on value of type null
$user = [];
$user['admin']?->getAddress()?->city //Warning: Undefined array key "admin"
To emulate null safe operator, you can take inspiration from the option type. The idea is simple - you wrap the value in an object, as you suggested, and have a magic method handling. Now, the magic method will either return $this - e.g. the same Option instance, if this is already a null, or call the method and wrap the result in an Option, to allow further chaining.
The challenge you face with PHP will be where to terminate, e.g. where to return the original value, and not the wrapper. If you can afford an explicit method call at the end of the chain, it becomes straightforward.
It would look something like (not tested, written for illustrative purposes)
class Option {
protected $value;
public function __construct($value)
{
$this->value = $value;
}
public function __call($methodName, $args) {
if (is_null($this->value)) {
return $this;
}
return new Option($this->value->$methodName($args));
}
public function __get($propertyName) {
if (is_null($this->value)) {
return $this;
}
return new Option($this->value->$propertyName);
}
public function get() {
return $this->value;
}
}
So you will do:
$country = new Option($session)->user->getAddress()->country->get();
We can create a black holed class to instead of throw an exception return Null if we call an undefined method of a generic class which will acts as our fallback.
<?php
// _7 - for brevity write and 7 is closer to question mark ;)
class _7 {
public function __call($method, $args) return null;
}
$myDate = (\DateTime::createFromFormat('d/m/Y','05/04/1989') ?: new _7)->format('Y-m-d') ?? 'Bad date';
//'1989-04-05' - because expected date format is provided to Datetime
$myDate = (\DateTime::createFromFormat('d/m/Y','1989') ?: new _7)->format('Y-m-d') ?? 'Bad date';
//'Bad date' - Very cool! No exceptions here, successfull fallback;
Important! This approach only works with PHP >= 7.0, i will collect info to work with 5.x soon as possible.