In PHP, how should enum cases be compared, and why do < and > or array keys not work with them?
answer
- === on singleton cases
- never equal to their backing value
- < and > are always false
- array offset TypeError
- match compares with ===
basics
~20 sCompare 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 sEach 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
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 arraygo deeper
Recall that enum cases are compared with === and are not equal to their string or int values.
Explain singleton identity, why < and > are always false, the array-key TypeError and how match compares.
Convert at boundaries, key storage by value, and rely on match without default so new cases fail loudly in tests.
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