Enums (PHP 8.1)

Reviewed & published by Brayan K

Stop passing fragile magic strings like "published" around your app. PHP 8.1 enums give you a fixed, type-safe set of named values — with their own methods — so invalid states become impossible and your editor can autocomplete every option.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ A Pure Enum

The simplest enum lists a handful of named cases and nothing else. Each case is a singleton object of the enum's type, so you can compare them with === and check them with instanceof. Reading ->name gives you the case name as a string. This is called a pure enum because the cases carry no scalar value.

<?php
// An enum is a type with a FIXED, named set of possible values.
// Before PHP 8.1 people used loose class constants or magic strings;
// an enum makes the whole set explicit and impossible to typo past.

enum Suit {
    case Hearts;     // each "case" is one allowed value
    case Diamonds;
    case Clubs;
    case Spades;
}

// A case is a singleton object of the enum type.
$card = Suit::Hearts;

echo $card->name . "\n";                 // Hearts  — the case name as a string
var_dump($card === Suit::Hearts);        // bool(true)  — same singleton
var_dump($card instanceof Suit);         // bool(true)  — it's a Suit
?>

2️⃣ Backed Enums

Most real-world enums need a value you can store or transmit — the string 'published' that goes in a database column, or an integer code. A backed enum declares a type after the name (: string or : int) and gives every case a value. You read it with ->value, and you convert back from a raw value using from() (throws on a bad value) or tryFrom() (returns null).

<?php
// A BACKED enum gives every case a scalar value (int OR string, never mixed).
// Perfect for the value you store in a database or send over the wire.

enum Status: string {
    case Draft     = 'draft';
    case Published = 'published';
    case Archived  = 'archived';
}

$s = Status::Published;
echo $s->value . "\n";                  // published  — the backing value

// from()   : turn a raw value INTO a case, or throw if it's invalid.
$loaded = Status::from('archived');
echo $loaded->name . "\n";             // Archived

// tryFrom(): same, but returns null instead of throwing on a bad value.
var_dump(Status::tryFrom('nope'));      // NULL  — no exception
?>

A backed enum's values must all be the same scalar type and must be unique — two cases can't share a value, and you can't mix ints and strings. This is what makes a backed enum a perfect, lossless bridge between your code and a database column.

3️⃣ Methods on an Enum

Enums aren't just data — they can hold methods, which makes them an excellent home for behaviour that depends on the value. Inside an instance method, $this is the current case, so pairing a method with match($this) gives you clean, exhaustive logic. Enums can also have static methods (handy as named constructors), implement interfaces, and declare constants.

<?php
// Enums can hold METHODS — they're a great home for behaviour tied to a value.
// They may also implement interfaces and use constants.

enum Priority: int {
    case Low    = 1;
    case Medium = 2;
    case High   = 3;

    // An instance method: $this is the current case.
    public function label(): string {
        return match($this) {
            Priority::Low    => 'Low priority',
            Priority::Medium => 'Medium priority',
            Priority::High   => 'High priority — handle now',
        };
    }

    // A static method on the enum itself.
    public static function fromScore(int $score): self {
        return $score >= 8 ? self::High
             : ($score >= 4 ? self::Medium : self::Low);
    }
}

$p = Priority::fromScore(9);
echo $p->name  . "\n";       // High
echo $p->value . "\n";       // 3
echo $p->label() . "\n";     // High priority — handle now
?>

4️⃣ Listing Cases with cases()

Every enum gets a free static cases() method that returns all its cases as an array, in declaration order. This is the idiomatic way to build a <select> dropdown, validate that an input is one of the allowed options, or simply iterate over every possibility.

<?php
// Every enum gets a built-in cases() method: an array of ALL its cases,
// in declaration order. Great for building dropdowns or validating input.

enum Weekday: string {
    case Mon = 'Monday';
    case Tue = 'Tuesday';
    case Wed = 'Wednesday';
}

foreach (Weekday::cases() as $day) {
    echo $day->name . ' => ' . $day->value . "\n";
}

echo 'Total cases: ' . count(Weekday::cases()) . "\n";
?>

Now you try — fill in each ___ using the 👉 hint, then run it and check against the Output panel.

<?php
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.

enum Direction: string {
    case North = ___;   // 👉 a string value 'N'
    case South = 'S';
    case East  = 'E';
    case West  = 'W';
}

$d = Direction::___;       // 👉 pick the East case
echo $d->name  . "\n";    // should print: East
echo $d->value . "\n";    // should print: E

// from() should turn 'W' back into a case:
$w = Direction::___('W');  // 👉 the method that throws on a bad value
echo $w->name . "\n";     // should print: West

// ✅ Expected output:
//    East
//    E
//    West
?>

🧩 Reorder Challenge

These lines define a backed enum and use it — but they're scrambled. Put them in the order PHP needs to run without error.

B → D → C → E → A

The enum must be fully declared first: open it (B), list the case (D), then close the body (C). Only then can you reference a case (E) and read its ->value (A), which prints admin.

🧠 Quick Recall — Predict the Output

L large — ->name is the case name, ->value is the backing string.

2. What does this print?

NULL — tryFrom() returns null for an unknown value instead of throwing. (from('off') would throw a ValueError.)

3. True or false?

bool(true) — every case is a singleton, so the same case is always identical to itself with ===.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Enums

SyntaxExampleWhat It Does
enum X { ... }enum Suit { case Hearts; }Declare a pure enum
enum X: stringcase Draft = 'draft';Backed enum with values
->nameStatus::Draft->nameCase name as a string
->valueStatus::Draft->valueBacking value (backed only)
X::from()Status::from('draft')Value → case (throws if bad)
X::tryFrom()Status::tryFrom($x)Value → case or null
X::cases()Status::cases()Array of all cases

