Models & data
How Eloquent casts raw database values into objects
The mistake
Most people read $user->status as a typed property on the object: assign to it and the row changes, read it and you get what the column holds.
Neither half happens. Eloquent keeps the row as raw values in an internal $attributes array, and $casts says how to convert them. That conversion runs when you read. Writing does not run the same conversion backwards: it stores what you gave it, converting on the way in only for some casts. Nothing reaches the database until save(), and what counts as a change is worked out then, by comparing $attributes against $original.
The machine
Model attributes, cast transformations, and original snapshot run on the tested reducer.
Drive it
- Read a cast property. Click
$user->statusor$user->settings. The raw strings"draft"and'{"theme":"dark"}'become an enum and an array as you read them. - Change a value. Toggle the status or the settings. The raw
$attributesarray changes and$originalstays untouched. - Toggle
is_admintwice. The raw entry does not come back to where it started: it began as the column string'0'and is now the PHPfalse. Eloquent reportsisDirty()asfalseanyway, because for a primitive cast it compares the two sides cast, not raw. - Watch dirty tracking.
isDirty()turnstrueon a real change, and back tofalseonce$user->save()syncs the two arrays.
The mechanism
Eloquent models store raw database columns inside the internal $attributes array. The $casts property tells Eloquent how to convert those raw values to native PHP types when you read a property. The path is __get, then getAttribute.
Writing is not the mirror image of reading. setAttribute converts on the way in only for some casts: an enum is stored as its backing value, an array or json cast is serialised to a JSON string, a datetime is formatted to a date string. A primitive cast like boolean or integer converts nothing. $user->is_admin = true puts the PHP true straight into $attributes, sitting alongside raw column strings like '0'. The panel above shows quotes for the strings so you can tell the two apart.
When you call $user->save(), Eloquent compares $attributes against $original. Only keys that differ are included in the SQL UPDATE. After the query completes, Eloquent copies $attributes into $original, resetting the dirty state.
That comparison is not raw equality, which matters because of the rule above. originalIsEquivalent first checks whether the two raw values are identical. If they are not, and the key has a primitive cast, it casts both sides and compares the results. So assigning false over a raw '0' genuinely changes the array, and isDirty() still returns false, because both sides cast to false and nothing needs writing.
In your code
class User extends Model
{
protected $casts = [
'status' => StatusEnum::class,
'is_admin' => 'boolean',
'settings' => 'array',
'published_at' => 'datetime',
];
}
$user = User::find(42);
// Accessing reads raw $attributes['status'] and converts it to StatusEnum
if ($user->status === StatusEnum::Draft) {
// Mutating converts StatusEnum::Published back to raw string 'published'
$user->status = StatusEnum::Published;
}
// $user->isDirty('status') is true because $attributes['status'] != $original['status']
$user->save(); // Runs UPDATE, then syncs $original = $attributes
The fine print
- A custom cast class implementing
CastsAttributescontrols both directions:get()on the way out andset()on the way in. - Changing an array or JSON attribute in place needs care. Use array access or the
AsArrayObjectcast, or dirty tracking will not see a nested change. - An accessor,
getFooAttribute()or anAttributereturn, wins over the raw column value.getAttributeis not an accessor: it is the resolver that runs them.
Further reading
- Laravel: Attribute Casting covers built-in casts, custom cast classes, and array object casting.
- Laravel: examining attribute changes
is
isDirty,isCleanandwasChanged, and how Eloquent decides what changed.
Spotted a problem, or have a way to make this clearer? Suggest an improvement.