skip to content

Enumerations

Enums define a closed set of cases, pure or backed by int or string, that can carry methods, constants and interfaces. Interviewers probe from vs tryFrom and why enums beat class-constant lists.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP 8.1 and later, why use an enum instead of class constants for a fixed set of values such as order statuses?

level: juniorimportance: must knowfreq 62%

answer

  1. a type, not just a value
  2. parameter typed OrderStatus rejects strings
  3. closed set, listed by cases()
  4. behaviour lives on the enum
  5. added in PHP 8.1

basics

~20 s

An enum is a real type with a closed set of cases, so a parameter typed OrderStatus accepts only those cases and rejects any string. Class constants are plain strings or ints that any typo or foreign value can impersonate.

solid answer

~40 s

Before PHP 8.1, a status was a set of constants like `Order::STATUS_PLACED = 'placed'`, and every function took a `string`. Nothing stopped `'plaecd'`, or a value from another constant group, from flowing through. An **enum** (PHP 8.1+) is a type: `function notify(OrderStatus $status)` accepts only `OrderStatus` cases and throws `TypeError` for anything else, so validation happens once at the boundary. The set is closed and listable with `OrderStatus::cases()`, cases compare by identity with `===`, and behaviour such as `label()` or `canTransitionTo()` lives on the enum instead of in `switch` statements scattered around the code. IDEs and static analysers know every case. A **backed** enum still maps each case to a stable `string` or `int` for the database or an API.

code

php · 25 lines
php
<?php
declare(strict_types=1);

enum OrderStatus: string
{
    case Placed = 'placed';
    case Preparing = 'preparing';
    case OutForDelivery = 'out_for_delivery';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Placed => 'Order placed',
            self::Preparing => 'Being prepared',
            self::OutForDelivery => 'On the way',
            self::Delivered, self::Cancelled => ucfirst($this->value),
        };
    }
}

function notify(OrderStatus $status): string { return $status->label(); }
echo notify(OrderStatus::OutForDelivery), PHP_EOL; // On the way
// notify('out_for_delivery');                     // TypeError: must be of type OrderStatus

go deeper

for a junior

Recall that an enum is a type whose values are its cases, and that typing a parameter with it rejects other values.

for a middle

Explain pure versus backed enums, cases(), identity comparison and how methods on the enum replace scattered switch statements.

for a senior

Use enums at system boundaries, converting stored values once, and keep behaviour such as transitions on the enum.

for a principal

Decide where enums replace string codes across services and databases, and how their values become a stable contract.

