skip to content

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%

answer

  1. pure: name only; backed: name and value
  2. backing type int or string, not both
  3. from(): ValueError when no case matches
  4. tryFrom(): null when no case matches
  5. wrong type still throws TypeError

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.

solid answer

~40 s

A **pure** enum (`enum Size { case Small; }`) has cases with only a `name` and implements `UnitEnum`. A **backed** enum (`enum OrderStatus: string`) declares one backing type, `int` or `string` (no union, no float), and every case must have an explicit, unique value, exposed as the read-only `->value`. Backed enums also implement `BackedEnum`, which adds `from()` and `tryFrom()`. `OrderStatus::from('placed')` returns the case or throws `ValueError` (`"x" is not a valid backing value for enum OrderStatus`), so use it for trusted data where a miss is a bug. `tryFrom()` returns `null` on a miss, for untrusted input you handle yourself. Both follow normal typing rules: an int-backed enum given a non-numeric string throws `TypeError`, even from `tryFrom()`.

code

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

enum OrderStatus: string
{
    case Placed = 'placed';
    case Delivered = 'delivered';
}

var_dump(OrderStatus::from('placed') === OrderStatus::Placed); // bool(true)
var_dump(OrderStatus::tryFrom('refunded'));                    // NULL

try {
    OrderStatus::from('refunded');
} catch (ValueError $e) {
    echo $e->getMessage(), PHP_EOL;
    // "refunded" is not a valid backing value for enum OrderStatus
}

go deeper

for a junior

Recall that backed enums map each case to an int or string value, and that from() and tryFrom() convert a value back to a case.

for a middle

Explain ValueError versus null, the backing-type and uniqueness rules, and how strict_types affects from() and tryFrom().

for a senior

Choose from() for trusted stored data and tryFrom() plus type checks for external input, and handle each failure deliberately.

for a principal

Treat backing values as a public contract shared by databases, APIs and queues, and govern how they may change.

## Pure enums A **pure enum** lists cases with no scalar equivalent: ```php enum Vehicle { case Bike; case Scooter; case Car; } ``` Each case has one read-only property, `name`, holding the case-sensitive case name (`Vehicle::Bike->name` is `'Bike'`). Pure enums implement the built-in `UnitEnum` interface, which provides the static `cases()` method. They have no `from()`/`tryFrom()`, because there is no scalar to convert from. ## Backed enums A **backed enum** gives every case a scalar equivalent, which is what you store in a database or send in an API: ```php enum OrderStatus: string { case Placed = 'placed'; case Delivered = 'delivered'; } ``` Rules the engine enforces: - The backing type is **`int` or `string`**, exactly one of them. Anything else fails with `Enum backing type must be int or string, ... given`. - **Every case needs a value** (`Case X of backed enum OrderStatus must have a value`), and values must be **unique**; a duplicate throws `Duplicate value in enum OrderStatus for cases ...`. - Values are not auto-generated. Since PHP 8.2 they may be constant scalar expressions, for example built from other constants; before that only literals and literal expressions were allowed. - Each case gains a read-only `value` property; writing it throws `Error: Cannot modify readonly property OrderStatus::$value`. Backed enums implement `BackedEnum`, which extends `UnitEnum` and adds two static methods. ## `from()` versus `tryFrom()` | Method | Signature | Value found | Value not found | |---|---|---|---| | `from()` | `from(int\|string $value): static` | returns the case | throws `ValueError` | | `tryFrom()` | `tryFrom(int\|string $value): ?static` | returns the case | returns `null` | The `ValueError` message reads `"refunded" is not a valid backing value for enum OrderStatus`. Choosing between them is about **who is wrong** when the value is missing: 1. Data your own code wrote, such as a database column, should always map. Use `from()`: a miss means corrupted data or a missed migration, and failing loudly is right. 2. Data from outside, such as a query parameter or a webhook, may be anything. Use `tryFrom()` and turn `null` into a validation error or a default: `OrderStatus::tryFrom($input) ?? throw new InvalidArgumentException('bad status')`. ## Type rules still apply `from()` and `tryFrom()` are ordinary methods with typed parameters, so the caller's `strict_types` mode matters: - On a **string-backed** enum in coercive mode, an `int` is converted to a string before lookup; in strict mode it is a `TypeError`. - On an **int-backed** enum, a numeric string such as `'2'` is coerced in coercive mode, but a non-numeric string such as `'abc'` throws `TypeError` (`must be of type int, string given`) in both modes, **even from `tryFrom()`**. - A `float` is a `TypeError` in strict mode. So `tryFrom()` does not make arbitrary input safe on its own: validate the scalar type first, or catch `TypeError` at the boundary. ## Where each conversion belongs | Boundary | Direction | Tool | |---|---|---| | HTTP request, webhook, CLI argument | scalar to case | type check, then `tryFrom()` with a validation error | | Database or cache written by your system | scalar to case | `from()`, so drift throws | | Database write, JSON response, queue message | case to scalar | `->value` | | Logs and debugging | case to text | `->name` or a `label()` method | Keeping conversions at these edges means the rest of the code only ever sees `OrderStatus` cases, never raw strings. A related habit: expose one named constructor for each external format instead of calling `from()` everywhere. A static `fromApi(string $raw): self` that trims, lowercases and then calls `tryFrom()` keeps every input quirk in one place, next to the cases it maps to. ## Converting by name There is no built-in `fromName()`. Prefer backing values for anything external. When you truly need the name, compare against `cases()`: ```php foreach (OrderStatus::cases() as $case) { if ($case->name === $name) { return $case; } } ``` The manual also mentions `defined()` and `constant()` with `'OrderStatus::Placed'`, but discourages them in favour of backed enums.

  • In PHP, what does tryFrom('abc') do on an int-backed enum?
    It throws `TypeError` (`must be of type int, string given`) instead of returning `null`, because the parameter is typed for the backing type and a non-numeric string cannot be coerced. `tryFrom()` only softens a missing value, not a wrong type, so validate input types first.
  • In PHP, can a backed enum use int|string as its backing type, or leave some cases without values?
    No to both. The backing type must be exactly `int` or exactly `string`, and every case of a backed enum must declare an explicit, unique value. A pure enum, in turn, may not give any case a value.
  • In PHP, can you define your own from() method on a backed enum to add aliases?
    No. `from()`, `tryFrom()` and `cases()` are provided by the engine, and redeclaring one is a fatal error. Add a differently named static method, such as `fromLegacyCode()`, that maps aliases and then calls `from()`.

saying these in an interview costs you the question

  • Says tryFrom() never throws, whatever the input type
  • Believes pure enums also have a value property
  • Uses from() on user input and lets ValueError become a 500 error
  • Thinks backed enums auto-number int cases like other languages
  • Claims a backed enum may mix int and string values