Development

PHP Match and Enums.

4 minutes

Match and Enums have become a happy place for me writing modern PHP. For those that don’t know, the match expression was added in PHP 8.0 as an alternative to switch. As per the docs:

The match expression is similar to a switch statement but has some key differences:

  • A match arm compares values strictly (===) instead of loosely as the switch statement does.

  • A match expression returns a value.

  • match arms do not fall-through to later cases the way switch statements do.

  • A match expression must be exhaustive.

Enums were added in PHP 8.1 “intended to provide a way to define a closed set of possible values for a type” and they can be plain:

php
1match ($file->type) {
2 FileType::Video => $this->handleVideo($file),
3 FileType::Image => $this->handleImage($file),
4 FileType::Document => $this->handleDocument($file),
5 default => throw new RuntimeException('Unknown file type'),
6};
php
1enum DoorState {
2 case Open;
3 case Closed;
4 case Locked;
5}

Or ‘backed’ with integer or string values:

php
1enum DoorState: int {
2 case Open = 1;
3 case Closed = 2;
4 case Locked = 3;
5}
6 
7// DoorState::Open->value returns 1
8enum DoorState: string {
9 case Open = 'open';
10 case Closed = 'closed';
11 case Locked = 'locked';
12}
13 
14// DoorState::Open->value returns 2

This gives us more options for encapsulating state and, with PHP’s type system improvements, we can pass around enum values rather than integer and string literals and we get some nice hand holding from our tools:

php
1// bad
2function dont_do_this(string $doorState) { ... }
3 
4// this value has a typo but will still run
5dont_do_this('closet');
6 
7// lovely
8function do_this(DoorState $state) { ... }
9 
10// this typo will make your IDE shout at you "Constant 'Closet' not found in DoorState"
11do_this(DoorState::Closet) ;

Stay up-to-date with Jump24

Every month we send out a newsletter containing lots of interesting stuff for the modern developer.

Playing nicely together

As Match expressions are exhaustive and Enums are a closed set of values, if we don’t handle all the cases like this:

php
1enum DoorState {
2 case Open;
3 case Closed;
4 case Locked;
5}
6 
7final class Door {
8 public DoorState $state;
9 public function checkState(): string
10 {
11 return match ($this->state) {
12 DoorState::Open => 'is open',
13 DoorState::Closed => 'is closed',
14 // doh - we forgot to handle locked!
15 };
16 }
17}

Again our IDE / Static Analysis will shout at us “Match expression does not handle remaining value: DoorState::Locked”. You can fix this by handling the missing case or adding a default arm:

php
1match ($this-state) {
2 DoorState::Open => 'is open',
3 DoorState::Closed => 'is closed',
4 default => 'the door is neither open nor closed',
5};

Always another thing to learn

These two features have been in PHP for awhile so using them has become commonplace for me, however there’s always something new to learn in programming. In a recent project with a multi-step upload where each step will call different actions depending on the file type I started using this approach where a related action could be handled by another method on the action class:

“Job chaining allows you to specify a list of queued jobs that should be run in sequence after the primary job has executed successfully”

sjksdfkds

This works but we love writing tests for our code right? These actions can be quite complicated and testing individual class methods can require a lot of setup related to other things on that class that a unit test for that specific method isn’t actually testing. For example we don’t want to fake a file or file upload in a unit test that is only testing specific data flows from the bigger upload process. So it was better to refactor to instantiating separate actions in each arm instead like this:

php
1match ($file->type) {
2 FileType::Video => (new HandleVideoAction)->execute($file),
3 FileType::Image => (new HandleImageAction)->execute($file),
4 FileType::Document => (new HandleDocumentAction)->execute($file),
5 default => throw new RuntimeException('Unknown file type'),
6};

Now we can have simpler unit tests for the separate action classes, nice. But… in some cases I wanted to do more before calling another action. For example, create a DTO from validated file data specific to the action to process and save it, something like:

php
1if ($fileType === FileType::Video) {
2 // build dto
3 $dto = StoreVideoMetadataDTO::fromRequestData($request->validated());
4 // pass dto to action
5 $result = (new SaveVideoMetadataAction))->execute($dto);
6}

As PHP doesn’t [yet] support multi-line short closures I didn’t think it was possible to inline this in the match arm. I was wrong as PHP has supported Immediately Invoked Function Execution aka IFFE (Iffy!) since PHP 7, so we can combine this functionality to do this:

php
1match ($file->type) {
2 FileType::Video => (function ($request) {
3 $dto = StoreVideoMetadataDTO::fromRequestData($request->validated());
4 return (new SaveVideoMetadataAction))->execute($dto);
5 })($request),
6 // handle other cases...
7};

Whilst the syntax is a little verbose, this feels like a really solution nice to me and great to have another option to reach for.

Wishful thinking

That being said I do still hope that some day we will be able to do this:

php
1// potential multi-line short closure syntax
2match ($file->type) {
3 FileType::Video => fn () => {
4 $dto = StoreVideoMetadataDTO::fromRequestData($request->validated());
5 return (new SaveVideoMetadataAction))->execute($dto);
6 },
7 // handle other cases...
8};

Or even this:

php
1// simple scope block syntax
2match ($file->type) {
3 FileType::Video => {
4 $dto = StoreVideoMetadataDTO::fromRequestData($request->validated());
5 return (new SaveVideoMetadataAction))->execute($dto);
6 },
7 // handle other cases...
8};

Blurred lines

As much as I love the Match + Enum combination I recently found a case where I preferred not to use it. Here we have a Laravel collection and are filtering it. The filter method will keep the values that return true from the callback:

php
1$filtered = $collection->filter(function (string $value): bool {
2 return match(true) {
3 $value === FeatureFlagEnum::OptionOne && FeatureFlagEnum::OptionOne->isNotActive(),
4 $value === FeatureFlagEnum::OptionTwo && FeatureFlagEnum::OptionTwo->isNotActive() => false,
5 default => true,
6 };
7});

It is possible to group the conditions that will return false by comma separating them but this felt like a step too far as more conditions and more complicated conditions may have to be added. Therefore I fell back to using a standard guard clause like this:

php
1$filtered = $collection->filter(function (string $value): bool {
2 if (
3 $value === FeatureFlagEnum::OptionOne && FeatureFlagEnum::OptionOne->isNotActive()
4 || $value === FeatureFlagEnum::OptionTwo && FeatureFlagEnum::OptionTwo->isNotActive()
5 ) {
6 return false;
7 }
8 return true;
9});

What do you think? What are your favourite ways to use Enums and Match in PHP? What’s your favourite modern PHP feature?

Looking to start your next Laravel project?

We’re a passionate, happy team and we care about creating top-quality web applications that stand out from the crowd. Let our skilled development team work with you to bring your ideas to life! Get in touch today and let’s get started!

Get in touch

Laravel Partner

Since 2014, we’ve built, managed and upgraded Laravel applications of all types and scales with clients from across the globe. We are one of Europe’s leading Laravel development agencies and proud to support the framework as an official Laravel Partner.

Vue Experts

We use Vue alongside Laravel on many of our projects. We have grown our team of experts and invested heavily in our knowledge of Vue over the last five years.