Creating an `Option` Type in PHP

php
Table of Contents

If you've ever worked with the Rust language, you've likely run into Option.

There is no concept of null or nil in Rust, nor is there any concept of optional parameters in function definitions.

This is one of the things that makes Rust's type system so powerful, reliable and, at times, annoying.

The Option type itself is actually just an enum (enumeration) that is defined as:

In Rust, enums are able to hold values inside of variants. This is something that has been thought of in PHP land too.

Rust provides some global constructors for the Option type too. You don't need to type Option::Some(value) everytime, you can just type Some(value).

If you were to add an optional parameter to a function, it would look something like this:

Again, the Option enum is generic so you need to provide the type of the wrapped value.

Calling unwrap() on the Option will return the inner value, in this case a String. If the Option is None, then the program panics and fails to run.

The Option type is best used in situations where you sometimes return a value from a function (typically an object).

It's not uncommon to forget about the cases where no value, or null, is returned from the function.

By introducing an Option type you have to explicitly handle the None or null case, otherwise you won't be able to access the wrapped value.

Here's an example in a Laravel application:

If the user is logged in this code will work fine. If the user isn't logged in, Auth::user() will return null and the ->name access will error out.

In PHP 8, you could handle this with the ?-> (nullsafe) operator but you would still end up with a null value in $name.

If the Auth::user() function used Option:

To access the User, you would need to do something like this:

If None is returned from the Auth::user() function, the unwrap() method call will fail and throw an Exception because you can't unwrap a None value.

You have to explicitly consider the None scenario when working with Option.

We can start by defining an interface for our Option type that the Some and None types will implement.

I'll be writing PHP 8.0 compatible code. If you haven't upgraded already, you definitely should.

These three functions are the bare essentials. You'll be able to determine if a type is None, Some and retrieve the internal value.

With this interface defined, we can go ahead and create our None type.

The None type is the simplest. The unwrap() method will throw a \RuntimeException as you can't unwrap a None value.

In PHP 8.1 you could replace the mixed return type with never as it always throws a \RuntimeException.

The Some type has a little more going on. We need to define a __construct method to actually hold the value and also return that value from unwrap().

With these implemented, we can go ahead and write our first function that uses Option.

When you start using Option, you'll find yourself using ?: (ternaries) or if..else statements to set default values.

We can help ourselves out by introducing a new unwrapOr(mixed $or) method.

Let's first add it to our Option interface.

And then implement in our Some and None classes.

The Some class should always hold a value, so we will never need to return the $or.

The None class can never be unwrapped so we will always return the value of $or.

With this method in place, we can remove the ternary from our sayHello() function:

PHP doesn't support runtime generics. There is no first-party syntax for them either.

We're very fortunate to have static analysis engines though, so we can use DocBlocks to imitate generic types.

Let's start by adding some annotations to your Option interface.

I'll be using annotations supported by PHPStan.

This makes the Option type generic on T. To actually make T do something, we'll need to tell the analyser where T comes from.

Let's add some annotations to the Some type.

These class-level annotations will tell the analyser that the Some class is also generic and that the type of T in Some should also be used as the generic type of Option which we're implementing.

Since our unwrap() and unwrapOr() method return a mixed type inside of Some, we can't guarantee that any further method calls or property access is valid.

To tackle this we can add more annotations to those methods, as well as the property promotion in our constructor.

Adding the @var annotation to the promoted property, the analyser will figure out that the type of $value should be used as T.

It can then detect any problems with unwrap and unwrapOr calls.

I've implemented everything inside of an Option interface and 2 classes that implement that interface.

This means that our Some and None classes have to implement all of the methods that are part of the contract.

It's not the prettiest thing ever, so if you wanted to refactor this you could instead create an abstract class Option that the Some and None classes extend.

You can then implement the methods inside of the abstract class instead and reduce some of the duplication between the two option types.

PHP 8.1 will be introducing first-party enum types. This brings us one tiny step closer to a Rust-like implementation:

The only differences between the two languages would be that fact that PHP can't hold values inside of enum variants and that they can't be generically typed.

I hope that PHP will get tagged enums in the near future. This draft RFC has already started the conversation.

With tagged enums, we could have something like this:

Add some generics ontop and we have a slightly more verbose but more powerful Option type.

Enjoyed this post or found it useful? Please consider sharing it on Twitter.