## The problem with constant lists For years PHP code modelled a fixed set of values with class constants: ```php final class Order { public const STATUS_PLACED = 'placed'; public const STATUS_PREPARING = 'preparing'; public const STATUS_DELIVERED = 'delivered'; } function notify(string $status): void { /* ... */ } ``` This works until it does not: - **Any string is accepted.** `notify('plaecd')`, `notify('')` or `notify(Payment::STATUS_FAILED)` all pass the `string` type. Every function has to re-validate or trust its caller. - **The set is not a thing.** There is no built-in way to ask for "all statuses"; code keeps a separate array that drifts from the constants, or uses reflection. - **Behaviour scatters.** The label, the colour and the allowed next statuses end up in `switch` statements in controllers, templates and jobs. - **Tools cannot help.** An IDE sees a `string` parameter and cannot suggest the valid values. ## What an enum changes PHP 8.1 added **enumerations**. An enum declares a new type whose only values are its **cases**: | Concern | Class constants | Enum | |---|---|---| | Parameter type | `string` (anything) | `OrderStatus` (only cases) | | Invalid value | passes silently | `TypeError` at the call | | Listing values | hand-maintained array | `OrderStatus::cases()` | | Comparison | string equality | identity, `===` on singleton cases | | Behaviour | scattered `switch` | methods on the enum | | Storage form | the constant's value | `->value` on a backed enum | Each case is a **singleton object**: `OrderStatus::Placed` is always the same instance, so `===` is reliable and cheap. ## Pure and backed An enum can be **pure** (cases have no scalar equivalent, only a `name`) or **backed** by `int` or `string`, which gives each case a `value` for storage and a `from()`/`tryFrom()` pair to convert back. For order statuses that live in a database column, a string-backed enum is the usual choice. ## Behaviour belongs to the enum Enums can declare methods and constants and implement interfaces. That turns scattered logic into one place: 1. `label()` returns the text shown to the customer. 2. `canTransitionTo(self $next)` encodes that a delivered order cannot go back to preparing. 3. `isFinal()` answers whether the order is closed. When a new status such as `Refunded` is added, the methods on the enum are where the change has to be made, and a `match` without a `default` arm makes a forgotten case fail loudly instead of silently falling through. ## What enums do not do - They carry **no per-case state**: no properties beyond `name` and `value`, so per-order data (timestamps, courier) stays on the order entity. - They are **closed**: an enum cannot be extended, and new cases require a code change and a deploy. - They are not strings: templates and JSON need `->value` or `->name` explicitly. ## Migrating from constants Most codebases meet enums while replacing an existing constant list. A low-risk order: 1. Declare a string-backed enum whose **values equal the old constant values**, so stored data needs no migration. 2. Convert at the **boundaries**: hydrate from the database with `OrderStatus::from($row['status'])`, and write `$status->value` back. 3. Change parameter and property types from `string` to `OrderStatus`, starting with the core domain; the type checks will point at every caller still passing strings. 4. Move `switch` blocks on the old constants into methods on the enum. 5. Delete the old constants once nothing references them. The result is that validation happens once, where data enters, and everything inside the application handles only valid cases. ## A note on readability Enums also make signatures self-documenting. `function reassign(Order $order, OrderStatus $to): void` says exactly what is allowed, while `function reassign(Order $order, string $to)` needs a comment, a validation block and a test for bad input. Reviewers read the type and move on. ## When class constants are still fine Constants remain right for values that are not a closed set of alternatives: a timeout, a page size, a header name. The test is simple: if a parameter should accept "one of these and nothing else", it wants an enum.

  • In PHP, how do you list every case of an enum, for example to fill a select box?
    Call the static `cases()` method, which every enum gets from the `UnitEnum` interface. It returns a list of the case objects in declaration order; map it to `->value` and `->label()` (or `->name` for a pure enum) to build the options.
  • In PHP, can an enum case hold per-order data such as a delivery time?
    No. Enums cannot declare properties; each case is a stateless singleton with only `name` and, when backed, `value`. Per-order data belongs on the order object, which holds an `OrderStatus` alongside its own fields.

Class constants are a free-text box on an order form with a list of suggestions printed beside it; an enum is a set of labelled buttons, so you can only press one that exists.

saying these in an interview costs you the question

  • Says enums are just syntax sugar over class constants with the same type
  • Believes a string parameter can be restricted to enum values without typing it as the enum
  • Thinks PHP enums can store per-case mutable state
  • Claims enums were available before PHP 8.1
  • Uses enums for unrelated configuration values like timeouts
open as a page

In PHP, what is the difference between a pure and a backed enum, and between from() and tryFrom() on a backed enum?

level: middleimportance: must knowfreq 58%

basics

~20 s

A pure enum's cases have only a name; a backed enum gives every case a unique int or string value. On a backed enum, from() returns the matching case or throws ValueError, while tryFrom() returns null when no case matches.

open as a page

In PHP, how should enum cases be compared, and why do < and > or array keys not work with them?

level: middleimportance: should knowfreq 40%

basics

~20 s

Compare enum cases with ===, which is identity on singleton objects, or instanceof for the type. Cases are objects, so < and > always return false and using one as an array key throws TypeError; key by ->value instead.

open as a page

In PHP, what can an enum declare besides its cases, and what does the engine forbid inside an enum?

level: middleimportance: should knowfreq 42%

basics

~20 s

An enum may declare methods, static methods, constants, interfaces and traits without properties, plus __call, __callStatic and __invoke. It may not have properties, constructors, destructors, abstract methods or other magic methods, and cannot extend or be extended.

open as a page

A PHP app stores order statuses as a string-backed enum in the database; what breaks when you rename a case, change a value or add a case?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The stored ->value is the contract. Renaming a case breaks code and anything that stored the name; changing a value makes from() throw ValueError on old rows; adding a case makes every default-less match throw UnhandledMatchError until handled.

open as a page