Carbon in Laravel: datums en tijdzones goed afhandelen

Carbon in Laravel: datums en tijdzones goed afhandelen
Kort antwoord

Laravel zet de datumkolommen van je modellen automatisch om naar Carbon-objecten. Onthoud drie dingen: sla alles op in UTC en reken pas bij het weergeven om met ->tz(), declareer je eigen kolommen in casts(), en weet dat de methoden diffIn* in Carbon 3 een kommagetal met teken teruggeven en geen absoluut geheel getal meer.

Bijna elke Laravel-applicatie rekent met datums: een publicatiedatum, het einde van een proefperiode, een levertermijn, het verschil tussen twee gebeurtenissen. Laravel laat dat werk over aan Carbon, een uitbreiding van de PHP-klasse DateTime, en zet de datumkolommen van je modellen automatisch om naar Carbon-instanties.

Artikel uit het leertraject Webontwikkeling. Deze gids behandelt wat je in de praktijk echt doet: een datum maken, hem naar de juiste tijdzone omrekenen, een verschil berekenen, hem in het Frans weergeven, hem in Eloquent gebruiken en hem in je tests bevriezen. Alle resultaten hieronder komen van Laravel 13.30.1 en Carbon 3.13.2.

Wat er met Carbon 3 is veranderd

Laravel 11, 12 en 13 leveren Carbon 3 mee. Sla je code open die voor Laravel 8 of 9 is geschreven, dan zullen twee wijzigingen je verrassen.

1. De methoden diffIn* geven een kommagetal terug, geen integer meer. In Carbon 2 gaf diffInDays() nog 113 terug. In Carbon 3 levert dezelfde expressie de exacte waarde:

php
$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);   // 113

2. Het resultaat heeft een teken. In Carbon 2 was het verschil standaard absoluut. In Carbon 3 wordt het negatief zodra de meegegeven datum eerder ligt:

php
$a->diffInDays($b);         //  113.72916666666667
$b->diffInDays($a);         // -113.72916666666667
$b->diffInDays($a, true);   //  113.72916666666667   (absolute: true)

Concreet: een if ($date->diffInDays(now()) > 30) die voor Carbon 2 is geschreven, kan na een update niet meer afgaan omdat de waarde negatief is geworden. Dat is de duurste valkuil van de migratie.

Meteen ook: floatDiffInDays() en zijn varianten zijn verdwenen, want ze deden dubbel werk met het nieuwe gedrag.

Een datum maken

De twee globale helpers van Laravel dekken de meeste behoeften:

php
now();     // 2026-09-02 14:30:00  — huidig tijdstip
today();   // 2026-09-02 00:00:00  — vandaag om middernacht

Voor al de rest gebruik je de facade. Let op de namespace: Laravel biedt een eigen subklasse aan, Illuminate\Support\Carbon, die een paar methoden toevoegt aan Carbon\Carbon. Die is het die je in een Laravel-applicatie importeert.

app/Services/Planning.php
<?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);                     // tijd = nu
Carbon::createMidnightDate(2026, 2, 1);                 // tijd = 00:00:00
Carbon::createFromTimeString('10:15:00');               // datum = vandaag
Carbon::createFromFormat('d/m/Y', '01/02/2026');
Pas op met createFromDate()

createFromDate(2026, 2, 1) zet de tijd niet op nul, maar houdt de huidige tijd aan. Getest op 2 september 2026 om 14:30 uur geeft de aanroep 2026-02-01 14:30:00 terug. Wil je middernacht, neem dan createMidnightDate().

Carbon::parse() slikt zowat alles, ook Engelse uitdrukkingen:

php
Carbon::parse('2026-09-02T14:30:00+02:00');
Carbon::parse('last day of February 2024');   // 2024-02-29 00:00:00

Die relatieve uitdrukkingen zijn verleidelijk maar broos: last day of next month geeft iets anders terug naargelang de dag waarop de code draait. Houd parse() voor strings uit een machinebron (ISO 8601, timestamp) en bouw de andere datums expliciet op.

