In an Eloquent model, what does the $attributes property do, and why must its default values be written in raw database form?
answer
- initial value of the attributes array
- applied to every new instance
- raw, storable form: '[]' not []
- casts run on read, not on defaults
- database defaults are not read back
basics
~20 sThe $attributes property gives new model instances default column values. They sit in the raw attributes array without passing through casts or mutators, so a JSON-cast column needs the string '[]', not a PHP array.
solid answer
~40 s`protected $attributes` is the initial content of the model's internal attributes array, so every `new Reservation` starts with those values before `fill()` applies anything you pass. The array stores **raw, storable** values, exactly as a row read from the database would hold them, and a default placed there bypasses `setAttribute()`, casts and mutators. That is why a column cast to `array` needs the default `'[]'` as a JSON string: reading it decodes the string, while a PHP array would reach the insert unencoded. Model defaults also differ from database column defaults: Eloquent does not read the row back after an insert, so a value supplied only by the database is missing from the returned model unless you reload it or, from Laravel 13.33, list the column in `#[Refreshes]`.
go deeper
Recall that $attributes holds default values that every new model instance starts with.
Explain the raw-versus-cast distinction and why a JSON-cast default must be a string, plus how model defaults differ from database defaults.
Keep PHP and database defaults consistent, and choose between duplicating defaults, reloading and #[Refreshes] where database-computed values matter to the response.
Decide whether defaults are owned by the schema or the domain layer, and how that choice affects imports and writes that bypass Eloquent.
## What `$attributes` is Every Eloquent model keeps its column values in an internal array, the **attributes** array. The `protected $attributes` property is that array's initial value, so anything you put there becomes a **default attribute value** on every new instance: ```php class Reservation extends Model { protected $attributes = [ 'status' => 'pending', 'guests' => 1, 'preferences' => '[]', ]; } ``` `new Reservation` now has `status` set to `pending` before anything else runs. The constructor then calls `fill()` with any array you pass, so explicit values override the defaults, subject to the usual mass-assignment rules. ## Why values must be raw The attributes array holds values in their **raw, storable form**, the way they come back from the database, not the way your code reads them. Casts and accessors are applied when you read an attribute; mutators and casts are applied when you set one through `setAttribute()`. A default in `$attributes` goes straight into the raw array without passing through either path. So for a column cast to `array` or `json`, the default must be the JSON **string** `'[]'`, not the PHP array `[]`. Reading `$reservation->preferences` then decodes the string into an array as usual. A PHP array placed there would reach the insert unencoded and the database driver would have no scalar to bind. The same idea applies to other cast types: - booleans can be written as `false` or `0`, because both bind as scalars; - a datetime default is a date string in the stored format; - an encrypted column cannot sensibly have a plain-text default, because reading it would try to decrypt it. ## Model defaults versus database defaults A column default declared in the migration and a model default in `$attributes` are different things. | | `$attributes` default | database column default | |---|---|---| | Visible on `new Model` | yes | no | | Seen by `creating` / `saving` observers | yes | no | | Present on the model after `create()` | yes | no, unless reloaded | | Applies to inserts outside Eloquent | no | yes | That last row is the classic surprise: after `Reservation::create([...])`, a column filled only by a database default is absent from the returned model. Eloquent inserts what it has and does not read the row back, so `$reservation->status` would be `null` in PHP even though the database stored `pending`. Options are: 1. put the default in `$attributes` as well, so PHP and the database agree; 2. reload the model after saving when the value matters; 3. on Laravel 13.33 or later, add `#[Refreshes('status')]`, which re-reads the listed columns from the database after each insert or update. It is aimed at generated columns but works for any column the database fills. ## Where it fits among model settings - `$attributes` sets **values**; `$fillable` and `$guarded` decide which keys `fill()` may set, and defaults ignore those lists entirely. - `$attributes` is not the place for computed values that depend on other fields; a `creating` observer or an explicit assignment in the service that creates the record is clearer. - Because the defaults are synced as the model's original state when it is constructed, a freshly built model does not report them as changed; they are still written on insert, which sends every attribute. ## Common mistakes - Writing a cast-type value instead of a raw one: an enum-cast column should default to its backing value, such as `'pending'`, the same scalar a database row would hold. - Duplicating a default in `$attributes` and the migration, then changing only one of them, so rows created through Eloquent and rows inserted by SQL scripts disagree. - Expecting `$attributes` defaults on rows written by `Reservation::insert([...])`: that call goes straight to the query builder, never builds a model and so never sees the defaults. - Using `$attributes` for values that depend on the current user or time; those belong in the code that creates the record, where the context is available.
- After `Reservation::create([...])`, why is `$reservation->status` null when the migration gives `status` a default?Eloquent inserts the attributes it holds and does not select the row back, so a value the database filled in never reaches the PHP object. Put the same default in `$attributes`, reload the model when you need database-computed values, or on Laravel 13.33 and later list the column in `#[Refreshes]` so Eloquent re-reads it after each write.
- Do `$fillable` or `$guarded` restrict the keys you can put in `$attributes`?No. Mass-assignment lists only filter arrays passed to `fill()` and the methods built on it. `$attributes` is the raw starting state of the model, so any key there is set regardless of the fillable or guarded settings.
saying these in an interview costs you the question
- A JSON-cast default should be written as a PHP array like []
- Defaults in $attributes pass through casts and mutators when applied
- A migration column default shows up on the model right after create()
- Keys in $attributes must also be listed in $fillable
- $attributes is where Eloquent stores cast definitions