Let's discuss null
for a moment. Some have called the concept of null
a "billion dollar mistake", arguing it allows for a range of edge cases that we have to take into account when writing code. It might seem strange to work in a programming language which doesn't support null
, but there are in fact useful patterns to replace it, and get rid of its downsides.
Let's illustrate those downsides first with an example. Here we have a Date
value object with a timestamp variable, format function and static constructor called now
. Why we're not using DateTime
will become clear soon.
class Date
{
public int $timestamp;
public static function now(): self { /* … */ }
public function format(): string { /* … */ }
// …
}
Next we have an invoice with a payment date:
class Invoice
{
public ?Date $paymentDate = null;
// …
}
The payment date is nullable because invoices can be pending, and hence not yet have a payment date.
As a side note: take a look at the nullable notation: we've prefixed Date
with a question mark, indicating that it can either be Date
or null
. We've also added a default = null
value, to make sure the value is never uninitialized; you know, to prevent all those runtime errors you might encounter.
Back to our example: what if we want to do something with our payment date's timestamp:
$invoice->paymentDate->timestamp;
Since we're not sure $invoice->paymentDate
is a Date
or null
, we risk running into runtime errors:
Trying to get property 'timestamp' of non-object
Before PHP 7.0, you'd use isset
to prevent those kinds of errors:
isset($invoice->paymentDate)
? $invoice->paymentDate->timestamp
: null;
That's rather verbose though and is why a new operator was introduced: the null coalescing operator.
$invoice->paymentDate->timestamp ?? null;
This operator will automatically perform an isset
check on its lefthand operand. If that returns false
, it will return the fallback provided by its righthand operand. In this case: the payment date's timestamp or null
. A nice addition that reduces the complexity of our code.
PHP 7.4 added another null coalescing shorthand: the null coalescing assignment operator. This one not only supports the default value fallback, but will also write it directly to the lefthand operand. It looks like this:
$temporaryPaymentDate = $invoice->paymentDate ??= Date::now();
So if the payment date is already set, we'll use that one in $temporaryPaymentDate
, otherwise we'll use Date::now()
as the fallback for $temporaryPaymentDate
and also write it to $invoice->paymentDate
immediately.
A more common use case for the null coalescing assignment operator is a memoization function: a function that stores the result once it's calculated:
function match_pattern(string $input, string $pattern) {
static $cache = [];
$key = $input . $pattern;
return $cache[$key] ??= (function (string $input, string $pattern) {
preg_match($pattern, $input, $matches);
return $matches[0];
})($input, $pattern);
}
This function will perform a regex match on a string with a pattern, but if the same string and same pattern are provided, it will simply return the cached result. Before we had the null coalescing operator assignment, we'd need to write it like so:
function match_pattern(string $input, string $pattern) {
static $cache = [];
$key = $input . $pattern;
if (! isset($cache[$key])) {
$cache[$key] = (function (string $input, string $pattern) {
preg_match($pattern, $input, $matches);
return $matches[0];
})($input, $pattern);
}
return $cache[$key];
}
There's one more null-oriented feature in PHP added in PHP 8: the nullsafe operator. Take a look at this example:
$invoice->paymentDate->format();
What happens if our payment date is null
? You'd again get an error:
Call to a member function format() on null
Your first thought might be to use the null coalescing operator, but that wouldn't work:
$invoice->paymentDate->format('Y-m-d') ?? null;
You see, the null coalescing operator doesn't work with method calls on null
. So before PHP 8, you'd need to do this:
$paymentDate = $invoice->paymentDate;
$paymentDate ? $paymentDate->format('Y-m-d') : null;
Fortunately there's the nullsafe operator: it will only perform method calls when possible and otherwise return null
instead:
$invoice->getPaymentDate()?->format('Y-m-d');
# Dealing with null — there's another way
I started this section saying null
is called a "billion dollar mistake" but next I showed you three ways PHP is embracing null
with fancy syntax. The reality is that null
is a frequent occurrence in PHP. It's a good thing we have syntax to deal with it in a sane way. However, it's also good to look at alternatives to using null
altogether. One such alternative is the null object pattern.
Instead of one Invoice
class that manages internal state about whether it's paid or not; let's have two classes: PendingInvoice
and PaidInvoice
. The PendingInvoice
implementation looks like this:
class PendingInvoice implements Invoice
{
public function getPaymentDate(): UnknownDate
{
return new UnknownDate();
}
}
PaidInvoice
looks like this:
class PaidInvoice implements Invoice
{
// …
public function getPaymentDate(): Date
{
return $this->date;
}
}
Next, there's an Invoice
interface:
interface Invoice
{
public function getPaymentDate(): Date;
}
Finally, here are the two date classes:
class Date
{
// …
}
class UnknownDate extends Date
{
public function format(): string
{
return '/';
}
}
The null object pattern aims to replace null
with actual objects; objects that behave differently because they represent the "absence" of the real object. Another benefit of using this pattern is that classes become more representative of the real world: instead of a "date or null", it's a "date or unknown date", instead of an "invoice with a state" it's a "paid invoice or pending invoice". You wouldn't need to worry about null
anymore.
$invoice->getPaymentDate()->format(); // A date or '/'