In an earlier tip, I showed that Eloquent's date cast only strips the time when you read a value.
On write, the full time still goes to the database, and a DATETIME column keeps it.
The best fix is a DATE column. But sometimes you can't change the schema, like with a legacy database or a table another system owns.
In that case, a small custom cast can fix the asymmetry.
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
class DateOnly implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon
{
return $value === null ? null : Carbon::parse($value)->startOfDay();
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
{
return $value === null ? null : Carbon::parse($value)->toDateString();
}
}
The get method behaves like the built-in cast and hands you a Carbon instance at midnight.
The set method is the key part that Laravel's cast skips.
It drops the time before the value reaches the database.
protected function casts(): array
{
return [
'start_date' => DateOnly::class,
];
}
$record->start_date = '2023-05-15 14:30:45';
$record->save();
DB::table('events')->where('id', $record->id)->value('start_date');
// 2023-05-15 00:00:00
Now, what Eloquent shows you actually matches what the database holds.
It's also key to not skip the null checks.
Carbon::parse(null) returns the current time, so without them a nullable column would quietly fill up with today's date.
Here to help,
Joel
P.S. A test that reads the raw column through the DB facade catches this kind of mismatch right away. Our testing workshop shows you how to write tests that check what actually gets stored.