
Laravel automatycznie rzutuje kolumny dat w modelach na obiekty Carbon. Zapamiętaj trzy rzeczy: przechowuj wszystko w UTC i konwertuj dopiero przy wyświetlaniu za pomocą ->tz(), deklaruj własne kolumny w casts() i pamiętaj, że w Carbonie 3 metody diffIn* zwracają znakowaną liczbę zmiennoprzecinkową, a nie bezwzględną całkowitą.
Prawie każda aplikacja Laravel operuje na datach: data publikacji, koniec okresu próbnego, termin dostawy, odstęp między dwoma zdarzeniami. Laravel powierza tę pracę Carbonowi, rozszerzeniu klasy DateTime z PHP, i automatycznie rzutuje kolumny dat w modelach na instancje Carbona.
Artykuł ze ścieżki Programowanie webowe. Ten przewodnik obejmuje to, co robi się naprawdę: utworzenie daty, konwersję do właściwej strefy, obliczenie różnicy, wyświetlenie po francusku, pracę z Eloquentem i zamrożenie czasu w testach. Wszystkie wyniki pokazane niżej powstały na Laravelu 13.30.1 i Carbonie 3.13.2.
Co zmieniło się w Carbonie 3
Laravel 11, 12 i 13 dostarczają Carbona 3. Jeśli czytasz kod napisany pod Laravela 8 albo 9, dwie zmiany cię zaskoczą.
1. Metody diffIn* zwracają liczbę zmiennoprzecinkową, a nie całkowitą. W Carbonie 2 diffInDays() zwracało 113. W Carbonie 3 to samo wyrażenie zwraca dokładną wartość:
$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. Wynik ma znak. W Carbonie 2 różnica była domyślnie bezwzględna. W Carbonie 3 jest ujemna, gdy data przekazana w argumencie jest wcześniejsza:
$a->diffInDays($b); // 113.72916666666667
$b->diffInDays($a); // -113.72916666666667
$b->diffInDays($a, true); // 113.72916666666667 (absolute: true)W praktyce if ($date->diffInDays(now()) > 30) napisany pod Carbona 2 może po aktualizacji przestać się uruchamiać, bo wartość zrobiła się ujemna. To najdroższa pułapka tej migracji.
Przy okazji zniknęły floatDiffInDays() i jego warianty: dublowały nowe zachowanie.
Tworzenie daty
Dwa globalne helpery Laravela pokrywają większość potrzeb:
now(); // 2026-09-02 14:30:00 — bieżąca chwila
today(); // 2026-09-02 00:00:00 — dziś o północyDo całej reszty użyj fasady. Zwróć uwagę na przestrzeń nazw: Laravel udostępnia własną podklasę Illuminate\Support\Carbon, która dokłada kilka metod do Carbon\Carbon. To właśnie ją importuje się w aplikacji Laravel.
<?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); // godzina = teraz
Carbon::createMidnightDate(2026, 2, 1); // godzina = 00:00:00
Carbon::createFromTimeString('10:15:00'); // data = dziś
Carbon::createFromFormat('d/m/Y', '01/02/2026');createFromDate(2026, 2, 1) nie zeruje godziny: zachowuje bieżący czas. Testowane 2 września 2026 o 14:30, wywołanie zwraca 2026-02-01 14:30:00. Jeśli chcesz północ, użyj createMidnightDate().
Carbon::parse() przyjmuje niemal wszystko, łącznie z wyrażeniami po angielsku:
Carbon::parse('2026-09-02T14:30:00+02:00');
Carbon::parse('last day of February 2024'); // 2024-02-29 00:00:00Wyrażenia względne kuszą, ale są kruche: last day of next month zwraca co innego w zależności od dnia, w którym kod się wykonuje. Zostaw parse() dla ciągów z maszynowego źródła (ISO 8601, timestamp), a pozostałe daty buduj jawnie.
Strefy czasowe: jedyny model, który się broni
Reguła mieści się w jednym zdaniu: przechowuj wszystko w UTC, konwertuj dopiero przy wyświetlaniu. Laravel trzyma się jej domyślnie, bo config/app.php ustawia 'timezone' => 'UTC'. Nie zmieniaj tej wartości: decyduje o tym, co trafia do bazy, a baza w czasie lokalnym staje się nie do opanowania przy pierwszej zmianie czasu.
Konwersja odbywa się na instancji, w momencie wyświetlania:
$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:00Te trzy obiekty opisują tę samą chwilę. Łatwo to sprawdzić: mają identyczny timestamp uniksowy. Zmiana strefy nie przesuwa momentu, zmienia tylko sposób jego odczytu.
Trzeba za to odróżnić dwa podobnie wyglądające zapisy:
Carbon::tomorrow('Europe/Paris'); // północ w Paryżu
Carbon::tomorrow()->tz('Europe/Paris'); // północ UTC odczytana w czasie paryskim (02:00)Pierwszy tworzy datę w danej strefie. Drugi tworzy datę w UTC, a potem ją konwertuje. Lista akceptowanych stref pochodzi z PHP, 419 pozycji na PHP 8.4.25:
timezone_identifiers_list(); // ['Africa/Abidjan', 'Africa/Accra', …]Żeby zapisać strefę użytkownika, dodaj kolumnę timezone do tabeli users i wypełniaj ją z listy rozwijanej zbudowanej na tym zestawie. Pakiet w rodzaju jamesmills/laravel-timezone automatyzuje wykrywanie przy logowaniu, ale w większości przypadków wystarczą kolumna i formularz.
Dodawanie i odejmowanie
Każda jednostka ma metodę w liczbie pojedynczej i w mnogiej, zarówno przy dodawaniu, jak i przy odejmowaniu:
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); // pomija sobotę i niedzielęDwa zachowania warto poznać, zanim odkryjesz je na produkcji.
Carbon jest mutowalny. addMonth() modyfikuje obiekt, na którym go wywołujesz, i nie zwraca kopii:
$date = Carbon::create(2026, 1, 31);
$date->addMonth();
echo $date; // 2026-03-03 — $date się zmieniła
$date = Carbon::create(2026, 1, 31);
echo $date->copy()->addMonth(); // 2026-03-03
echo $date; // 2026-01-31 — $date bez zmianMiesiące się przelewają. 31 stycznia plus miesiąc daje 3 marca, bo 31 lutego nie istnieje, a Carbon przenosi nadmiar dalej. Jeśli chodzi ci o koniec miesiąca, poproś o niego wprost:
Carbon::create(2026, 1, 31)->copy()->addMonth(); // 2026-03-03
Carbon::create(2026, 1, 31)->copy()->addMonthNoOverflow(); // 2026-02-28Jak uniknąć mutacji dzięki CarbonImmutable
Zamiast rozsiewać copy() po całym kodzie, można uczynić wszystkie daty w aplikacji niemutowalnymi. Każda operacja zwraca wtedy nową instancję, a oryginał nigdy się nie rusza:
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;
public function boot(): void
{
Date::use(CarbonImmutable::class);
}Po tym wywołaniu now() zwraca instancję Carbon\CarbonImmutable, tak samo jak daty hydrowane przez Eloquent. To globalna zmiana zachowania: decyduje się o niej na początku projektu, a nie w aplikacji naszpikowanej wywołaniami addDay(), które liczą na mutację.
Obliczanie różnicy między dwiema datami
Przy znakowanych i zmiennoprzecinkowych wartościach Carbona 3 zawsze doprecyzuj, czego oczekujesz:
$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); // wartość bezwzględna
(int) $debut->diffInDays($fin); // 113, pełne dni
round($debut->diffInHours($fin), 2); // 2729.5
$debut->diffInWeekdays($fin); // 82, bez weekendów
$debut->diffInMonths($fin); // 3.733198924731183Do czytelnego wyświetlenia sformułowaniem zajmie się diffForHumans(), również po francusku:
$fin->diffForHumans($debut); // '3 months after'
$fin->locale('fr')->diffForHumans($debut); // '3 mois après'
$fin->locale('fr')->diffForHumans(); // 'dans 3 mois' (względem teraz)Wyświetlanie daty po francusku
format() korzysta z kodów PHP i niczego nie tłumaczy: nazwy dni i miesięcy zostają po angielsku. Do francuskiego potrzebujesz translatedFormat() albo 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() zachowuje kody PHP i tłumaczy etykiety. isoFormat() używa kodów w rodzaju LLLL, LL czy dddd, które dostosowują się do konwencji każdego języka. Żeby nie pisać locale('fr') w kółko, ustaw locale aplikacji raz:
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FRCarbon w modelach Eloquent
created_at i updated_at są rzutowane automatycznie, pod warunkiem że kolumny faktycznie istnieją w bazie. Wracają jako instancje Illuminate\Support\Carbon, a nie jako ciągi znaków.
$post = Post::first();
$post->created_at->locale('fr')->isoFormat('LL'); // '2 septembre 2026'
$post->created_at->diffForHumans();
$post->created_at->isToday();W widoku Blade te wywołania zapisuje się tak samo, w klamrach.
Własne kolumny, dodane migracją, zadeklaruj w metodzie casts(). Uwaga: dawna właściwość protected $dates = [...] została usunięta z Laravela 10 i nie robi już nic. Kod, który wciąż na niej polega, zostawi ci ciągi znaków tam, gdzie spodziewasz się obiektów.
protected function casts(): array
{
return [
'published_at' => 'datetime',
'expires_at' => 'datetime:Y-m-d',
'starts_at' => 'immutable_datetime',
];
}Format podany po datetime: nie zmienia sposobu zapisu w bazie, tylko serializację do JSON-a. Porównania dat zapisuje się wtedy naturalnie w zapytaniach Eloquent:
Post::where('published_at', '<=', now())->get();
Post::whereDate('published_at', today())->get();
Post::whereBetween('published_at', [now()->subWeek(), now()])->get();Zamrażanie czasu w testach
Test zależny od rzeczywistego zegara prędzej czy później wywali się 29 lutego albo tuż po północy. Carbon potrafi skłamać na temat bieżącej daty, a Laravel udostępnia do tego helpery:
$this->travelTo(Carbon::parse('2026-09-02 14:30'));
expect(now()->toDateTimeString())->toBe('2026-09-02 14:30:00');
$this->travel(5)->days(); // przesuwa zegar o 5 dni
$this->travelBack(); // powrót do rzeczywistego czasuPoza testami Laravela surowym odpowiednikiem jest Carbon::setTestNow(), który anuluje się wywołaniem bez argumentu. To właśnie ten mechanizm posłużył do wyliczenia wartości z tego przewodnika, wszystkich liczonych od 2 września 2026, godzina 14:30 UTC.
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow(); // zwalnia zegarCzęste błędy
absolute: true albo rzutuj na (int).$date->addMonth() zmienia $date. Wywołaj copy() przed operacją albo przełącz aplikację na CarbonImmutable przez Date::use().addMonthNoOverflow(), żeby zostać w tym samym miesiącu.casts(), inaczej operujesz na ciągach znaków.createMidnightDate() daje faktycznie 00:00:00.translatedFormat() albo isoFormat().

