Null safe operator in practice
Null-safe operator has been added to PHP 8.1: it is a new object operator that prevents a Fatal error, and its following execution stop, when calling a method or a property on the null
value. It has a high appeal, given that it is called ‘safe’ and it reduces the checks prior a method call. And the null safe operator in practice also has its own drawbacks.
The first drawback is the check that was before the method call, should actually be after it: it is still needed. The second one is that the types used in the expression must now avoid the null
type. As usual, a new syntax just means new organisation around it. Pick your side.
Let’s review that together, shall we ?
Null safe operator
The Null safe operator ?->
is a close cousin to the object operator ->
, with whom it shares the arrow design. The difference lies in the handling of null
, when used as the object part of the syntax.
<?php $x = null; $x?->foo(); // Nothing happens $x->foo(); // Fatal error: Uncaught Error: Call to a member function foo() on null in ?>
It only works on null
values: there is no differences when calling a method on an integer or an array: Fatal error.
The first impact of this operator is to prevent a pesky stop in execution, just because an object is not available. There are countless methods that return an object, or null in case of failure to locate the information. Those methods require an extra check on their result, and the null-safe operator is just there for this.
<?php function getInstance($what) : ?What {} $x = getInstance('what'); $x?->foo(); // foo() is executed on What, or nothing happens. $x->foo(); // Fatal error if getInstance() returned null // safe version of the above, with extra code if ($x !== null) { $x->foo(); } ?>
Hence, the name of the operator: it is safe, because it doesn’t block execution if a null
value manage to find its way. The second advantage is the short code writing: when the code is prone to errors due to null
values, just add a question mark ?
to make the problem go away.
Check before, check after: just check it!
When using the null-safe operator, the object being null
cancels the whole expression: it produces a result, which is null
itself. The story doesn’t tell if it is the same null
as the object, and that is left to the reader to figure out.
Since the execution continues, this is makes the null-safe expression optional. It might be a fire-and-forget call, where the call does something that has no local impact, like a log; it might be an optional call, or a call that fails safely. It might be a void
returning function, but we’ll come back to this case later.
On the other hand, when the expression is supposed to return a value, that value has to be checked against null
. Remember that no check was done on the object before execution.
<?php function getInstance($what) : ?What {} $result = getInstance('what')?->foo(); // Checking the result of the expression. if ($result === null) { manageError(); } // else go on with execution, all is good! ?>
The first observation here is the check on null. With ->
, the ckeck is made before the call, and the returned value is only the result. With ?->
, the check is made after the call, against the returned value: the result may be any valid value, or null
in case it was not possible.
In the end, the check was not removed, and there is still some extra code to add to ensure smooth execution.
Which null is this?
The second observation is that the code can’t differentiate between null
because the expression has a wrong object, or null
that is a valid returned value. Consider the foo
method defined like this:
<?php class What { function foo() : ?int {} } function getInstance($what) : ?What {} $result = getInstance('what')?->foo(); $result = null?->foo(); // Is it null because of foo? or because of getInstance() ? if ($result === null) { manageError(); } ?>
Traditionally, null
is a sign of error: we asked getInstance()
and foo
to do something, and, at least, one of them failed. For both cases, error management might be on the menu.
More precisely, the code can’t tell the source of the errors apart, because both of them are null
. It needs an extra step to discover that source, so more code. Damned, this was just what we wanted to save.
Typing for null-safe operator
The conclusion of that second observation is that the last method of the object expression should not return null
. That way, a failed object expression can show its true colors, using the null
type.
By extension, that method shall also not return void
: while this type is different from null
, collecting the returned value of a void-function yield… null
again. So, this type should also be avoided.
<?php class What { function foo() : int {} function goo() : void {} } function getInstance($what) : ?What {} // This returns an int, or NULL in case of error $result = getInstance('what')?->foo(); // Always returns null, due to void $result = getInstance('what')?->goo(); // This returns int in case of success, and null otherwise // the last -> should hopefully always work $result = getInstance('what')?->foo1()->foo2()->property->goo(); $result = getInstance('what')?->foo1()->foo2()->property->property2; ?>
Finally, take note that only the last called element of the object expression shall have a non-null type. In the example above, it is the goo
method. It could be a typed property too.
All the other methods and properties in-between must succeed, and their type won’t matter. In the example above, they must always return a valid type to keep the chain going. More ?->
would also be possible.
Null safe operator in practice with static analysis
?->
is a nice syntactic upgrade from ->
, as it makes the validation optional. For any code that is disciplined enough, it works just fine and prevents blocking bugs, while reducing the amount of code.
Yet, it should be monitored for two extra patterns:
- the resulting value must be compared to
null
, to allow for error management - the last method or property called cannot be typed nullable, to avoid confusing one error for the other.
?->
main advantage is the switching the check’s load from before the call to after the call. It comes with the possible advantage of saving the check altogether by just ignoring it.
There are many situations like this in source code : uninitialized variables, calling int-typed argument with some strings, or json_decode() without a try/catch: it works the vast majority of the time, until an error appears. And with ?->
, that error is hidden.