Tijdzones: het enige model dat standhoudt

De regel past in één zin: sla alles op in UTC en reken pas om bij het weergeven. Laravel houdt zich daar standaard aan, config/app.php zet 'timezone' => 'UTC'. Laat die waarde staan: ze bepaalt wat er in de database wordt geschreven, en een database in lokale tijd wordt onbeheersbaar bij de eerste overgang naar zomertijd.

Omrekenen gebeurt op de instantie, op het moment van weergeven:

php
$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:00

Die drie objecten beschrijven hetzelfde tijdstip. Dat valt te controleren: hun Unix-timestamp is identiek. De tijdzone wijzigen verplaatst het moment niet, het verandert alleen hoe je het leest.

Twee schrijfwijzen die op elkaar lijken, moet je wel uit elkaar houden:

php
Carbon::tomorrow('Europe/Paris');        // middernacht in Parijs
Carbon::tomorrow()->tz('Europe/Paris');  // middernacht UTC, gelezen in Parijse tijd (02:00)

De eerste maakt een datum in die tijdzone. De tweede maakt een datum in UTC en zet hem daarna om. De lijst met aanvaarde tijdzones is die van PHP, 419 vermeldingen op PHP 8.4.25:

php
timezone_identifiers_list();   // ['Africa/Abidjan', 'Africa/Accra', …]

Wil je de tijdzone van een gebruiker opslaan, voeg dan een kolom timezone toe aan de tabel users en vul die vanuit een keuzelijst die op deze lijst is gebouwd. Een package als jamesmills/laravel-timezone automatiseert de detectie bij het inloggen, maar in de meeste gevallen volstaan een kolom en een formulier.

Optellen en aftrekken

Elke eenheid heeft een methode in het enkelvoud en een in het meervoud, zowel om op te tellen als om af te trekken:

php
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);   // slaat zaterdag en zondag over

Twee gedragingen kun je maar beter kennen voordat je ze in productie ontdekt.

Carbon is mutable. addMonth() wijzigt het object waarop je de methode aanroept, het geeft geen kopie terug:

php
$date = Carbon::create(2026, 1, 31);
$date->addMonth();
echo $date;   // 2026-03-03  —  $date is gewijzigd

$date = Carbon::create(2026, 1, 31);
echo $date->copy()->addMonth();   // 2026-03-03
echo $date;                       // 2026-01-31  —  $date is ongewijzigd

Maanden lopen over. 31 januari plus één maand geeft 3 maart, want 31 februari bestaat niet en Carbon schuift het overschot door. Wil je het einde van de maand, vraag er dan om:

php
Carbon::create(2026, 1, 31)->copy()->addMonth();            // 2026-03-03
Carbon::create(2026, 1, 31)->copy()->addMonthNoOverflow();  // 2026-02-28

Mutaties vermijden met CarbonImmutable

In plaats van overal copy() rond te strooien kun je alle datums van de applicatie immutable maken. Elke bewerking geeft dan een nieuwe instantie terug en het origineel verandert nooit:

app/Providers/AppServiceProvider.php
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;

public function boot(): void
{
    Date::use(CarbonImmutable::class);
}

Na die aanroep geeft now() een instantie van Carbon\CarbonImmutable terug, en de datums die Eloquent hydrateert net zo goed. Dat is een globale gedragsverandering: beslis erover bij de start van een project, niet in een applicatie die al vol staat met addDay()-aanroepen die op de mutatie rekenen.

Het verschil tussen twee datums berekenen

Nu de waarden in Carbon 3 een teken hebben en kommagetallen zijn, geef je altijd expliciet aan wat je wilt:

php
$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 waarde
(int) $debut->diffInDays($fin);        // 113, hele dagen
round($debut->diffInHours($fin), 2);   // 2729.5
$debut->diffInWeekdays($fin);          // 82, zonder weekends
$debut->diffInMonths($fin);            // 3.733198924731183

Voor een leesbare weergave verzorgt diffForHumans() de formulering, ook in het Frans:

