Lesson 25 of 100
Classes12 min5 checksPHP 8.5

Enums: a fixed set of choices

Learn how PHP enums define a closed set of legal values, how backed enums map to strings, and how to convert untrusted input safely.

01 · THE IDEAREAD → TRY → LEARN

The problem enums solve

Many values in a program come from a small, closed list: an order is draft, published or archived, a card is a heart or a spade. Strings such as 'published' work, but a typo like 'publised' is still a valid string, so mistakes slip through quietly. An enum defines a new type with a fixed number of legal values. Anything outside that list simply is not an instance of the type.

Pure enums

An enum is declared with enum, and each legal value is a case. Enums share namespaces with classes, and each case is a single shared object of the enum type. That has some consequences:

  • Two references to the same case are identical under ===.
  • A case is never equal to a plain string with the same name.
  • Every case has a read-only name property holding its case-sensitive name.
  • Comparisons with < and > are not meaningful for enum cases.
Example
<?php
enum Suit
{
    case Hearts;
    case Spades;
}

$a = Suit::Spades;
$b = Suit::Spades;
echo $a->name, "\n";
echo json_encode($a === $b), "\n";
echo json_encode($a === 'Spades'), "\n";
Output
Spades
true
false

An enum whose cases carry no extra data is called a pure enum. It is a good fit when you only need to tell the options apart inside your own code.

Backed enums

Data often has to leave your program, for example in JSON, a form field or a stored record. For that, give the enum a scalar type and assign each case a value. This is a backed enum. Every case must have a value of the declared type (string or int), and values must be unique.

A backed enum gives you three useful tools:

  • ->value reads the scalar behind a case.
  • Status::from($x) returns the matching case, or throws ValueError if there is none.
  • Status::tryFrom($x) returns the matching case, or null if there is none.
  • Status::cases() returns a list of all cases in declaration order.
Example
<?php
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
}

echo Status::Draft->value, "\n";
echo Status::from('published')->name, "\n";
var_dump(Status::tryFrom('missing'));
echo json_encode(array_map(fn(Status $s) => $s->value, Status::cases())), "\n";
Output
draft
Published
NULL
["draft","published"]

Choosing from() or tryFrom()

Use from() when an unknown value means a bug, such as reading data your own code wrote. Use tryFrom() when the input is untrusted and an unknown value is an ordinary situation you want to handle. The nullsafe operator pairs nicely with it: $status?->value ?? 'none' handles the null case in one expression.

Normalise before converting. Matching is exact, so 'Draft' and ' draft' do not match 'draft'. Trimming and lowercasing the input first is a common step.

Type checking with enums

When a function parameter is typed as an enum, only cases of that enum can be passed. A string that merely looks right is rejected with a TypeError. This moves validation to the edge of your program: convert once, then pass real enum values everywhere else.

Example
<?php
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
}

function describe(Status $status): string
{
    return 'Status is ' . $status->value;
}

echo describe(Status::Published), "\n";

try {
    describe('draft');
} catch (TypeError $e) {
    echo "TypeError\n";
}

try {
    Status::from('nope');
} catch (ValueError $e) {
    echo "ValueError\n";
}
Output
Status is published
TypeError
ValueError

Adding behaviour

Because enums are class-like, they can have methods. A label() method that uses match ($this) keeps the display text next to the cases it describes, so there is one place to update when a case is added.

Exercise

Write statusLabel(string $input): string. The Status enum with cases Draft, Published and Archived (backed by 'draft', 'published', 'archived') is already in the starter. Trim and lowercase the input, convert it with tryFrom(), and return the case's label ('Draft', 'Published' or 'Archived'). For anything that is not a known status, including an empty string, return 'Unknown'. Tests check the returned string.

Recap

  • An enum is a type with a closed set of cases; each case is a single object.
  • Cases are not equal to strings, and they have a name property.
  • A backed enum adds value, from(), tryFrom() and cases().
  • Use tryFrom() for untrusted input and handle null.
  • Typed parameters reject anything that is not a case of the enum.
Before you start

Classes

You’ll practice
  • Define a backed enum
  • Safely convert input to a known enum value
Your task

Make statusLabel() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.