PHP Core

Boost PHP Code Quality: A Guide to Using Enums in PHP 8.1

Learn practical techniques to replace constants with enums for better code efficiency and safety

Daniel Archer

Hey there, PHP enthusiasts! Today, we're going to explore one of the most powerful features that PHP 8.1 brought to the table: enums. If you're not familiar with enums, don't worry - we're going to break it all down together.

Enums, short for enumerations, are a powerful feature introduced in PHP 8.1 that allow developers to define a set of named constants. They represent a significant improvement in PHP’s type system, offering a way to create more expressive, self-documenting, and type-safe code. In this article, we will take a deep dive into enums in PHP, exploring their benefits, use cases, and how they can dramatically improve your code quality and maintainability.

What are Enums?

Enums are a special kind of class used to represent a fixed set of constant values. These values are typically used to represent a collection of related items, states, or options within a system. For example, you might use an enum to represent the days of the week, card suits in a deck, or status codes in an application. Key enums characteristics include:

  • Type safety: Enums provide compile-time checking, ensuring that only valid enum values are used.
  • Self-documentation: The enum’s name and cases clearly communicate the intent and possible values.
  • Extensibility: Enums can be enriched with methods and properties, allowing for more complex behavior.

IPC NEWSLETTER

All news about PHP and web development

 

Before Enums in PHP

Before PHP 8.1, there was no built-in support for enums in PHP. Developers had to use constants or classes to define state and workflow. This approach was not ideal as can see in the following example, where most of the work needs to be done manually for any validation or transformation.

  class Status {
      const PENDING = 'pending';
      const APPROVED = 'approved';
      const REJECTED = 'rejected';
      
      public function __construct($status) {
          /*...*/ 
      }
      
      public function all() {
          return [
              self::PENDING,
              self::APPROVED,
              self::REJECTED
          ];
      }
  }

Other forms of implementation include using interfaces, abstract classes, traits, or even arrays. However, these approaches are not as clean and readable as enums.

  // Arrays
  $status = [
      'PENDING' => 'pending',
      'APPROVED' => 'approved',
      'REJECTED' => 'rejected'
  ];
  
  // Interfaces, traits or abstract classes
  interface Status {
      const PENDING = 'pending';
      const APPROVED = 'approved';
      const REJECTED = 'rejected';
  }
  
  // SplEnum (https://pecl.php.net/package/SPL_Types - not supported since php 7)
  class Status extends SplEnum {
      const PENDING = 'pending';
      const APPROVED = 'approved';
      const REJECTED = 'rejected';
  }

YOU LOVE PHP?

Explore the PHP Core Track

 

Pure Enums or Backed Enums

There are two types of enums in PHP: Pure Enums and Backed Enums. In the first case, pure enums are a set of named constants without a value associated with them. In the second case, backed enums are a set of named constants with a value scalar type associated with them.

Example:

  // Pure Enum
  enum Status {
      case Pending;
      case Approved;
      case Rejected;
  }
  
  // Backed Enum
  enum Status: string {
      case Pending = 'pending';
      case Approved = 'approved';
      case Rejected = 'rejected';
  }

“When should I use one over the other?”, you might be asking. If you need to store the value or retrieve and build your enum from a value, you should use a backed enum. If you just need a set of named constants to represent a temporary state or a workflow, pure enums are the way to go.

Using Enums

We have a few ways to use enums in PHP. The first one is by using the match expression. This is a new feature in PHP 8.0 that allows you to match a value against a set of patterns. In the case of enums, you can match a value against the enum cases.

  enum Status {
      case Pending;
      case Approved;
      case Rejected;
  }
  
  class Order {
      public Status $status;
      
      public function __construct(Status $status) {
          $this->status = $status;
      }
      
      public function approve() {
          $this->status = Status::Approved;
      }
      
      public function process() {
          match($this->status) {
              Status::Pending => $this->processPending(),
              Status::Approved => $this->processApproved(),
              Status::Rejected => $this->processRejected(),
          };
      }
  }

Accessing enum values

It’s important to note that when retrieving the values of the enum, this method ::ATTR will return the instance of the enum, not the value itself and because of that it will throw an exception in case you try to use it as a string. To get the value, you need to call the ->value method.

