PHP Enums

An enum (short for enumeration) is a special kind of class, introduced in PHP 8.1, that lets you define a fixed, closed set of possible values for something — like a status, a direction, or a card suit — instead of relying on loose strings, integers, or scattered class constants. Enums give you real type safety: a function that expects an OrderStatus can only ever receive one of the values you actually declared, never an arbitrary typo’d string.

This matters more than it looks. Before enums, PHP developers modeled a fixed set of options with class constants (const STATUS_PENDING = 'pending';) plus a raw string type hint, which meant nothing stopped you from passing 'panding' and getting a silent bug instead of an error. Enums close that gap: the value itself becomes a real, checkable type.

Overview: How Enums Work

Under the hood, an enum declaration is compiled by the Zend Engine into a special kind of class. Every case you write inside an enum becomes a single, globally shared object — not a new instance created each time you reference it, but the exact same object every time. That is why Suit::Spades === Suit::Spades is always true: you are comparing one singleton instance to itself, the same way true === true always holds. This singleton behavior is also why enums are safe and cheap to pass around, store in arrays, and compare with strict equality.

There are two flavors of enum:

  • Pure enums have cases that carry no scalar value of their own — only an identity (their name, and whatever methods you attach).
  • Backed enums (declared as enum Foo: string or enum Foo: int) attach a scalar value to every case, available as $case->value. PHP also gives backed enums two static lookup methods: from() and tryFrom().

Every enum, backed or not, implicitly implements the built-in UnitEnum interface, which is what gives every enum the cases() static method and the name property. Backed enums additionally implement BackedEnum, adding from() and tryFrom(). Beyond that, enums behave a lot like ordinary classes: they can implement your own interfaces, use traits, define class constants, and declare both instance and static methods. Three restrictions are deliberate and enforced by the engine: you cannot instantiate an enum with new, an enum cannot extend another class (though it can implement interfaces), and enum cases cannot hold mutable instance properties — only the fixed identity (and optional backing value) that was declared at compile time.

Syntax

The general shape of an enum declaration looks like this:

<?php

interface SomeInterface
{
    public function someMethod(): string;
}

enum EnumName: string implements SomeInterface
{
    case CaseOne = 'value1';
    case CaseTwo = 'value2';

    const SOME_CONSTANT = self::CaseOne;

    public function someMethod(): string
    {
        return match ($this) {
            self::CaseOne => 'First',
            self::CaseTwo => 'Second',
        };
    }
}

echo EnumName::CaseOne->someMethod() . "\n";
echo EnumName::SOME_CONSTANT->value . "\n";

Output:

First
value1
Part Meaning
enum EnumName Declares a new enum type, just like class declares a class.
: string or : int Optional — makes it a backed enum with that scalar type. Omit it for a pure enum.
implements SomeInterface Optional; an enum can implement any number of interfaces.
case CaseOne = 'value1'; A case. The = 'value1' part is required if the enum is backed, and forbidden if it is not.
const SOME_CONSTANT Class constants work exactly as in ordinary classes, and can even point to a case.
Methods Instance methods (using $this) and static methods are both allowed.

Examples

Example 1: A pure enum for card suits

<?php

enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

function suitColor(Suit $suit): string
{
    return match ($suit) {
        Suit::Hearts, Suit::Diamonds => 'Red',
        Suit::Clubs, Suit::Spades => 'Black',
    };
}

$card = Suit::Spades;

echo $card->name . "\n";
echo suitColor($card) . "\n";
echo ($card === Suit::Spades ? 'Same instance' : 'Different') . "\n";

Output:

Spades
Black
Same instance

This is a pure enum — there is no : string or : int after the enum name, so cases have no backing value, only a name. The match expression groups cases with a comma to share one result arm, and the strict comparison === confirms that $card is literally the same object as Suit::Spades, not merely an equal-looking copy.

Example 2: A backed enum implementing an interface

<?php

interface HasLabel
{
    public function label(): string;
}

enum OrderStatus: string implements HasLabel
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Awaiting Fulfillment',
            self::Shipped => 'On Its Way',
            self::Delivered => 'Delivered',
            self::Cancelled => 'Cancelled',
        };
    }

    public function isFinal(): bool
    {
        return $this === self::Delivered || $this === self::Cancelled;
    }
}

$status = OrderStatus::from('shipped');

echo $status->value . "\n";
echo $status->label() . "\n";
echo ($status->isFinal() ? 'Final' : 'Not final') . "\n";

$unknown = OrderStatus::tryFrom('lost');
echo ($unknown === null ? 'No matching case' : $unknown->value) . "\n";

Output:

shipped
On Its Way
Not final
No matching case

Here OrderStatus is backed by string, so every case carries a real value you can store in a database column or send over an API. from('shipped') looks up the matching case or throws a ValueError if nothing matches; tryFrom('lost') does the same lookup safely, returning null instead of throwing when the value doesn’t exist. The enum also implements HasLabel, proving that enums participate in your type system exactly like classes do.

Example 3: Iterating cases() and enum-returning methods

<?php

enum Direction: int
{
    case North = 0;
    case East = 90;
    case South = 180;
    case West = 270;

    const DEFAULT = self::North;

    public function opposite(): self
    {
        return self::from(($this->value + 180) % 360);
    }
}

foreach (Direction::cases() as $direction) {
    echo $direction->name . ' (' . $direction->value . ') -> opposite: ' . $direction->opposite()->name . "\n";
}

echo Direction::DEFAULT->name . "\n";

Output:

North (0) -> opposite: South
East (90) -> opposite: West
South (180) -> opposite: North
West (270) -> opposite: East
North

Direction::cases() returns every case in declaration order, which is perfect for building a dropdown or exhaustively testing every possibility. The opposite() method returns self — another case of the same enum — computed by re-looking-up a value with from(). Notice DEFAULT is a class constant whose value is itself an enum case, showing that constants and cases can reference each other freely.

How It Works Step by Step (Under the Hood)

  • PHP compiles the enum block into a special, final, non-instantiable class-like structure.
  • For every case line, PHP creates exactly one object of that type and exposes it as a class constant with the same name — which is why Suit::Hearts both looks and behaves like a constant, but is actually a singleton instance you can call methods on.
  • If the enum is backed, PHP validates at compile time that every case has a unique value of the declared scalar type, and builds an internal lookup table used by from()/tryFrom().
  • cases(), inherited from UnitEnum, simply returns that list of singleton objects in the order they were declared.
  • Because a case is a genuine object, you get full IDE autocompletion, instanceof checks against implemented interfaces, and strict === comparisons that always work correctly — unlike raw strings, which can accidentally match across unrelated contexts.
  • One catch: PHP array keys must be int or string, so an enum case cannot be used directly as an array key. Use $case->value (backed enums) or $case->name (any enum) as the key instead.

Common Mistakes

Mistake 1: Trying to add a mutable property to an enum

<?php

enum Status
{
    case Active;
    case Inactive;

    public string $label = 'default';
}

Enums are not allowed to declare instance properties — only cases, constants, and methods. This fails to compile with a fatal error. If you need per-case data, expose it through a method (often a match over $this) instead:

<?php

enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';

    public function label(): string
    {
        return ucfirst($this->value);
    }
}

echo Status::Active->label() . "\n";

Output:

Active

Mistake 2: Using from() on untrusted input

<?php

enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

function statusFromRequest(string $value): OrderStatus
{
    return OrderStatus::from($value);
}

$status = statusFromRequest('unknown');

echo $status->value . "\n";

Output:

PHP Fatal error: Uncaught ValueError: "unknown" is not a valid backing value for enum "OrderStatus"

from() throws when the value doesn’t match any case, which is exactly right when you already trust the value (say, it came from your own database column). But request input is never trustworthy — use tryFrom() and handle the null case explicitly:

<?php

enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

function statusFromRequest(string $value): ?OrderStatus
{
    return OrderStatus::tryFrom($value);
}

$status = statusFromRequest('unknown');

echo ($status === null ? 'Invalid status supplied' : $status->value) . "\n";

Output:

Invalid status supplied

Mistake 3: Duplicate backing values

<?php

enum Level: int
{
    case Low = 1;
    case Medium = 2;
    case High = 2;
}

Every case in a backed enum must have a unique value; PHP raises a fatal compile-time error the moment it sees a duplicate. Fix it by giving each case its own value:

<?php

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

echo Level::High->value . "\n";

Output:

3

Best Practices

  • Reach for a backed enum whenever you need to store, serialize, or transmit the value (database columns, JSON payloads, query strings). Use a pure enum when identity alone is enough.
  • Attach behavior directly to the enum with methods instead of writing external switch/match statements scattered across the codebase every time you need to branch on a case.
  • Define a shared interface (like HasLabel or HasColor) when several enums need the same kind of behavior, so calling code can type-hint the interface instead of a specific enum.
  • Prefer tryFrom() for any value coming from outside your program (HTTP input, query strings, third-party APIs); reserve from() for values you already know are valid.
  • Use cases() to build validation lists or HTML <select> options instead of hand-maintaining a parallel array of allowed values that can drift out of sync.
  • Keep match expressions over enum cases exhaustive (list every case explicitly). PHP won’t force this, but a static analyzer like PHPStan or Psalm will flag a missing arm as soon as a new case is added.
  • Never use an enum case directly as an array key — key by ->value or ->name since PHP array keys must be scalar.

Practice Exercises

  • Exercise 1: Write a pure enum Weekday with all seven days as cases, plus a method isWeekend(): bool that returns true only for Saturday and Sunday. Loop over Weekday::cases() and print each day’s name next to whether it is a weekend.
  • Exercise 2: Write a backed enum HttpStatus: int with at least four cases (for example 200, 301, 404, 500) and a method message(): string returning a short phrase for each (e.g. 'OK', 'Not Found'). Use tryFrom(418) and print a fallback message when no case matches.
  • Exercise 3: Write a backed string enum Currency (e.g. USD, EUR, GBP) implementing an interface HasSymbol with a symbol(): string method. Then write a function formatPrice(float $amount, Currency $currency): string that returns the amount prefixed with the correct symbol, such as '$19.99'.

Summary

  • Enums (PHP 8.1+) define a fixed, type-safe set of possible values, compiled into singleton objects — the same case instance is returned every time it is referenced.
  • Pure enums have only an identity (name); backed enums (: string or : int) additionally carry a scalar value.
  • Every enum implements UnitEnum (giving cases() and name); backed enums also implement BackedEnum (giving from() and tryFrom()).
  • Enums can implement interfaces, use traits, define constants, and declare instance/static methods — but cannot be instantiated with new, cannot extend a class, and cannot hold mutable instance properties.
  • Use from() for trusted values and tryFrom() for untrusted input to avoid an uncaught ValueError.
  • Backed enum case values must be unique, or the enum fails to compile.
  • Enum cases cannot be used directly as array keys — use ->value or ->name.