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: stringorenum Foo: int) attach a scalar value to every case, available as$case->value. PHP also gives backed enums two static lookup methods:from()andtryFrom().
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
enumblock into a special, final, non-instantiable class-like structure. - For every
caseline, PHP creates exactly one object of that type and exposes it as a class constant with the same name — which is whySuit::Heartsboth 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 fromUnitEnum, 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,
instanceofchecks 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
intorstring, 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/matchstatements scattered across the codebase every time you need to branch on a case. - Define a shared interface (like
HasLabelorHasColor) 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); reservefrom()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
matchexpressions 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
->valueor->namesince PHP array keys must be scalar.
Practice Exercises
- Exercise 1: Write a pure enum
Weekdaywith all seven days as cases, plus a methodisWeekend(): boolthat returnstrueonly for Saturday and Sunday. Loop overWeekday::cases()and print each day’s name next to whether it is a weekend. - Exercise 2: Write a backed enum
HttpStatus: intwith at least four cases (for example 200, 301, 404, 500) and a methodmessage(): stringreturning a short phrase for each (e.g.'OK','Not Found'). UsetryFrom(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 interfaceHasSymbolwith asymbol(): stringmethod. Then write a functionformatPrice(float $amount, Currency $currency): stringthat 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 (: stringor: int) additionally carry a scalarvalue. - Every enum implements
UnitEnum(givingcases()andname); backed enums also implementBackedEnum(givingfrom()andtryFrom()). - Enums can implement interfaces, use traits, define constants, and declare instance/static methods — but cannot be instantiated with
new, cannotextenda class, and cannot hold mutable instance properties. - Use
from()for trusted values andtryFrom()for untrusted input to avoid an uncaughtValueError. - Backed enum case values must be unique, or the enum fails to compile.
- Enum cases cannot be used directly as array keys — use
->valueor->name.