php
$fin->diffForHumans($debut);                 // '3 months after'
$fin->locale('fr')->diffForHumans($debut);   // '3 mois après'
$fin->locale('fr')->diffForHumans();         // 'dans 3 mois' (ten opzichte van nu)

Een datum in het Frans weergeven

format() gebruikt de codes van PHP en vertaalt niets: dag- en maandnamen blijven Engels. Voor Frans heb je translatedFormat() of isoFormat() nodig.

php
$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() houdt de PHP-codes aan en vertaalt de labels. isoFormat() gebruikt codes van het type LLLL, LL, dddd, die zich aanpassen aan de conventies van elke taal. Om niet overal locale('fr') te hoeven schrijven, stel je de locale van de applicatie één keer in:

.env
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FR

Carbon in Eloquent-modellen

created_at en updated_at worden automatisch omgezet, op voorwaarde dat de kolommen echt in de database staan. Het zijn meteen instanties van Illuminate\Support\Carbon, geen strings.

php
$post = Post::first();

$post->created_at->locale('fr')->isoFormat('LL');   // '2 septembre 2026'
$post->created_at->diffForHumans();
$post->created_at->isToday();

In een Blade-view schrijf je diezelfde aanroepen tussen accolades.

Je eigen kolommen, toegevoegd via een migratie, declareer je in de methode casts(). Let op: de oude eigenschap protected $dates = [...] is uit Laravel 10 verwijderd en doet niets meer. Code die daarop leunt, laat je met strings zitten waar je objecten verwachtte.

app/Models/Post.php
protected function casts(): array
{
    return [
        'published_at' => 'datetime',
        'expires_at'   => 'datetime:Y-m-d',
        'starts_at'    => 'immutable_datetime',
    ];
}

Het formaat achter datetime: verandert niets aan de opslag, alleen aan de serialisatie naar JSON. Datumvergelijkingen schrijf je dan vanzelf in je Eloquent-queries:

php
Post::where('published_at', '<=', now())->get();
Post::whereDate('published_at', today())->get();
Post::whereBetween('published_at', [now()->subWeek(), now()])->get();

De tijd bevriezen in je tests

Een test die van de echte klok afhangt, faalt vroeg of laat op 29 februari of net na middernacht. Carbon kan liegen over de huidige datum, en Laravel biedt daar helpers voor:

tests/Feature/TrialTest.php
$this->travelTo(Carbon::parse('2026-09-02 14:30'));

expect(now()->toDateTimeString())->toBe('2026-09-02 14:30:00');

$this->travel(5)->days();   // zet de klok 5 dagen vooruit
$this->travelBack();        // keert terug naar de echte tijd

Buiten Laravel-tests is Carbon::setTestNow() het ruwe equivalent, op te heffen met een aanroep zonder argument. Met dat mechanisme zijn de waarden in deze gids geproduceerd, allemaal berekend vanaf 2 september 2026 om 14:30 uur UTC.

php
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow();   // geeft de klok weer vrij

Veelgemaakte fouten

diffInDays() geeft een kommagetal met teken terug Sinds Carbon 3 is het verschil negatief als het argument eerder ligt, en heeft het decimalen. Een voorwaarde die voor Carbon 2 is geschreven, kan stoppen met afgaan: geef absolute: true mee of cast naar (int).
Carbon is mutable $date->addMonth() wijzigt $date zelf. Gebruik copy() vóór de bewerking, of zet de applicatie via Date::use() om op CarbonImmutable.
Maanden lopen over 31 januari plus één maand geeft 3 maart, niet 28 februari. Gebruik addMonthNoOverflow() om binnen de maand te blijven.
De eigenschap $dates doet niets meer Geschrapt in Laravel 10. Declareer je datumkolommen in de methode casts(), anders werk je met strings.
createFromDate() zet de tijd niet op nul Ze houdt de huidige tijd aan. createMidnightDate() geeft wel 00:00:00.
format() vertaalt niet Dag- en maandnamen blijven Engels. Ga via translatedFormat() of isoFormat().

CarbonLaravelPHP

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.