Create a new Eloquent model, ask for $flight->delayed, and you get null. Not false, even if your database column defaults to false. That gap between "the database knows the default" and "the model knows the default" causes small, annoying bugs. Laravel gives you a one-property fix: $attributes. This article shows how it works, where it stops helping, and the format mistake that throws an error. Everything here was run against Laravel 13.35.0 Eloquent and MariaDB.
The basic example
Add an $attributes array to your model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The model's default values for attributes.
*
* @var array<string, mixed>
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
This is the example from the Laravel documentation. A new instance now carries those values from the moment it is created, before anything touches the database:
$flight = new Flight;
$flight->delayed; // false
$flight->options; // '[]' (or [] if you cast it, see below)
Why the database default is not enough
Say your migration has $table->boolean('delayed')->default(false) and $table->string('status')->default('scheduled'), but the model has no $attributes. I ran this:
$flight = Flight::create(['name' => 'A']);
$flight->delayed; // null (the row in the database has 0)
$flight->status; // null (the row in the database has 'scheduled')
$flight->refresh();
$flight->status; // 'scheduled'
The insert never mentioned those columns, so the database filled them in, and the model object never found out. If you return that model straight from an API controller, your JSON says null while the database says something else. Putting the same defaults in $attributes removes the mismatch, and it also makes the value available on a fresh new Flight for forms and validation.
The one rule: use the raw, storable format
The docs say values in $attributes must be in the raw format, as if just read from the database. That is why the example uses the string '[]', not an empty array. If you cast the column, Laravel converts the raw value when you read it:
class Flight extends Model
{
protected $attributes = [
'options' => '[]',
'delayed' => false,
'status' => 'scheduled',
];
protected function casts(): array
{
return [
'options' => 'array',
'delayed' => 'boolean',
'status' => FlightStatus::class, // a backed enum
];
}
}
In my test, a new instance returned [] for options, false for delayed, and the enum case for status, and a plain save() wrote '[]', 0 and 'scheduled' to the row. The mistake to avoid is putting [] (a real array) in $attributes next to an array cast. I tried it, and just reading the attribute threw a TypeError from json_decode() because it received an array instead of a string. Use '[]'.
What defaults do not do
These are the behaviors that surprise people, all confirmed by running them:
| Situation | What happens |
|---|---|
Flight::create(['delayed' => true]) |
Your value wins over the default |
Flight::create(['status' => null]) on a NOT NULL column |
The default is replaced by null and the database rejects it (integrity constraint error, column cannot be null) |
Flight::create(['options' => null]) on a nullable column |
null is stored |
Flight::select('id')->first() |
Defaults are not added. Only id is present and $flight->delayed is null
|
| Loading a row from the database | Database values are used, the defaults do not override them |
The second row is the important one. A model default only applies when the attribute is not set. If a request passes an explicit null, it wins. Keep your validation in place and do not treat $attributes as a safety net.
When a default needs to be calculated
The $attributes property holds fixed values only. A token, a reference number or anything based on the current time needs code. A creating event works (I tested the same pattern with a generated code and it was saved on insert):
use Illuminate\Support\Str;
protected static function booted(): void
{
static::creating(function (Flight $flight) {
$flight->code ??= 'FL-' . strtoupper(Str::random(6));
});
}
For UUID or ULID primary keys, Laravel has dedicated traits (HasUuids and HasUlids), so you do not need this pattern for IDs.
Model default or database default?
You can use both, and for important columns I would. They protect different things:
-
Database default: protects the data from every writer, including raw queries, imports, other apps and bulk
insert()calls that skip models. -
$attributes: makes the model object correct right away, with no refresh and no extra query.
Just keep the two values identical. If they drift apart, the model and the database will disagree about what "default" means, and that is a very hard bug to spot.
References
Originally published on DEV Talk.
Top comments (0)