skip to content

With Eloquent, how do you cast an invoice's status column to a PHP enum, and what happens with values the enum does not have?

level: middleimportance: should knowfreq 50%

answer

  1. enum class as the cast type
  2. backed enums use from()
  3. ValueError on unknown value
  4. assign a case or its value
  5. AsEnumCollection for lists

basics

~20 s

Map the column to the enum class in casts(), such as 'status' => InvoiceStatus::class. Reads return an enum case and writes store its backing value; a value the enum lacks raises a ValueError, from the database or from code.

solid answer

~40 s

In `casts()` write `'status' => InvoiceStatus::class`. On read, Eloquent turns the stored value into a case: for a backed enum it calls `InvoiceStatus::from($value)`, for a pure enum it looks the case up by name. On write you may assign a case or its raw value; Eloquent converts the raw value with `from()` and stores the backing value (or the name for a pure enum). Because `from()` is strict, a row holding `'refunded'` when the enum has no such case throws a `ValueError` the moment the attribute is read, and assigning an unknown string throws on assignment. Assigning a case of a **different** enum also throws a `ValueError`. Query builder bindings accept cases directly, so `where('status', InvoiceStatus::Paid)` binds `'paid'`. For a JSON column holding several values, `AsEnumCollection::of(Feature::class)` or `AsEnumArrayObject::of(...)` casts each element.

code

php · 15 lines
php
<?php

use App\Enums\InvoiceStatus;
use App\Models\Invoice;

$invoice = Invoice::findOrFail($id);

if ($invoice->status === InvoiceStatus::Open) {
    $invoice->status = InvoiceStatus::Paid;   // stored as 'paid'
    $invoice->save();
}

$invoice->status = 'payed';                  // ValueError: not a valid backing value

$paid = Invoice::where('status', InvoiceStatus::Paid)->count();

go deeper

for a junior

Recall that listing the enum class in casts() turns the column into enum cases on read and back into values on save.

for a middle

Explain from() on read and write, the ValueError on unknown values or wrong enum types, and how queries and JSON handle cases.

for a senior

Plan enum changes as data changes: deploy new cases before writers, migrate rows before removing cases, and constrain the column.

for a principal

Decide which statuses belong in enums versus lookup tables, weighing type safety against runtime configurability and cross-service contracts.

## Why enum casts A subscription invoice has a `status` column holding `'draft'`, `'open'`, `'paid'` or `'void'`. Without a cast the code passes strings around and a typo such as `'payed'` sails through. With an **enum cast**, the model hands out `InvoiceStatus` cases, so the type system and IDE know the legal values, and a `match` over the enum is exhaustive. ## Declaring the cast ```php enum InvoiceStatus: string { case Draft = 'draft'; case Open = 'open'; case Paid = 'paid'; case Void = 'void'; } protected function casts(): array { return ['status' => InvoiceStatus::class]; } ``` No cast class or extra package is needed: Eloquent recognises an enum class as the cast type. ## What happens on read and write Reading the framework's attribute code: 1. **Read:** if the stored value is `null`, the attribute is `null`. Otherwise a **backed** enum is resolved with `InvoiceStatus::from($value)`; a **pure** enum (no backing values) is resolved by case **name**. 2. **Write a case:** `$invoice->status = InvoiceStatus::Paid` stores `'paid'`. 3. **Write a raw value:** `$invoice->status = 'paid'` is converted with `from()` first, so an invalid string fails immediately. 4. **Write the wrong enum:** assigning a case from another enum class throws `ValueError` ("not of the expected enum type"). | Situation | Result | |---|---| | Row holds `'paid'` | `InvoiceStatus::Paid` | | Row holds `NULL` | `null` | | Row holds `'refunded'`, no such case | `ValueError` when the attribute is read | | Code assigns `'payed'` | `ValueError` on assignment | | Code assigns `OrderStatus::Paid` | `ValueError` on assignment | ## The unknown-value trap `from()` is strict, which is what you want for code, but it turns **data drift** into runtime errors. The usual story: a migration or another service writes `'refunded'` before the enum gains that case, and every page that touches those invoices throws. Defences: - Add the new case to the enum **before** anything writes the value. - Treat removing or renaming a case as a data migration, because old rows still hold the old value. - Keep a database check constraint or a validation rule aligned with the enum, so bad values never land. The language rules behind this (`from()` versus `tryFrom()`, pure versus backed) belong to PHP itself; the Eloquent-specific fact is that the cast uses the strict one. ## Querying and serializing - Query builder bindings unwrap enums, so `Invoice::where('status', InvoiceStatus::Paid)->get()` binds `'paid'`. - In `toArray()` and JSON a backed case serializes as its value (`"status": "paid"`). - `$invoice->status === InvoiceStatus::Paid` is the idiomatic comparison; comparing with the string `'paid'` is always false once the cast is in place. ## Putting behaviour on the enum Once the attribute is a case, presentation logic can live on the enum instead of in views and controllers: - a `label()` method returns "Paid" or "Overdue" for display; - a `color()` method picks a badge style; - a `canTransitionTo(self $next)` method keeps the state machine in one place. A Blade view then prints `$invoice->status->label()` without any `match` of its own, and adding a case forces every such method to handle it. ## Enum cast versus a plain string column | Concern | String column | Enum cast | |---|---|---| | Typos in code | reach the database | fail at assignment | | Legal values documented | in comments or validation | in the enum itself | | Unknown value in a row | passes through silently | `ValueError` on read | | Renaming a value | find and replace | rename plus data migration | The trade is strictness: an enum cast catches bugs early and turns bad data into loud failures. ## Lists of enum values A JSON column such as `features` holding `["priority_support", "sso"]` can be cast with `AsEnumCollection::of(Feature::class)` (a `Collection` of cases) or `AsEnumArrayObject::of(Feature::class)`. Because those helpers are method calls, they must be declared in the `casts()` method rather than in a `$casts` property default.

  • How would you roll out a new 'refunded' status without breaking reads?
    Deploy the enum with the new `Refunded` case first, then deploy the code or job that writes `'refunded'`. If the order is reversed, rows written early hold a value the running code's enum lacks, and reading their `status` throws a `ValueError` through the cast's strict `from()` call.
  • Does the enum cast work with a pure enum that has no backing values?
    Yes. For a pure enum Eloquent stores and reads the case name, resolving it with the enum's constant lookup rather than `from()`. That couples the column to case names, so renaming a case becomes a data migration.

saying these in an interview costs you the question

  • Saying an unknown database value quietly becomes null
  • Believing the enum cast needs a custom CastsAttributes class
  • Comparing $invoice->status with the string 'paid' after casting
  • Thinking the cast accepts a case from any enum class
  • Assuming where('status', InvoiceStatus::Paid) needs ->value