Unfortunately, you can’t use either __toString or __invoke magic methods to get the value directly. See https://wiki.php.net/rfc/enumerations#magic_read-methods.

  enum Status: string {
      case Pending = 'pending';
      case Approved = 'approved';
      case Rejected = 'rejected';
  }
  echo Status::Pending; // Fatal error: Uncaught Error: Object of class Status could not be converted to string ...
  echo Status::Pending->value; // pending
  echo Status::Pending->name; // Pending (the name of the case)

 

Enriched Enums

You can also enrich your enums with methods that can be called on each case. This is a great way to encapsulate logic. In the next example we will create a VirtualQueueStatus enum also including methods to retrieve the title and the css class for each status.

  enum VirtualQueueStatus: string {
      case BeingServed = 'being_server';
      case Completed = 'completed';
      case NoShow = 'no_show';
      case Removed = 'removed';
      case Abandoned = 'abandoned';
  
      public function title(): string {
          return match ($this) {
              self::BeingServed => 'Being served',
              self::Completed => 'Completed',
              self::NoShow => 'No show',
              self::Removed => 'Removed',
              self::Abandoned => 'User left',
          };
      }
  
      public function cssClass(): string {
          return match ($this) {
              self::BeingServed => 'bg-yellow-200 text-yellow-700',
              self::Completed => 'bg-green-200 text-green-700',
              self::NoShow => 'bg-red-200 text-red-700',
              self::Removed => 'bg-red-200 text-red-700',
              self::Abandoned => 'bg-red-200 text-red-700',
          };
      }
  }
  
  class MyQueue {
      public function __construct(public VirtualQueueStatus $status) {}
  }
  
  $status = 'being_server'; // from database
  $queue = new MyQueue(status: VirtualQueueStatus::from($status));
  echo $queue->status->title(); // Being served

Using ::from method, you can build your enum from a value. This is a great way to hydrate your enums from a database, where you almost certainly have a valid value. But when building from a user’s input you may use a ::tryFrom method that will return null if the value is not valid.

  $status = 'anything'; // from user input
  // in case of invalid value, it will return null and the default value will be set
  $queue = new MyQueue(status: VirtualQueueStatus::tryFrom($status) ?? VirtualQueueStatus::BeingServed);
  echo $queue->status->title(); // Being served
  Another great usage of enums is to list all possible values for a given scenario, like a dropdown in a form.
  // ['being_server', 'completed', 'no_show', 'removed', 'abandoned']
  foreach (VirtualQueueStatus::all() as $status) {
      echo $status; // being_server, completed, no_show, removed, abandoned
  }

Let’s implement another method where we can check if this is a final state and another one to retrieve only error states.

  enum VirtualQueueStatus: string {
      /* ... */
  
      public static function isFinal(VirtualQueueStatus $status): bool {
          return match ($status) {
              self::Completed,
              self::NoShow,
              self::Removed,
              self::Abandoned => true,
              default => false,
          };
      }
  
      public static function errorStates(): array {
          return [
              self::NoShow,
              self::Removed,
              self::Abandoned,
          ];
      }
  }
  
  foreach (VirtualQueueStatus::errorStates() as $status) {
      echo $status; // no_show, removed, abandoned
  }

IPC NEWSLETTER

All news about PHP and web development

 

Conclusion

Powerful and flexible, enums are a great addition to PHP. You can use enums anywhere you need a set of named constants, in my team we are constantly replacing old constants with enums. They are a great way to make your code more readable and maintainable.

You don’t need to worry about the performance of enums, as they are compiled to a very efficient structure just like any other class object internally.

Many of the examples in this article are inspired by the official documentation and real work projects, you can find more information in the official documentation or in the RFC.

 

Top Articles About PHP Core

Stay tuned!

Register for our newsletter

Behind the Tracks of IPC

PHP Core
Best practices & applications

General Web Development
Broader web development topics

Test & Performance
Software testing and performance improvements

Agile & People
Getting agile right is so important

Software Architecture
All about PHP frameworks, concepts &
environments

DevOps & Deployment
Learn about DevOps and transform your development pipeline

Content Management Systems
Sessions on content management systems

#slideless (pure coding)
See how technology really works

Web Security
All about
web security

PUSH YOUR CODE FURTHER