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

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 throws ValueError if the value is unknown;

  • tryFrom($value) returns the case or null.

<?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().

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.

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)

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

Serialisation and JSON

  • serialize() writes enum cases in a dedicated format (E:12:"Format:Ebook";), and unserialize() 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, __callStatic and __invoke (upcoming in PHP 8.6, __debugInfo() is allowed too);

  • comparison operators other than ===/== are meaningless: < and > always return false;

  • constants referring to cases and the cases themselves can be read with constant('Format::Ebook') or Format::{'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"

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)
  • array_column() reads name and value because they are public readonly properties, so the options of a <select> (value to label) take one call. With a label() method, array_map(fn (Format $f) => $f->label(), ...) over cases() does the same.

  • A case is an object, never its value. Format::Ebook == 'eb' is false even with loose comparison, so code that compares a database column to a case must compare ->value, or convert the column with from() first.

  • from() and tryFrom() follow the type rules of an ordinary int parameter. With strict_types, the string '2' from a query string is a TypeError, not a lookup failure, and tryFrom() does not catch it. Without strict_types, '2' is coerced and works, but '2 books' and 'poetry' are still a TypeError, and an unknown 7 is a ValueError. Validating with filter_var(..., FILTER_VALIDATE_INT) first is safer than an (int) cast: the cast turns 'poetry' into 0, which would silently match a case backed by 0.

  • Generic code works from the class name. enum_exists() is true only for enums (class_exists() is true for them too), every enum implements UnitEnum, backed ones also BackedEnum, and $enum::tryFrom() works on a class-name string. This is how frameworks cast request and database values to enums; Laravel’s enum casts and Rule::enum() are built on it (Eloquent Casts, Accessors and Serialization).

  • ReflectionEnum lists cases with their backing values without calling cases(), which documentation generators and form builders use.

Built-in enums

PHP itself increasingly uses enums in its APIs:

  • RoundingMode (8.4), the third argument of round() and of BcMath\Number::round(): HalfAwayFromZero (the default), HalfTowardsZero, HalfEven (banker’s rounding), HalfOdd, TowardsZero, AwayFromZero, NegativeInfinity and PositiveInfinity.

  • Random\IntervalBoundary (8.3) for Randomizer::getFloat().

  • Uri\UriComparisonMode and related enums in the URI extension (8.5).

  • SortDirection (upcoming in 8.6) with Ascending and Descending.

<?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
<?php

declare(strict_types=1);

// requires PHP 8.6
var_dump(array_map(fn (SortDirection $d): string => $d->name, SortDirection::cases()));   // ["Ascending", "Descending"]

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.