In a Laravel 13 Eloquent model, what does casts() declare, how does it relate to $casts, and what do common built-in casts return?
answer
- column name to type map
- method can call static helpers
- casts() wins over $casts on conflict
- datetime:format only shapes output
- decimal:2 returns a string
basics
~20 scasts() returns a map of attribute names to cast types such as boolean, datetime, decimal:2, enum classes and cast classes, applied on every read and write. The older $casts property still works; casts() is merged over it and can call static helpers.
solid answer
~40 sIn Laravel 13 a model declares `protected function casts(): array` returning, for example, `['paid' => 'boolean', 'amount' => 'decimal:2', 'issued_at' => 'immutable_datetime', 'status' => InvoiceStatus::class]`. Eloquent converts each value when you read the attribute, and converts dates, JSON, enums and cast classes back to a storable form when you set them. The older `protected $casts = [...]` property still works; at model initialisation the two are merged with `array_merge($this->casts, $this->casts())`, so the method wins on a conflict. The method exists mainly because a property default cannot call code, while the method can return `AsCollection::using(InvoiceLines::class)`. Two built-ins surprise people: `decimal:2` returns a **string** rounded to two places, not a float, and a format such as `datetime:Y-m-d` changes only the array and JSON output, not the Carbon object you read.
code
php · 20 lines<?php
namespace App\Models;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
protected function casts(): array
{
return [
'paid' => 'boolean',
'amount' => 'decimal:2', // '19.90' (string)
'issued_at' => 'immutable_datetime',
'due_at' => 'datetime:Y-m-d', // Carbon; JSON prints 2026-10-01
'status' => InvoiceStatus::class,
];
}
}go deeper
Recall that casts() maps attributes to types such as boolean, datetime and decimal, and that casts convert both on read and on write.
Explain the merge between $casts and casts(), why decimal returns a string, and that a date format only affects serialization.
Catch float money, mutable date aliasing and format-versus-storage confusion in review, and pick immutable_datetime by default.
Set conventions for money, time zones and date serialization across services so API consumers never parse ambiguous values.
## What a cast is Database drivers hand PHP mostly strings and integers: a `paid` flag arrives as `0` or `1`, a timestamp as `'2026-09-29 10:15:00'`, a JSON column as text. A **cast** tells Eloquent to convert an attribute to a richer PHP type when you read it and back to a storable value when you set it, so the rest of the code works with booleans, dates, enums and value objects instead of raw strings. ## Declaring casts: the method and the property The Laravel 13 skeleton's `User` model declares casts with a method: ```php protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } ``` The older form, `protected $casts = [...]`, still works. When the model initialises, Eloquent merges both with `array_merge($this->casts, $this->casts())`, so an entry in `casts()` **overrides** the same key in `$casts`. The method form matters because a property's default value must be a constant expression, while a method can call helpers such as `AsCollection::using(InvoiceLines::class)` or `AsEnumCollection::of(Feature::class)`. At runtime, `mergeCasts()` adds casts to one instance, and `withCasts()` adds them to one query. ## The built-in casts you meet most | Cast | Read value | Notes | |---|---|---| | `boolean`, `integer`, `float`, `string` | PHP scalar | `null` stays `null` | | `datetime` | `Illuminate\Support\Carbon` | mutable instance | | `immutable_datetime` | `CarbonImmutable` | `addDays()` returns a new object | | `date` / `immutable_date` | Carbon at 00:00:00 | time is dropped | | `datetime:Y-m-d` | Carbon | the format applies only to `toArray()` and JSON | | `decimal:2` | **string**, e.g. `'19.90'` | rounded half up to the given scale | | `array`, `json`, `object`, `collection` | decoded JSON | whole-value replacement only | | `encrypted`, `encrypted:array` … | decrypted value | stored as ciphertext | | `hashed` | stored hash | hashes plain text on assignment | | `InvoiceStatus::class` | enum case | backed or pure enum | ## Traps interviewers probe 1. **`decimal:2` is a string.** The framework formats the value with an arbitrary-precision decimal library and returns a string, so `'19.90'` survives without float rounding. Comparing it with `===` against a float fails; doing arithmetic on it silently converts to float. 2. **A date format does not format the object.** `'due_at' => 'datetime:Y-m-d'` still gives a full Carbon on `$invoice->due_at`; only `toArray()` and `toJson()` print `2026-10-01`. Without a format, dates serialize as UTC ISO-8601 (`2026-10-01T00:00:00.000000Z`). The storage format is a separate setting on the model. 3. **Mutable dates are shared objects.** With `datetime`, `$due = $invoice->due_at; $reminder = $due->subDays(3);` also moves `$due`, because Carbon's mutators change and return the same instance; `immutable_datetime` returns a new object and makes that bug impossible. Each read of the attribute re-parses the stored value, so neither form writes a change back unless you assign it. 4. **Not every cast converts on write.** Dates, JSON, encrypted, hashed, enum and class casts convert the value when you assign it. Scalar casts such as `boolean` and `integer` convert only on read, so `$invoice->paid = 'yes'` writes the raw `'yes'`; validate input before assigning it. ## How casts interact with dirty checking Casts also decide whether a model is **dirty**. Eloquent compares the new raw value with the original raw value, applying cast-aware comparison, so: - assigning `'1'` to a `boolean` attribute whose stored value is `1` is not treated as a change; - assigning a Carbon for the same instant to a `datetime` attribute is not a change either; - assigning `'19.9'` to a `decimal:2` attribute holding `'19.90'` compares the scaled values, not the raw strings. This keeps `save()` from issuing pointless updates when a form re-submits the same values. ## A checklist for a new model - Every flag column gets `boolean`, every timestamp gets `datetime` or `immutable_datetime`. - Money never uses `float`: use `decimal:<scale>`, or integer cents behind a value-object cast. - Status columns use an enum class instead of strings. - JSON columns choose between `array` and the object casts depending on whether code edits keys in place. - Secrets use `encrypted`, passwords use `hashed`. ## Where custom behaviour goes When the built-ins are not enough, the same map accepts a **cast class** (implementing `CastsAttributes`) or an enum class. Computed values that do not map to one column belong in an accessor instead. Both are covered by their own questions; the principle is the same: the model converts at the boundary, so controllers and views never see raw column text.
- How do you cast a raw value selected in one query, such as MAX(paid_at)?Call `withCasts(['last_paid_at' => 'datetime'])` on the query builder. The cast applies to the models that query returns, so the aggregated column comes back as a Carbon instance instead of a string, without changing the model's permanent `casts()` map.
- Why store money as decimal:2 rather than float?A float cannot represent most decimal fractions exactly, so sums drift by fractions of a cent. `decimal:2` returns a string rounded half up to two places by an arbitrary-precision library, which keeps the exact value from the column. Many teams go further and store integer cents behind a custom cast that returns a money value object.
saying these in an interview costs you the question
- Saying decimal:2 returns a float rounded to two places
- Believing datetime:Y-m-d changes the Carbon object you read
- Claiming the $casts property no longer works in Laravel 13
- Thinking $casts wins when both declare the same attribute
- Treating $invoice->due_at as a plain string after a datetime cast