Optionals are objects that represent a presence of something or a lack of something. It is a close behaviour to nullable types. You can also treat an optional as an collection (for example, an array) that can either be empty or contain exactly one value.
You can create an optional with two static methods of Optional class: some($value) creates an optional with a value,
and none() creates an empty optional:
/** @var Optional<int> $someInt */
$someInt = Optional::some(15);
/** @var Optional<int> $emptyInt */
$emptyInt = Optional::none();
Generic-wise, Optional::none() returns type that is understood by PhpStan/Psalm as Optional<null>, but because
all empty optionals, no matter the type, are the same, there is no problem with defining another type with @var
annotation.
Optionals have isEmpty() and nonEmpty() functions that both return bool and are negations of each other. It also
implements two methods from IterableOnce interface:
getLength(): int, which will return 0 for empty optionals and 1 for non-empty optionalscount(callable $filter): int, which return 1 for non-empty optional that contains a value matching filter, and 0 otherwise/** @var Optional<int> $someInt */
$someInt = Optional::some(15);
$someInt->isEmpty(); // false
$someInt->nonEmpty(): // true
$someInt->getLength(); // 1
$someInt->count(fn (int $i) => $i > 10); // 1
$someInt->count(fn (int $i) => $i > 100); // 0
/** @var Optional<int> $emptyInt */
$emptyInt = Optional::none();
$emptyInt->isEmpty(); // true
$emptyInt->nonEmpty(); // false
$someInt->getLength(); // 0
$someInt->count(fn (int $i) => $i > 10); // 0
$someInt->count(fn (int $i) => $i > 100); // 0
You can also use the ifSet(callable $consumer) and ifEmpty(callable $action) methods. Callable passed in ifSet
will be called only if optional has a value, while callable passed in ifEmpty will be called only if optional does
not have a value. In case of ifSet, content of the optional will be passed as an argument to the callable.
/** @var Optional<int> $someInt */
$someInt = Optional::some(15);
$someInt->ifSet(fn (int $i) => doSomething($i)); // "doSomething" will be called with "15" as an argument
$someInt->ifEmpty(fn () => doSomething()); // "doSomething" will not be called
/** @var Optional<int> $emptyInt */
$emptyInt = Optional::none();
$emptyInt->ifSet(fn (int $i) => doSomething($i)); // "doSomething" will not be called
$emptyInt->ifEmpty(fn () => doSomething()); // "doSomething" will be called
The most basic and most dangerous way of getting value from optional is to call a get() method, but that method
will throw NoSuchElementException if optional is empty:
Optional::some(15)->get(); // 15
Optional::none()->get(); // throws NoSuchElementException
Safer way is to call a getOrElse($default) method that, for empty optionals, will return a passed default value:
Optional::some(15)->getOrElse(30); // 15
Optional::none()->getOrElse(30); // 30
There is also an additional getOrNull() method that can be treated as an alias for getOrElse(null):
Optional::some(15)->getOrNull(); // 15
Optional::none()->getOrNull(); // null
If you still want to use optional, but you want it to contain a default value, you can use orElse($value) method:
Optional::some(15)->orElse(30)->get(); // 15
Optional::none()->orElse(30)->get(); // 30
Because Optional class implements IterableOnce interface, it comes with some methods that, while having a huge sense
for collections, might not be obvious here. While those methods are described in details below, it is worth noting that
IterableOnce interface extends a native IteratorAggregate interface from PHP standard library, which allows you
to run a foreach over an optional:
foreach (Optional::some(15) as $value) {
echo $value; // will print "15" once
}
foreach (Optional::none() as $value) {
echo $value; // won't do anything
}
You will achieve the same result with forEach method:
Optional::some(15)->forEach(
function (int $i) {
echo $i; // will print "15" once
}
);
Optional::none()->forEach(
function (int $i) {
echo $i; // won't do anything
}
);
map(callable $mapper) applies content of optional to a $mapper and creates a new optional with result. If current
optional is empty, then resulting optional will also be empty.
Optional::some(15)->map(fn (int $i) => $i + 3)->get(); // 18
Optional::none()->map(fn (int $i) => $i + 3)->isEmpty(); // true
If callable returns another optional instead of a value, instead of having an “optional inside optional” you can use
a flatMap to flatten results to a single optional:
// with map():
/** @var Optional<Optional<int>> $twoLayers */
$twoLayers = Optional::some(15)->map(fn (int $i) => Optional::some($i + 3));
$twoLayers->get()->get(); // 18
// with flatMap():
/** @var Optional<int> $singleLayer */
$singleLayer = Optional::some(15)->flatMap(fn (int $i) => Optional::some($i + 3));
$singleLayer->get(); // 18
And another example, with callable returning empty optional:
// with map():
/** @var Optional<Optional<int>> $twoLayers */
$twoLayers = Optional::some(15)->map(fn (int $i) => Optional::none());
$twoLayers->isEmpty(); // false
$twoLayers()->get()->isEmpty(); // true
// with flatMap():
/** @var Optional<int> $singleLayer */
$singleLayer = Optional::some(15)->flatMap(fn (int $i) => Optional::none());
$twoLayers->isEmpty(); // true
filter(callable $predicate) and filterNot(callable $predicate) will return optionals that are:
filterNot, does not match) a passed predicateOptional::some(15)->filter(fn (int $i) => $i > 10)->get(); // 15
Optional::some(15)->filter(fn (int $i) => $i > 100)->isEmpty(); // true
Optional::some(15)->filterNot(fn (int $i) => $i > 10)->isEmpty(); // true
Optional::some(15)->filterNot(fn (int $i) => $i > 100)->get(); // 15
Optional::none()->filter(fn (int $i) => $i > 10)->isEmpty(); // true
Optional::none()->filterNot(fn (int $i) => $i > 10)->isEmpty(); // true
IterableOnce interface defines two methods for value testing:
exists(callable $predicate): bool, that will return true if there is at least one element of collection that
matches a predicateforAll(callable $predicate): bool, that will return true if all elements of collection match a predicate.From perspective of optionals, the most important difference between those two functions is the fact that if any
collection is empty, then forAll will always return true (because if there are no elements, that means that all
elements match a predicate).
That means that:
exists and forAll will return true if value in optional matches given predicateexists will return false and forAll will return true:Optional::some(15)->exists(fn (int $i) => $i > 10); // true
Optional::some(15)->forAll(fn (int $i) => $i > 10); // true
Optional::some(15)->exists(fn (int $i) => $i > 100); // false
Optional::some(15)->forAll(fn (int $i) => $i > 100); // false
Optional::none()->exists(fn (int $i) => $i > 10); // false
Optional::none()->forAll(fn (int $i) => $i > 10); // true
Six methods of folding and reducing defined in IterableOnce interface are described in more detail in separate
documentation page. In case of optionals, these methods do not have much sense and can be described as aliases of
other methods:
foldLeft($startValue, callable $operator) is equal to
map(fn ($value) => $operator($startValue, $value))->getOrElse($startValue)foldRight($startValue, callable $operator) is equal to
map(fn ($value) => $operator($value, $startValue))->getOrElse($startValue) (only difference between those is order
of arguments when invoking $operator callable inside map())reduceLeft(callable $operator) and reduceLeft(callable $operator) ignore $operator completely and instead return
result content of optional, if optional is non-empty, or throw UnsupportedTraversalException if optional is empty.reduceLeftOption(callable $operator) and reduceRightOption(callable $operator) both return $this.Note that null can also be stored inside an Optional, just like it can be stored inside array:
/** @var Optional<int|null> $someNullableWithInt */
$someNullableWithInt = Optional::some(15);
/** @var Optional<int|null> $someNullableWithNull */
$someNullableWithNull = Optional::some(null);
/** @var Optional<int|null> $emptyNullable */
$emptyNullable = Optional::none();
In this example, $someNullableWithNull is not an empty optional, but an optional with value - it just so happen
that this value is equal to null.
Real life usage of optionals of nullable values is parsing request body for PATCH request. In typical REST API,
in PATCH request only changed properties should be sent. For example, let’s consider a resource representing a single
person, with three fields:
firstName of type stringmiddleName of type string|nulllastName of type stringExample resource:
{
"firstName": "John",
"middleName": "Adam",
"lastName": "Doe"
}
If we want to change lastName to “Smith”, we should send PATCH request to that resource with content that contains
only properties that we want to change:
{
"lastName": "Smith"
}
Now the API, written in PHP, converts this request to object of class that represents a PATCH request. How to store
information that remaining two fields were not sent and thus should not change? While the first, most obvious answer
to that would be “use nullable types”, it creates a new problem with middleName field: null is a proper value there,
because we decided that middleName is a nullable property.
Another solution would be to add a boolean flag, for example $isMiddleNamePassed, to the request class:
$isMiddleNamePassed === false, that means that middle name was not passed in request and thus it should not change$isMiddleNamePassed === true and $middleName === null, that means that null was passed as a value in request
and thus middle name should be changed to null.This “value + flag” pairing is what optionals do. While entity representing a single person still has fields with
simple string or string|null types, a class representing a PATCH request will use optionals:
firstName of type Optional<string>middleName of type Optional<string|null>lastName of type Optional<string>If a property is not passed in request, then request object will contain Optional::none() for that property. If
a property is passed, request object will contain Optional::some($value) for that property, even if that value can be
equal to null.