Carbon in Laravel: Datum und Zeitzonen richtig handhaben

Carbon in Laravel: Datum und Zeitzonen richtig handhaben
Schnelle Antwort

Laravel wandelt die Datumsspalten deiner Modelle automatisch in Carbon-Objekte um. Merke dir drei Dinge: Speichere alles in UTC und rechne erst bei der Ausgabe mit ->tz() um, deklariere eigene Spalten in casts(), und rechne damit, dass die diffIn*-Methoden seit Carbon 3 einen vorzeichenbehafteten Float statt eines absoluten Integers liefern.

Fast jede Laravel-Anwendung rechnet mit Datumswerten: ein Veröffentlichungsdatum, das Ende einer Testphase, eine Lieferfrist, der Abstand zwischen zwei Ereignissen. Laravel überlässt diese Arbeit Carbon, einer Erweiterung der PHP-Klasse DateTime, und wandelt die Datumsspalten deiner Modelle automatisch in Carbon-Instanzen um.

Artikel aus dem Lernpfad Webentwicklung. Dieser Leitfaden behandelt, was in der Praxis wirklich anfällt: ein Datum erzeugen, es in die richtige Zeitzone umrechnen, einen Abstand berechnen, es auf Französisch ausgeben, es in Eloquent verarbeiten und es in Tests einfrieren. Alle unten gezeigten Ergebnisse stammen aus Laravel 13.30.1 und Carbon 3.13.2.

Was sich mit Carbon 3 geändert hat

Laravel 11, 12 und 13 bringen Carbon 3 mit. Wenn du Code wieder aufschlägst, der für Laravel 8 oder 9 geschrieben wurde, werden dich zwei Änderungen überraschen.

1. Die diffIn*-Methoden liefern eine Fließkommazahl, keinen Integer mehr. In Carbon 2 gab diffInDays() noch 113 zurück. In Carbon 3 liefert derselbe Ausdruck den exakten Wert:

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. Das Ergebnis hat ein Vorzeichen. In Carbon 2 war die Differenz standardmäßig absolut. In Carbon 3 wird sie negativ, sobald das übergebene Datum weiter zurückliegt:

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

Konkret: Ein für Carbon 2 geschriebenes if ($date->diffInDays(now()) > 30) greift nach dem Update womöglich nicht mehr, weil der Wert negativ geworden ist. Das ist die teuerste Falle der Migration.

Nebenbei sind floatDiffInDays() und seine Varianten verschwunden: Sie waren durch das neue Verhalten überflüssig geworden.

Ein Datum erzeugen

Die beiden globalen Helper von Laravel decken die meisten Fälle ab:

php
now();     // 2026-09-02 14:30:00  — aktueller Zeitpunkt
today();   // 2026-09-02 00:00:00  — heute um Mitternacht

Für alles Weitere nimmst du die Fassade. Achte auf den Namespace: Laravel stellt eine eigene Unterklasse bereit, Illuminate\Support\Carbon, die Carbon\Carbon um einige Methoden ergänzt. Genau die gehört in einer Laravel-Anwendung importiert.

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);                     // Uhrzeit = jetzt
Carbon::createMidnightDate(2026, 2, 1);                 // Uhrzeit = 00:00:00
Carbon::createFromTimeString('10:15:00');               // Datum = heute
Carbon::createFromFormat('d/m/Y', '01/02/2026');
Vorsicht bei createFromDate()

createFromDate(2026, 2, 1) setzt die Uhrzeit nicht auf null, sondern übernimmt die aktuelle Uhrzeit. Am 2. September 2026 um 14:30 Uhr getestet, liefert der Aufruf 2026-02-01 14:30:00. Wenn du Mitternacht willst, nimm createMidnightDate().

Carbon::parse() schluckt so ziemlich alles, auch englische Ausdrücke:

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

Diese relativen Ausdrücke sind verlockend, aber fragil: last day of next month liefert je nach Ausführungstag etwas anderes. Halte parse() für Strings aus maschinellen Quellen frei (ISO 8601, Timestamp) und baue alle anderen Datumswerte explizit auf.

