skip to content

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

level: middleimportance: should knowfreq 40%

answer

  1. === on singleton cases
  2. never equal to their backing value
  3. < and > are always false
  4. array offset TypeError
  5. match compares with ===

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.

solid answer

~40 s

Each case is a singleton object, so `$status === OrderStatus::Delivered` is the right check, and `OrderStatus::from('delivered') === OrderStatus::Delivered` is `true` because `from()` returns the same instance. A case never equals its backing value: `OrderStatus::Delivered === 'delivered'` is `false`, and loose `==` against a string is `false` too; compare `->value` when you have a string. `instanceof OrderStatus` checks the type. Ordering is not defined: `<` and `>` between cases always return `false`, so sorting needs an explicit rank, such as the position in `cases()`. Cases cannot be array keys (`TypeError: Cannot access offset of type OrderStatus on array`); key by `->value` or `->name`, or use `SplObjectStorage`/`WeakMap`. `match` compares with `===`, so it is the natural way to branch on a case.

code

php · 16 lines
php
<?php
enum OrderStatus: string
{
    case Placed = 'placed';
    case Delivered = 'delivered';
}

$s = OrderStatus::from('delivered');
var_dump($s === OrderStatus::Delivered);            // bool(true)
var_dump($s === 'delivered');                        // bool(false)
var_dump(OrderStatus::Placed < OrderStatus::Delivered); // bool(false)
var_dump(OrderStatus::Placed > OrderStatus::Delivered); // bool(false)

$counts = [];
$counts[$s->value] = 1;   // fine: string key
// $counts[$s] = 1;       // TypeError: Cannot access offset of type OrderStatus on array

go deeper

for a junior

Recall that enum cases are compared with === and are not equal to their string or int values.

for a middle

Explain singleton identity, why < and > are always false, the array-key TypeError and how match compares.

for a senior

Convert at boundaries, key storage by value, and rely on match without default so new cases fail loudly in tests.

for a principal

Decide how ordering and exhaustiveness of domain enums are expressed and enforced across a codebase and its tooling.

## Cases are singleton objects Every enum case is an **object**, and there is exactly one instance of each case per request. `OrderStatus::Delivered` written in two places, `OrderStatus::from('delivered')` and `OrderStatus::cases()[3]` all refer to the same object. That is why identity comparison is the correct tool: ```php $status === OrderStatus::Delivered; // identity: same case? $status instanceof OrderStatus; // type: is it an OrderStatus case at all? ``` `==` also returns `true` for the same case and `false` for different cases, but `===` states the intent and avoids any loose-comparison rules. ## A case is not its value A backed case is not a string or an int: | Expression | Result | |---|---| | `OrderStatus::Delivered === 'delivered'` | `false` | | `OrderStatus::Delivered == 'delivered'` | `false` | | `OrderStatus::Delivered->value === 'delivered'` | `true` | | `OrderStatus::from('delivered') === OrderStatus::Delivered` | `true` | Mixing the two usually means a conversion is missing at a boundary. Convert once, where data enters (`from()` or `tryFrom()`), and pass cases inward. A common symptom is a template or query builder comparing `$order->status == 'delivered'` after the property was changed to an enum: the condition becomes silently `false` for every order, with no error to point at the cause. Static analysers can flag such comparisons, which is one more reason to run them on code that adopts enums. ## No ordering Enum cases have no natural order. The manual states that `<` and `>` between cases **always return `false`**, in both directions. Consequences: - `sort()` on an array of cases does not produce a meaningful order. - "Is this status later than that one?" needs an explicit rule. Two honest ways to define order: 1. A `rank(): int` method using `match ($this)`, when the order is a domain rule (placed, preparing, out for delivery, delivered). 2. The index in `cases()`, via `array_search($case, OrderStatus::cases(), true)`, when declaration order is the intended order. ## No array keys PHP array keys must be `int` or `string`. An object, including an enum case, is not converted, even though it has a `name`: ```php $counts[OrderStatus::Placed] = 3; // TypeError: Cannot access offset of type OrderStatus on array ``` Options: - key by `$status->value` (backed) or `$status->name` (pure), and convert back with `from()` when reading; - use `SplObjectStorage` or `WeakMap`, which accept objects, including enum cases, as keys. ## Enums in `match` `match` compares its subject with each arm using **strict identity**, so enum cases fit it exactly: ```php $eta = match ($status) { OrderStatus::Placed, OrderStatus::Preparing => 'about 30 minutes', OrderStatus::OutForDelivery => 'about 10 minutes', OrderStatus::Delivered, OrderStatus::Cancelled => null, }; ``` A `match` without a `default` arm that meets a case no arm lists throws `UnhandledMatchError` at runtime. PHP does not check exhaustiveness at compile time, so adding a case to the enum is caught by tests or static analysis, not by the compiler. Leaving out `default` is still the better design: a new case fails loudly instead of silently taking a generic branch. `switch` also works, but it compares with `==`, which is fine for cases yet invites mixing in strings; `match` keeps comparisons strict and returns a value. ## Identity outside a single expression The singleton guarantee holds wherever PHP produces a case. `OrderStatus::from('placed')`, a constant alias, an element of `cases()` and a case restored by `unserialize()` are all the same object as `OrderStatus::Placed`, so identity checks never need a fallback to comparing values. `var_export()` writes a case as `\OrderStatus::Placed`, which evaluates back to that same singleton. ## Working with lists of cases Array functions behave predictably once you remember that cases are objects: - `in_array($status, $allowed, true)` checks membership by identity; the strict flag makes the intent explicit. - `array_search($status, OrderStatus::cases(), true)` returns the declaration index, useful as a rank. - `array_map(fn (OrderStatus $s) => $s->value, OrderStatus::cases())` produces the list of stored values for validation or an API schema. - `in_array('placed', OrderStatus::cases(), true)` is always `false`: the haystack holds objects, not strings. Map to values first. ## Checklist - `===` or `match` for case checks, `instanceof` for type checks. - `->value` when comparing with or storing as a scalar. - Explicit rank for ordering; never `<`/`>`. - Scalar keys or object maps; never a case as an array key.

  • In PHP, how do you count orders per status when statuses are enum cases?
    Key a plain array by `$status->value` and convert keys back with `OrderStatus::from()` when reading, or use `SplObjectStorage`, which accepts the case objects as keys directly. A case itself cannot be an array key.
  • In PHP, what happens when a match over an enum lacks an arm for a newly added case?
    When that case reaches the `match` and there is no `default` arm, PHP throws `UnhandledMatchError` at runtime. Nothing checks exhaustiveness at compile time, so tests or static analysis must catch it.

saying these in an interview costs you the question

  • Compares a backed case directly with its string value
  • Sorts enum cases with sort() and expects declaration order
  • Uses an enum case as an array key
  • Thinks PHP checks match exhaustiveness at compile time
  • Believes from() creates a new object that is not identical to the case