Mini-Challenge: HTTP Status Helper

No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments.

<?php
// 🎯 MINI-CHALLENGE: an HTTP status helper.
// No code is filled in — work from the steps below, then run it.
//
// 1. Make a BACKED enum HttpStatus: int with cases:
//       Ok = 200, NotFound = 404, ServerError = 500
// 2. Add a method isError(): bool that returns true when the
//    backing value is >= 400  (use $this->value).
// 3. Add a method message(): string using match($this) to map
//    each case to text, e.g. Ok => 'OK', NotFound => 'Not Found'.
// 4. Take HttpStatus::from(404), then echo:
//       Code:    404
//       Message: Not Found
//       Error?:  yes
//
// ✅ Expected output:
//    Code:    404
//    Message: Not Found
//    Error?:  yes

// your code here
?>

🎉 Lesson Complete!

Practice quiz

What is the difference between a pure enum and a backed enum?

  • A pure enum is faster
  • A pure enum can hold properties
  • A backed enum gives every case a scalar value (->value); a pure enum has only ->name
  • There is no difference in PHP 8.1

Answer: A backed enum gives every case a scalar value (->value); a pure enum has only ->name. A backed enum (enum Status: string) attaches an int or string value to each case, exposed via ->value. A pure enum's cases are just named singletons with ->name.

Given enum Size: string { case S = 'small'; case L = 'large'; }, what does Size::L->name . ' ' . Size::L->value print?

  • L large
  • large large
  • L L
  • large L

Answer: L large. ->name is the case name (L) and ->value is the backing string (large), so it prints 'L large'.

What does Status::tryFrom('off') return when 'off' is not a valid backing value?

  • false
  • An empty string
  • It throws a ValueError
  • null

Answer: null. tryFrom() returns null for an unknown value instead of throwing, making it ideal for untrusted input.

What does from() do when given a value no case has?

  • Returns null
  • Throws a ValueError
  • Returns the first case
  • Returns false

Answer: Throws a ValueError. from() throws a ValueError on an invalid value — use it when an invalid value is a bug that should stop execution.

Which built-in method returns an array of all cases in declaration order?

  • cases()
  • all()
  • values()
  • toArray()

Answer: cases(). Every enum gets a static cases() method returning all its case objects in declaration order — great for dropdowns and validation.

What must be true about the backing values of a backed enum?

  • They can mix ints and strings freely
  • They must be sequential integers
  • They must all be the same scalar type and be unique
  • They are optional

Answer: They must all be the same scalar type and be unique. A backed enum's values must all be the same scalar type (all int or all string) and must be unique — no two cases can share a value.

How do you add per-case behaviour to an enum?

  • Give each case a mutable property
  • Define a method that uses match($this) to return the right value
  • Subclass the enum per case
  • Enums cannot have behaviour

Answer: Define a method that uses match($this) to return the right value. Enums can't hold mutable per-case state, but they can have methods. A common pattern is match($this) inside a method to map each case to a value.

Inside an enum's instance method, what does $this refer to?

  • The enum class
  • The backing value
  • Always the first case
  • The current case

Answer: The current case. In an instance method, $this is the current case, which is why pairing it with match($this) gives clean per-case logic.

For a pure enum Suit { case Hearts; }, what is var_dump(Suit::Hearts === Suit::Hearts)?

  • bool(false)
  • bool(true)
  • It throws an error
  • NULL

Answer: bool(true). Each case is a singleton object, so the same case is always identical to itself under ===, giving bool(true).

Which safe one-liner turns request data into an enum with a default fallback?

  • Status::from($input) ?? Status::Draft
  • Status::cases()[$input]
  • Status::tryFrom($input) ?? Status::Draft
  • new Status($input)

Answer: Status::tryFrom($input) ?? Status::Draft. tryFrom() returns null on an invalid value, so the ?? operator can supply a sensible default in one line.

Continue this course

Frequently asked questions

What's the difference between a pure enum and a backed enum in PHP?

A pure enum (enum Suit { case Hearts; }) has cases that are just named singleton objects with no scalar value — you can read ->name but there is no ->value. A backed enum (enum Status: string { case Draft = 'draft'; }) attaches an int or string to every case, exposes ->value, and unlocks from()/tryFrom() for converting raw values into cases. Use a backed enum whenever the value must be stored in a database, sent in JSON, or read from a form; use a pure enum when the set of names is all you need.

When should I use from() versus tryFrom()?

Both convert a raw scalar into an enum case. from() throws a ValueError if the value doesn't match any case — use it when an invalid value is a bug that should stop execution. tryFrom() returns null instead of throwing — use it for untrusted input (a query string, a form field) where you want to handle the 'not a valid value' case gracefully, for example return a 400 response. A common pattern is $status = Status::tryFrom($input) ?? Status::Draft to fall back to a default.

Can a PHP enum have properties to store extra data per case?

An enum case cannot have its own mutable properties — enums are not allowed to hold state, which is what keeps each case a true singleton. If you need extra data per case, expose it through a method that uses match($this) to return the right value, or implement an interface and define a method like color() or label(). The backing value (->value) is the one piece of scalar data a case carries directly.

How do I list every value of an enum?

Every enum automatically has a static cases() method that returns an array of all its case objects in declaration order. Loop over Weekday::cases() to build a dropdown, validate input, or count options. For a backed enum you can read ->value and ->name on each item. There is no built-in values()-only method, but array_map(fn($c) => $c->value, Weekday::cases()) gives you a plain array of the backing values.

Related lessons