Zeitzonen: das einzige Modell, das trägt

Die Regel passt in einen Satz: Speichere alles in UTC und rechne erst bei der Ausgabe um. Laravel hält sich standardmäßig daran, config/app.php setzt 'timezone' => 'UTC'. Lass diesen Wert stehen: Er bestimmt, was in die Datenbank geschrieben wird, und eine Datenbank in Ortszeit wird spätestens bei der ersten Zeitumstellung unbeherrschbar.

Umgerechnet wird auf der Instanz, im Moment der Ausgabe:

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

Diese drei Objekte beschreiben denselben Zeitpunkt. Das lässt sich prüfen: Ihr Unix-Timestamp ist identisch. Ein Zeitzonenwechsel verschiebt den Moment nicht, er ändert nur, wie man ihn liest.

Zwei sehr ähnliche Schreibweisen musst du dagegen auseinanderhalten:

php
Carbon::tomorrow('Europe/Paris');        // Mitternacht in Paris
Carbon::tomorrow()->tz('Europe/Paris');  // Mitternacht UTC, in Pariser Zeit gelesen (02:00)

Die erste erzeugt ein Datum in der Zeitzone. Die zweite erzeugt ein Datum in UTC und rechnet es anschließend um. Akzeptiert wird die Zeitzonenliste von PHP, 419 Einträge unter PHP 8.4.25:

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

Um die Zeitzone eines Nutzers zu speichern, ergänzt du die Tabelle users um eine Spalte timezone und befüllst sie über ein Auswahlfeld, das auf dieser Liste aufbaut. Ein Paket wie jamesmills/laravel-timezone automatisiert die Erkennung beim Login, aber in den meisten Fällen reichen eine Spalte und ein Formular.

Addieren und subtrahieren

Jede Einheit hat eine Methode im Singular und eine im Plural, jeweils zum Addieren und zum Subtrahieren:

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);   // überspringt Samstag und Sonntag

Zwei Verhaltensweisen solltest du kennen, bevor du sie in der Produktion entdeckst.

Carbon ist mutable. addMonth() verändert das Objekt, auf dem du die Methode aufrufst, und gibt keine Kopie zurück:

php
$date = Carbon::create(2026, 1, 31);
$date->addMonth();
echo $date;   // 2026-03-03  —  $date hat sich geändert

$date = Carbon::create(2026, 1, 31);
echo $date->copy()->addMonth();   // 2026-03-03
echo $date;                       // 2026-01-31  —  $date ist unverändert

Monate laufen über. Der 31. Januar plus ein Monat ergibt den 3. März, denn den 31. Februar gibt es nicht und Carbon schiebt den Überschuss weiter. Wenn du das Monatsende willst, verlange es ausdrücklich:

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

Mutationen mit CarbonImmutable vermeiden

Statt überall copy() zu verstreuen, kannst du sämtliche Datumswerte der Anwendung unveränderlich machen. Jede Operation liefert dann eine neue Instanz, das Original bleibt unangetastet:

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

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

Nach diesem Aufruf liefert now() eine Instanz von Carbon\CarbonImmutable, und die von Eloquent hydrierten Datumswerte ebenso. Das ist eine globale Verhaltensänderung: eine Entscheidung für den Projektstart, nicht für eine Anwendung, die längst mit addDay()-Aufrufen arbeitet, die auf die Mutation setzen.

Den Abstand zwischen zwei Datumswerten berechnen

Bei den vorzeichenbehafteten Fließkommawerten von Carbon 3 sagst du besser immer genau, was du haben willst:

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);        // Absolutwert
(int) $debut->diffInDays($fin);        // 113, ganze Tage
round($debut->diffInHours($fin), 2);   // 2729.5
$debut->diffInWeekdays($fin);          // 82, ohne Wochenenden
$debut->diffInMonths($fin);            // 3.733198924731183

