Enumerations
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05; the SortDirection example was run on the PHP 8.6
release candidate. An enumeration (8.1) defines a type with a fixed, closed set of possible values, called
cases. Enums replace the class-constant idiom (const FORMAT_EBOOK = 'eb';), which allowed any string to slip
through a parameter typed string.
Pure enums
A pure enum has cases with no associated value. Each case is a singleton object of the enum’s type, so cases are
compared with === and used as types in declarations:
<?php
declare(strict_types=1);
enum LoanStatus
{
case Active;
case Returned;
case Overdue;
}
function canRenew(LoanStatus $status): bool // only the three cases are accepted
{
return $status === LoanStatus::Active;
}
var_dump(canRenew(LoanStatus::Active)); // bool(true)
var_dump(LoanStatus::Overdue->name); // string(7) "Overdue"
var_dump(LoanStatus::Active instanceof LoanStatus); // bool(true)
var_dump(count(LoanStatus::cases())); // int(3) -- cases() lists them in declaration order
Source: Enumerations basics
Backed enums
A backed enum associates each case with a unique scalar value of a single type, int or string, declared after
the enum name. Backed enums are the bridge to the outside world — databases, JSON, forms — and gain two static
methods:
-
from($value)returns the case or throwsValueErrorif the value is unknown; -
tryFrom($value)returns the case ornull.
<?php
declare(strict_types=1);
enum Format: string
{
case Hardcover = 'hc';
case Paperback = 'pb';
case Ebook = 'eb';
}
$fromDatabase = 'eb';
$format = Format::from($fromDatabase);
var_dump($format, $format->value, $format->name); // enum(Format::Ebook) "eb" "Ebook"
var_dump(Format::tryFrom('audio')); // NULL
try {
Format::from('audio');
} catch (ValueError $e) {
echo $e->getMessage(), "\n"; // "audio" is not a valid backing value for enum Format
}
$fromForm = $_POST['format'] ?? 'xx';
$chosen = Format::tryFrom($fromForm) ?? Format::Paperback; // safe default for user input
echo $chosen->name, "\n"; // Paperback
Backing values must be literals or constant expressions, unique within the enum; a backed enum cannot mix types, and
all cases must have a value. Pure enums have no value, from() or tryFrom().
Source: Backed enumerations
Methods, static methods and constants
Enums can have methods (with $this being the case), static methods and constants, including constants that alias
cases. They cannot have properties (other than the read-only name and value).
<?php
declare(strict_types=1);
enum Format: string
{
case Hardcover = 'hc';
case Paperback = 'pb';
case Ebook = 'eb';
public const self DEFAULT = self::Paperback; // a typed constant aliasing a case
public function loanDays(): int
{
return match ($this) {
self::Hardcover, self::Paperback => 21,
self::Ebook => 14,
};
}
public function label(): string
{
return match ($this) {
self::Hardcover => 'Hardcover',
self::Paperback => 'Paperback',
self::Ebook => 'E-book',
};
}
public static function physical(): array
{
return array_values(array_filter(self::cases(), fn (self $f): bool => $f !== self::Ebook));
}
}
echo Format::Ebook->label(), ': ', Format::Ebook->loanDays(), " days\n"; // E-book: 14 days
echo Format::DEFAULT->label(), "\n"; // Paperback
echo implode(', ', array_map(fn (Format $f): string => $f->label(), Format::physical())), "\n";
The match ($this) pattern is the heart of enum design: behaviour lives next to the cases, and because match throws
UnhandledMatchError on a missing arm (and static analysers report non-exhaustive matches), adding a case forces every
match to be updated.
Source: Enumeration methods, Static methods and Constants
Interfaces and traits
Enums can implement interfaces, and can use traits that contain no properties. All enums automatically implement
UnitEnum (with cases()); backed enums also implement BackedEnum (with from() and tryFrom()). Code can
accept "any enum" with these types.
<?php
declare(strict_types=1);
interface HasLabel
{
public function label(): string;
}
trait LabelFromName
{
public function label(): string
{
return ucwords(strtolower(preg_replace('/(?<!^)[A-Z]/', ' $0', $this->name)));
}
}
enum Genre: string implements HasLabel
{
use LabelFromName;
case ScienceFiction = 'sf';
case Poetry = 'poetry';
case HistoricalNovel = 'hist';
}
/** @param class-string<BackedEnum&HasLabel> $enum @return array<string, string> */
function options(string $enum): array
{
$result = [];
foreach ($enum::cases() as $case) {
$result[$case->value] = $case->label(); // e.g. for an HTML <select>
}
return $result;
}
print_r(options(Genre::class));
var_dump(Genre::Poetry instanceof UnitEnum, Genre::Poetry instanceof BackedEnum); // bool(true) bool(true)
Array
(
[sf] => Science Fiction
[poetry] => Poetry
[hist] => Historical Novel
)
bool(true)
bool(true)
Source: Traits and The BackedEnum interface
Enums in constant expressions and match
Enum cases are constant expressions: they can be used as parameter defaults, property defaults, class constants,
attribute arguments and array keys of backed values (not the cases themselves, which are objects). Since 8.2, ->
can read name and value in constant expressions too.
<?php
declare(strict_types=1);
enum Format: string { case Paperback = 'pb'; case Ebook = 'eb'; }
final class LoanRequest
{
public const string DEFAULT_CODE = Format::Paperback->value; // 8.2: ->value in a constant expression
public function __construct(public Format $format = Format::Paperback) {} // case as a default
}
$fees = [Format::Ebook->value => 0, Format::Paperback->value => 50]; // backed values as array keys
echo $fees[(new LoanRequest())->format->value], ' ', LoanRequest::DEFAULT_CODE, "\n"; // 50 pb
// Cases themselves cannot be array keys, but WeakMap and SplObjectStorage accept them
$counts = new WeakMap();
$counts[Format::Ebook] = 3;
echo $counts[Format::Ebook], "\n"; // 3
Source: Enumeration constant expressions
Serialisation and JSON
-
serialize()writes enum cases in a dedicated format (E:12:"Format:Ebook";), andunserialize()returns the same singleton, so===still holds. Unserialising a case that no longer exists throws. -
json_encode()writes a backed enum as its value and throws (or fails) for pure enums, which have no scalar form. -
var_export()writes\Format::Ebook. -
Decoding is always explicit:
Format::from($json['format']).
<?php
declare(strict_types=1);
enum Format: string { case Ebook = 'eb'; }
enum LoanStatus { case Active; }
var_dump(json_encode(['title' => 'Kindred', 'format' => Format::Ebook])); // {"title":"Kindred","format":"eb"}
var_dump(unserialize(serialize(Format::Ebook)) === Format::Ebook); // bool(true)
try {
json_encode(LoanStatus::Active, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo $e->getMessage(), "\n"; // Non-backed enums have no default serialization
}
Source: Serialization
How enums differ from objects
Enum cases are objects (gettype() is "object", Format::Ebook::class is "Format"), but with restrictions that
keep them singletons and values:
-
they cannot be instantiated with
new, cloned, or extended; -
they have no state: no properties, no constructor or destructor, and none of the magic methods except
__call,__callStaticand__invoke(upcoming in PHP 8.6,__debugInfo()is allowed too); -
comparison operators other than
===/==are meaningless:<and>always returnfalse; -
constants referring to cases and the cases themselves can be read with
constant('Format::Ebook')orFormat::{'Ebook'}(8.3).
<?php
declare(strict_types=1);
enum Format: string { case Hardcover = 'hc'; case Ebook = 'eb'; }
foreach (['clone', 'new'] as $attempt) {
try {
$x = $attempt === 'clone' ? clone Format::Ebook : new Format();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
var_dump(Format::Ebook < Format::Hardcover, Format::Ebook > Format::Hardcover); // bool(false) bool(false)
var_dump((new ReflectionEnum(Format::class))->getBackingType()?->getName()); // string(6) "string"
Trying to clone an uncloneable object of class Format
Cannot instantiate enum Format
bool(false)
bool(false)
string(6) "string"
Source: Differences from objects and ReflectionEnum
Enums at the edges: forms, input and class names
Most enum bugs happen where a value enters or leaves the program: an HTML form, a query string, a framework that
only knows the enum’s class name. Bookshelf’s shelves are numbered, so Shelf is backed by int, and everything
from a request is a string:
<?php
declare(strict_types=1);
enum Format: string
{
case Hardcover = 'hc';
case Paperback = 'pb';
case Ebook = 'eb';
}
enum Shelf: int
{
case Fiction = 1;
case Poetry = 2;
}
// a <select> from the cases: name and value are readonly properties
print_r(array_column(Format::cases(), 'name', 'value'));
// cases are never equal to their values, even with ==
var_dump(Format::Ebook == 'eb', Format::Ebook->value === 'eb');
// input arrives as strings: an int-backed enum needs a cast first under strict_types
try {
Shelf::from($_GET['shelf'] ?? '2');
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
var_dump(Shelf::tryFrom((int) '2'), Shelf::tryFrom((int) 'poetry'));
$raw = '2 ';
var_dump(filter_var($raw, FILTER_VALIDATE_INT), Shelf::tryFrom(filter_var($raw, FILTER_VALIDATE_INT) ?: 0));
// working from a class name, as a framework does
function castTo(string $enum, int|string $value): ?BackedEnum
{
if (! enum_exists($enum) || ! is_subclass_of($enum, BackedEnum::class)) {
throw new InvalidArgumentException("$enum is not a backed enum");
}
return $enum::tryFrom($value);
}
var_dump(castTo(Format::class, 'hc'), Format::Hardcover instanceof UnitEnum, Format::Hardcover instanceof BackedEnum);
var_dump((new ReflectionEnum(Shelf::class))->getCase('Poetry')->getBackingValue());
Array
(
[hc] => Hardcover
[pb] => Paperback
[eb] => Ebook
)
bool(false)
bool(true)
TypeError: Shelf::from(): Argument #1 ($value) must be of type int, string given
enum(Shelf::Poetry)
NULL
int(2)
enum(Shelf::Poetry)
enum(Format::Hardcover)
bool(true)
bool(true)
int(2)
Source: Backed enumerations, array_column, enum_exists and ReflectionEnumBackedCase
-
array_column()readsnameandvaluebecause they are public readonly properties, so the options of a<select>(value to label) take one call. With alabel()method,array_map(fn (Format $f) => $f->label(), ...)overcases()does the same. -
A case is an object, never its value.
Format::Ebook == 'eb'isfalseeven with loose comparison, so code that compares a database column to a case must compare->value, or convert the column withfrom()first. -
from()andtryFrom()follow the type rules of an ordinaryintparameter. Withstrict_types, the string'2'from a query string is aTypeError, not a lookup failure, andtryFrom()does not catch it. Withoutstrict_types,'2'is coerced and works, but'2 books'and'poetry'are still aTypeError, and an unknown7is aValueError. Validating withfilter_var(..., FILTER_VALIDATE_INT)first is safer than an(int)cast: the cast turns'poetry'into0, which would silently match a case backed by0. -
Generic code works from the class name.
enum_exists()istrueonly for enums (class_exists()istruefor them too), every enum implementsUnitEnum, backed ones alsoBackedEnum, and$enum::tryFrom()works on a class-name string. This is how frameworks cast request and database values to enums; Laravel’senumcasts andRule::enum()are built on it (Eloquent Casts, Accessors and Serialization). -
ReflectionEnumlists cases with their backing values without callingcases(), which documentation generators and form builders use.
Built-in enums
PHP itself increasingly uses enums in its APIs:
-
RoundingMode(8.4), the third argument ofround()and ofBcMath\Number::round():HalfAwayFromZero(the default),HalfTowardsZero,HalfEven(banker’s rounding),HalfOdd,TowardsZero,AwayFromZero,NegativeInfinityandPositiveInfinity. -
Random\IntervalBoundary(8.3) forRandomizer::getFloat(). -
Uri\UriComparisonModeand related enums in the URI extension (8.5). -
SortDirection(upcoming in 8.6) withAscendingandDescending.
<?php
declare(strict_types=1);
var_dump(round(2.5), round(2.5, 0, RoundingMode::HalfEven), round(3.5, 0, RoundingMode::HalfEven)); // 3 2 4
var_dump(round(-2.5, 0, RoundingMode::AwayFromZero), round(2.7, 0, RoundingMode::TowardsZero)); // -3 2
Source: The RoundingMode enum and round
<?php
declare(strict_types=1);
// requires PHP 8.6
var_dump(array_map(fn (SortDirection $d): string => $d->name, SortDirection::cases())); // ["Ascending", "Descending"]
Source: RFC: SortDirection enum
Where older books and tutorials are outdated
None of the books consulted for this section covers enums, which arrived in PHP 8.1. They represent closed sets of
values with class constants (const STATUS_ACTIVE = 1;) and validate them by hand, or with SplEnum from the PECL
SPL_Types extension, which never became part of PHP. Use native enums, and tryFrom() for validating input.