Carbon w Laravelu: daty i strefy czasowe w praktyce

Carbon w Laravelu: daty i strefy czasowe w praktyce
Szybka odpowiedź

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ść:

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. 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:

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

php
now();     // 2026-09-02 14:30:00  — bieżąca chwila
today();   // 2026-09-02 00:00:00  — dziś o północy

Do 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.

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);                     // 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');
Uwaga na createFromDate()

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:

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

Wyraż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:

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

Te 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:

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

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

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);   // 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:

php
$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 zmian

Miesią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:

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

Jak 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:

app/Providers/AppServiceProvider.php
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:

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);        // 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.733198924731183

Do czytelnego wyświetlenia sformułowaniem zajmie się diffForHumans(), również po francusku:

php
$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().

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() 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:

.env
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FR

Carbon 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.

php
$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.

app/Models/Post.php
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:

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

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();   // przesuwa zegar o 5 dni
$this->travelBack();        // powrót do rzeczywistego czasu

Poza 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.

php
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow();   // zwalnia zegar

Częste błędy

diffInDays() zwraca znakowaną liczbę zmiennoprzecinkową Od Carbona 3 różnica jest ujemna, gdy argument wskazuje wcześniejszą datę, i ma część dziesiętną. Warunek napisany pod Carbona 2 może przestać się uruchamiać: przekaż absolute: true albo rzutuj na (int).
Carbon jest mutowalny $date->addMonth() zmienia $date. Wywołaj copy() przed operacją albo przełącz aplikację na CarbonImmutable przez Date::use().
Miesiące się przelewają 31 stycznia + 1 miesiąc daje 3 marca, a nie 28 lutego. Użyj addMonthNoOverflow(), żeby zostać w tym samym miesiącu.
Właściwość $dates już nic nie robi Usunięta w Laravelu 10. Zadeklaruj kolumny dat w metodzie casts(), inaczej operujesz na ciągach znaków.
createFromDate() nie zeruje godziny Zachowuje bieżący czas. createMidnightDate() daje faktycznie 00:00:00.
format() nie tłumaczy Nazwy dni i miesięcy zostają po angielsku. Użyj translatedFormat() albo isoFormat().

CarbonLaravelPHP

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.