Für eine lesbare Ausgabe übernimmt diffForHumans() die Formulierung, auch auf Französisch:

php
$fin->diffForHumans($debut);                 // '3 months after'
$fin->locale('fr')->diffForHumans($debut);   // '3 mois après'
$fin->locale('fr')->diffForHumans();         // 'dans 3 mois' (bezogen auf jetzt)

Ein Datum auf Französisch ausgeben

format() arbeitet mit den Codes von PHP und übersetzt nichts: Tages- und Monatsnamen bleiben englisch. Für Französisch brauchst du translatedFormat() oder isoFormat().

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() behält die PHP-Codes bei und übersetzt die Bezeichnungen. isoFormat() verwendet Codes wie LLLL, LL oder dddd, die sich an die Konventionen der jeweiligen Sprache anpassen. Damit du nicht überall locale('fr') schreiben musst, stellst du die Locale der Anwendung einmal zentral ein:

.env
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FR

Carbon in Eloquent-Modellen

created_at und updated_at werden automatisch umgewandelt, sofern die Spalten in der Datenbank auch wirklich existieren. Es sind dann bereits Instanzen von Illuminate\Support\Carbon, keine Strings.

php
$post = Post::first();

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

In einer Blade-View schreiben sich dieselben Aufrufe genauso, nur zwischen geschweiften Klammern.

Eigene Spalten, die du per Migration angelegt hast, deklarierst du in der Methode casts(). Achtung: Die alte Eigenschaft protected $dates = [...] wurde in Laravel 10 entfernt und hat keinerlei Wirkung mehr. Code, der noch darauf setzt, hinterlässt dir Strings, wo du Objekte erwartest.

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

Das Format hinter datetime: ändert nichts an der Speicherung, nur an der Serialisierung nach JSON. Datumsvergleiche schreiben sich damit ganz natürlich in Eloquent-Abfragen:

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

Die Zeit in Tests einfrieren

Ein Test, der von der echten Uhrzeit abhängt, fällt irgendwann an einem 29. Februar oder kurz nach Mitternacht um. Carbon kann über das aktuelle Datum lügen, und Laravel stellt dafür passende Helper bereit:

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();   // stellt die Uhr 5 Tage vor
$this->travelBack();        // zurück zur echten Uhrzeit

Außerhalb der Laravel-Tests ist Carbon::setTestNow() das nackte Äquivalent, ein Aufruf ohne Argument hebt es wieder auf. Genau dieser Mechanismus hat die Werte in diesem Leitfaden erzeugt, alle berechnet ab dem 2. September 2026, 14:30 Uhr UTC.

php
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow();   // gibt die Uhr wieder frei

Häufige Fehler

diffInDays() liefert einen vorzeichenbehafteten Float Seit Carbon 3 ist die Differenz negativ, wenn das Argument früher liegt, und hat Nachkommastellen. Eine für Carbon 2 geschriebene Bedingung greift womöglich nicht mehr: übergib absolute: true oder caste nach (int).
Carbon ist mutable $date->addMonth() verändert $date. Nutze copy() vor der Operation oder stelle die Anwendung über Date::use() auf CarbonImmutable um.
Monate laufen über 31. Januar + 1 Monat ergibt den 3. März, nicht den 28. Februar. Mit addMonthNoOverflow() bleibst du im Monat.
Die Eigenschaft $dates bewirkt nichts mehr In Laravel 10 entfernt. Deklariere deine Datumsspalten in der Methode casts(), sonst hantierst du mit Strings.
createFromDate() setzt die Uhrzeit nicht auf null Sie übernimmt die aktuelle Uhrzeit. createMidnightDate() liefert wirklich 00:00:00.
format() übersetzt nicht Tages- und Monatsnamen bleiben englisch. Nimm translatedFormat() oder isoFormat().

CarbonLaravelPHP

Damien Flandrin Webentwickler seit 2010, Gründer von Gekkode und Email Impact. Jeder Artikel wird vor der Veröffentlichung an einem echten Projekt getestet. Kontakt
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.