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?
answer
- enum class as the cast type
- backed enums use from()
- ValueError on unknown value
- assign a case or its value
- AsEnumCollection for lists
basics
~20 sMap 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 sIn `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
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
Recall that listing the enum class in casts() turns the column into enum cases on read and back into values on save.
Explain from() on read and write, the ValueError on unknown values or wrong enum types, and how queries and JSON handle cases.
Plan enum changes as data changes: deploy new cases before writers, migrate rows before removing cases, and constrain the column.
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