
Laravel automatically casts the date columns of your models into Carbon objects. Remember three things: store everything in UTC and convert only for display with ->tz(), declare your own columns in casts(), and keep in mind that in Carbon 3 the diffIn* methods return a signed float instead of an absolute integer.
Almost every Laravel application handles dates: a publication date, the end of a trial, a delivery deadline, the gap between two events. Laravel hands that job to Carbon, an extension of PHP’s DateTime class, and automatically casts the date columns of your models into Carbon instances.
Part of the Web development reading path. This guide covers what you will actually do: create a date, convert it to the right time zone, measure a gap, display it in French, work with it in Eloquent and freeze it in your tests. Every result shown below was produced on Laravel 13.30.1 and Carbon 3.13.2.
What changed in Carbon 3
Laravel 11, 12 and 13 ship with Carbon 3. If you are reading code written for Laravel 8 or 9, two changes will catch you out.
1. The diffIn* methods return a float, no longer an integer. In Carbon 2, diffInDays() returned 113. In Carbon 3, the same expression returns the exact value:
$a = Carbon::parse('2026-09-02 14:30');
$b = Carbon::parse('2026-12-25 08:00');
$a->diffInDays($b); // 113.72916666666667 (float)
(int) $a->diffInDays($b); // 1132. The result is signed. In Carbon 2 the difference was absolute by default. In Carbon 3 it comes back negative when the date passed as an argument is the earlier one:
$a->diffInDays($b); // 113.72916666666667
$b->diffInDays($a); // -113.72916666666667
$b->diffInDays($a, true); // 113.72916666666667 (absolute: true)In practice, an if ($date->diffInDays(now()) > 30) written for Carbon 2 can stop firing after an upgrade, because the value has turned negative. That is the most expensive trap of the migration.
Along the way, floatDiffInDays() and its variants are gone: they duplicated the new behaviour.
Creating a date
Laravel’s two global helpers cover most of what you need:
now(); // 2026-09-02 14:30:00 — the current instant
today(); // 2026-09-02 00:00:00 — today at midnightFor everything else, go through the facade. Mind the namespace: Laravel exposes its own subclass, Illuminate\Support\Carbon, which adds a few methods to Carbon\Carbon. That is the one to import in a Laravel application.
<?php
use Illuminate\Support\Carbon;
Carbon::now();
Carbon::today();
Carbon::tomorrow('Europe/Paris');
Carbon::yesterday();
Carbon::create(2026, 2, 1, 10, 0, 0, 'Europe/Paris'); // 2026-02-01 10:00:00
Carbon::createFromDate(2026, 2, 1); // time = now
Carbon::createMidnightDate(2026, 2, 1); // time = 00:00:00
Carbon::createFromTimeString('10:15:00'); // date = today
Carbon::createFromFormat('d/m/Y', '01/02/2026');createFromDate(2026, 2, 1) does not reset the time to zero: it keeps the current time. Tested on 2 September 2026 at 14:30, the call returns 2026-02-01 14:30:00. Use createMidnightDate() if you want midnight.
Carbon::parse() accepts just about anything, including plain English expressions:
Carbon::parse('2026-09-02T14:30:00+02:00');
Carbon::parse('last day of February 2024'); // 2024-02-29 00:00:00Those relative expressions are tempting but brittle: last day of next month does not return the same thing depending on the day the code runs. Keep parse() for strings coming from a machine source (ISO 8601, timestamp) and build the other dates explicitly.
Time zones: the only model that holds up
The rule fits in one sentence: store everything in UTC, convert only for display. Laravel follows it by default, with config/app.php setting 'timezone' => 'UTC'. Do not change that value: it decides what is written to the database, and a database kept in local time becomes unmanageable at the first daylight saving switch.
The conversion happens on the instance, at display time:
$moment = Carbon::create(2026, 7, 1, 12, 0, 0, 'UTC');
$moment->copy()->tz('Europe/Paris'); // 2026-07-01 14:00:00
$moment->copy()->setTimezone('Asia/Tokyo'); // 2026-07-01 21:00:00These three objects describe the same instant. You can check it: their Unix timestamp is identical. Changing the time zone does not move the moment, it changes the way you read it.
Two calls that look alike do need to be told apart, though:
Carbon::tomorrow('Europe/Paris'); // midnight in Paris
Carbon::tomorrow()->tz('Europe/Paris'); // midnight UTC, read in Paris time (02:00)The first creates a date in the time zone. The second creates a date in UTC and then converts it. The list of accepted time zones is PHP’s, 419 entries on PHP 8.4.25:
timezone_identifiers_list(); // ['Africa/Abidjan', 'Africa/Accra', …]To store a user’s time zone, add a timezone column to the users table and fill it from a drop-down built on that list. A package such as jamesmills/laravel-timezone automates detection at login, but a column and a form are enough in most cases.
Adding and subtracting
Every unit has a singular and a plural method, for adding as well as subtracting:
now()->addDay();
now()->addDays(30);
now()->subDay();
now()->subDays(30);
now()->addWeeks(2);
now()->addMonths(3);
now()->addYears(5);
now()->addHours(6);
now()->addMinutes(90);
now()->subWeekdays(3); // skips Saturday and SundayTwo behaviours are worth knowing before you meet them in production.
Carbon is mutable. addMonth() modifies the object you call it on, it does not return a copy:
$date = Carbon::create(2026, 1, 31);
$date->addMonth();
echo $date; // 2026-03-03 — $date has changed
$date = Carbon::create(2026, 1, 31);
echo $date->copy()->addMonth(); // 2026-03-03
echo $date; // 2026-01-31 — $date is untouchedMonths overflow. 31 January plus one month gives 3 March, because 31 February does not exist and Carbon carries the surplus over. If you want the end of the month, ask for it:
Carbon::create(2026, 1, 31)->copy()->addMonth(); // 2026-03-03
Carbon::create(2026, 1, 31)->copy()->addMonthNoOverflow(); // 2026-02-28Avoiding mutation with CarbonImmutable
Rather than scattering copy() calls everywhere, you can make every date in the application immutable. Each operation then returns a new instance and the original never moves:
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;
public function boot(): void
{
Date::use(CarbonImmutable::class);
}After that call, now() returns an instance of Carbon\CarbonImmutable, and so do the dates hydrated by Eloquent. It is a global change of behaviour: decide it at the start of a project, not on an application already written around addDay() calls that rely on mutation.
Measuring the gap between two dates
With the signed, floating-point values of Carbon 3, always spell out what you want:
$debut = Carbon::parse('2026-09-02 14:30');
$fin = Carbon::parse('2026-12-25 08:00');
$debut->diffInDays($fin); // 113.72916666666667
$debut->diffInDays($fin, true); // absolute value
(int) $debut->diffInDays($fin); // 113, whole days
round($debut->diffInHours($fin), 2); // 2729.5
$debut->diffInWeekdays($fin); // 82, weekends excluded
$debut->diffInMonths($fin); // 3.733198924731183For a readable output, diffForHumans() takes care of the wording, in French as well:
$fin->diffForHumans($debut); // '3 months after'
$fin->locale('fr')->diffForHumans($debut); // '3 mois après'
$fin->locale('fr')->diffForHumans(); // 'dans 3 mois' (relative to now)Displaying a date in French
format() uses the PHP codes and translates nothing: day and month names stay in English. For French you need translatedFormat() or isoFormat().
$d = Carbon::parse('2026-09-02 14:30');
$d->toDateTimeString(); // '2026-09-02 14:30:00'
$d->toDateString(); // '2026-09-02'
$d->toIso8601String(); // '2026-09-02T14:30:00+00:00'
$d->format('d/m/Y H:i'); // '02/09/2026 14:30'
$d->locale('fr')->translatedFormat('l j F Y'); // 'mercredi 2 septembre 2026'
$d->locale('fr')->isoFormat('LLLL'); // 'mercredi 2 septembre 2026 14:30'translatedFormat() keeps the PHP codes and translates the labels. isoFormat() uses codes such as LLLL, LL and dddd, which follow the conventions of each language. To avoid writing locale('fr') everywhere, set the application locale once:
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FRCarbon in Eloquent models
created_at and updated_at are cast automatically, provided the columns really do exist in the database. They come back as instances of Illuminate\Support\Carbon, not as strings.
$post = Post::first();
$post->created_at->locale('fr')->isoFormat('LL'); // '2 septembre 2026'
$post->created_at->diffForHumans();
$post->created_at->isToday();In a Blade view, the same calls are written the same way inside curly braces.
For your own columns, added by a migration, declare them in the casts() method. Careful: the old protected $dates = [...] property was removed in Laravel 10 and does nothing at all any more. Code that still relies on it will leave you with strings where you expected objects.
protected function casts(): array
{
return [
'published_at' => 'datetime',
'expires_at' => 'datetime:Y-m-d',
'starts_at' => 'immutable_datetime',
];
}The format passed after datetime: does not change how the value is stored, only how it is serialised to JSON. Date comparisons are then written naturally in Eloquent queries:
Post::where('published_at', '<=', now())->get();
Post::whereDate('published_at', today())->get();
Post::whereBetween('published_at', [now()->subWeek(), now()])->get();Freezing time in tests
A test that depends on the real clock always ends up failing on a 29 February or just after midnight. Carbon can lie about the current date, and Laravel exposes helpers for it:
$this->travelTo(Carbon::parse('2026-09-02 14:30'));
expect(now()->toDateTimeString())->toBe('2026-09-02 14:30:00');
$this->travel(5)->days(); // moves the clock forward by 5 days
$this->travelBack(); // back to the real clockOutside Laravel tests, the raw equivalent is Carbon::setTestNow(), which you cancel with a call without arguments. That is the mechanism used to produce the values in this guide, all computed from 2 September 2026 at 14:30 UTC.
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow(); // releases the clockCommon errors
absolute: true or cast to (int).$date->addMonth() modifies $date. Call copy() before the operation, or switch the application to CarbonImmutable with Date::use().addMonthNoOverflow() to stay inside the month.casts() method, otherwise you are handling strings.createMidnightDate() does give 00:00:00.translatedFormat() or isoFormat() instead.

