PHP: How do you use enums?

Status values, priorities, traffic light colours: again and again you need a fixed list of permitted values. Since PHP 8.1 there is a language construct for that, so you no longer have to make do with class constants.

The simple form

An enum without values is called a “pure enum”:

<?php
enum Status {
  case Open;
  case InProgress;
  case Done;
}

$s = Status::Open;
echo $s->name;   // Open
?>

Every case is an object of class Status. cases() gives you all of them:

Status::cases() : Offen, InArbeit, Erledigt

That is handy for filling a select field, for instance, without maintaining the list a second time.

The form with values

As soon as the value leaves your program — towards a database, JSON or a URL — you need a “backed enum”, that is one with values attached:

<?php
enum Priority: int {
  case Low    = 1;
  case Medium = 2;
  case High   = 3;
}

echo Priority::High->value;   // 3
?>

int and string are the permitted types. To get from the value back to the case you use from() or tryFrom():

Prioritaet::from(2)       : Mittel
Prioritaet::tryFrom(99)   : NULL
Prioritaet::from(99)      : ValueError
                            99 is not a valid backing value for enum Prioritaet

The difference matters. from() throws an exception, tryFrom() returns null. For values from a database that you trust, use from(). For user input, tryFrom():

<?php
  $prio = Priority::tryFrom((int) $_GET['prio']) ?? Priority::Medium;
?>

Enums can have methods

This is the point at which enums become considerably more than a list of constants:

<?php
enum TrafficLight: string {
  case Red    = 'red';
  case Yellow = 'yellow';
  case Green  = 'green';

  public function mayDrive(): bool {
    return match($this) {
      TrafficLight::Green => true,
      TrafficLight::Red, TrafficLight::Yellow => false,
    };
  }
}
?>
Rot    value=rot    darfFahren=false
Gelb   value=gelb   darfFahren=false
Gruen  value=gruen  darfFahren=true

The logic now lives where the values are defined, rather than scattered across if chains throughout half the application. Note also that match here needs no default — if a fourth case is added later, PHP tells you instead of quietly taking the wrong branch.

What you gain over class constants

Before PHP 8.1 you solved it like this:

<?php
class Status {
  const OPEN = 'open';
  const DONE = 'done';
}

function process(string $status): string {
  return "processing $status";
}
?>

The problem is in the signature. The parameter is ultimately a string, so any string at all gets through:

verarbeite gibtsnicht    <- kein Fehler, der Tippfehler faellt nicht auf

With an enum as the type, the language checks:

<?php
function process(TrafficLight $t): string {
  return "processing {$t->value}";
}
?>
String statt Enum -> TypeError

That is exactly the gain: an entire class of mistake disappears, because it can no longer be expressed.

Enums are singletons

Every case exists exactly once:

Status::Offen === Status::Offen   =>   true

That is why you can compare with === and why match($this) just works. You also cannot create an enum with new, nor clone one.

The JSON trap

A detail that is easy to trip over. A backed enum serialises to JSON without further ado, a pure enum does not:

json_encode(Ampel::Rot)      : "rot"
json_encode(Status::Offen)   : false
json_last_error_msg()        : Non-backed enums have no default serialization

The second value is the important one: json_encode() does not throw an exception, it returns false. If you do not check the return value you send an empty response and look for the bug in the wrong place. With the JSON_THROW_ON_ERROR flag you get a JsonException instead.

The practical rule that follows: as soon as a value leaves your program, use a backed enum.

Summary

  • Pure enum for purely internal states, backed enum as soon as the value goes outside.
  • from() throws, tryFrom() returns null — use tryFrom() for user input.
  • cases() returns all cases, handy for select fields.
  • Methods in the enum keep the logic next to the values.
  • The real gain is the type check, which constants do not give you.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).