Carbon in Laravel: come gestire date, ore e fusi orari

Carbon in Laravel: come gestire date, ore e fusi orari
Risposta rapida

Laravel converte automaticamente le colonne di tipo data dei tuoi modelli in oggetti Carbon. Ricorda tre cose: salva tutto in UTC e converti solo al momento di mostrare il dato con ->tz(), dichiara le tue colonne nel metodo casts() e tieni presente che in Carbon 3 i metodi diffIn* restituiscono un float con segno e non più un intero assoluto.

Quasi tutte le applicazioni Laravel manipolano date: una data di pubblicazione, la fine di un periodo di prova, un termine di consegna, l’intervallo tra due eventi. Laravel affida questo lavoro a Carbon, un’estensione della classe DateTime di PHP, e converte automaticamente le colonne di tipo data dei tuoi modelli in istanze di Carbon.

Articolo del percorso Sviluppo web. Questa guida copre quello che farai davvero: creare una data, convertirla nel fuso giusto, calcolare un intervallo, mostrarla in francese, usarla in Eloquent e congelarla nei test. Tutti i risultati riportati qui sotto sono stati prodotti con Laravel 13.30.1 e Carbon 3.13.2.

Cosa è cambiato con Carbon 3

Laravel 11, 12 e 13 includono Carbon 3. Se rileggi del codice scritto per Laravel 8 o 9, due cambiamenti ti coglieranno di sorpresa.

1. I metodi diffIn* restituiscono un numero in virgola mobile, non più un intero. In Carbon 2, diffInDays() restituiva 113. In Carbon 3 la stessa espressione restituisce il valore esatto:

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. Il risultato ha un segno. In Carbon 2 la differenza era assoluta per impostazione predefinita. In Carbon 3 diventa negativa quando la data passata come argomento è la più vecchia:

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

In pratica, un if ($date->diffInDays(now()) > 30) scritto per Carbon 2 può smettere di scattare dopo un aggiornamento, perché il valore è diventato negativo. È la trappola più costosa della migrazione.

Nel frattempo floatDiffInDays() e le sue varianti sono sparite: duplicavano il nuovo comportamento.

Creare una data

I due helper globali di Laravel coprono la maggior parte delle esigenze:

php
now();     // 2026-09-02 14:30:00  — istante attuale
today();   // 2026-09-02 00:00:00  — oggi a mezzanotte

Per tutto il resto passa dalla facade. Attenzione al namespace: Laravel espone una propria sottoclasse, Illuminate\Support\Carbon, che aggiunge qualche metodo a Carbon\Carbon. È quella da importare in un’applicazione 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);                     // ora = adesso
Carbon::createMidnightDate(2026, 2, 1);                 // ora = 00:00:00
Carbon::createFromTimeString('10:15:00');               // data = oggi
Carbon::createFromFormat('d/m/Y', '01/02/2026');
Attenzione a createFromDate()

createFromDate(2026, 2, 1) non azzera l’ora: mantiene l’ora corrente. Provata il 2 settembre 2026 alle 14:30, la chiamata restituisce 2026-02-01 14:30:00. Usa createMidnightDate() se vuoi la mezzanotte.

Carbon::parse() accetta più o meno qualsiasi cosa, comprese espressioni in inglese:

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

Queste espressioni relative sono allettanti ma fragili: last day of next month non restituisce la stessa cosa a seconda del giorno in cui il codice gira. Riserva parse() alle stringhe che arrivano da una sorgente macchina (ISO 8601, timestamp) e costruisci le altre date in modo esplicito.

Fusi orari: l’unico modello che regge

La regola sta in una frase: salva tutto in UTC, converti solo quando visualizzi. Laravel la rispetta di default, con config/app.php che imposta 'timezone' => 'UTC'. Non toccare quel valore: decide cosa viene scritto nel database, e un database in ora locale diventa ingestibile al primo cambio dell’ora.

La conversione avviene sull’istanza, al momento di mostrare il dato:

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

Questi tre oggetti descrivono lo stesso istante. È verificabile: il loro timestamp Unix è identico. Cambiare fuso non sposta il momento, cambia il modo di leggerlo.

Vanno però distinte due scritture che si somigliano:

php
Carbon::tomorrow('Europe/Paris');        // mezzanotte a Parigi
Carbon::tomorrow()->tz('Europe/Paris');  // mezzanotte UTC, letta nell'ora di Parigi (02:00)

La prima crea una data nel fuso. La seconda crea una data in UTC e poi la converte. L’elenco dei fusi accettati è quello di PHP, 419 voci su PHP 8.4.25:

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

Per salvare il fuso di un utente, aggiungi una colonna timezone alla tabella users e riempila da un menu a tendina costruito su questo elenco. Un pacchetto come jamesmills/laravel-timezone automatizza il rilevamento al login, ma nella maggior parte dei casi bastano una colonna e un form.

Aggiungere e sottrarre

Ogni unità ha un metodo al singolare e uno al plurale, sia in aggiunta sia in sottrazione:

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);   // ignora sabato e domenica

Ci sono due comportamenti da conoscere prima di scoprirli in produzione.

Carbon è mutabile. addMonth() modifica l’oggetto su cui la chiami, non restituisce una copia:

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

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

I mesi vanno in overflow. Il 31 gennaio più un mese dà il 3 marzo, perché il 31 febbraio non esiste e Carbon riporta l’eccedenza. Se vuoi la fine del mese, chiedila:

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

Evitare le mutazioni con CarbonImmutable

Invece di disseminare copy() ovunque, puoi rendere immutabili tutte le date dell’applicazione. Ogni operazione restituisce allora una nuova istanza e l’originale non si muove mai:

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

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

Dopo questa chiamata now() restituisce un’istanza di Carbon\CarbonImmutable, e lo stesso vale per le date idratate da Eloquent. È un cambio di comportamento globale: da decidere a inizio progetto, non su un’applicazione già scritta con degli addDay() che contano sulla mutazione.

Calcolare l’intervallo tra due date

Con i valori con segno e in virgola mobile di Carbon 3, di’ sempre esattamente cosa vuoi:

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);        // valore assoluto
(int) $debut->diffInDays($fin);        // 113, giorni interi
round($debut->diffInHours($fin), 2);   // 2729.5
$debut->diffInWeekdays($fin);          // 82, weekend esclusi
$debut->diffInMonths($fin);            // 3.733198924731183

Per una visualizzazione leggibile, diffForHumans() si occupa della formulazione, anche in francese:

php
$fin->diffForHumans($debut);                 // '3 months after'
$fin->locale('fr')->diffForHumans($debut);   // '3 mois après'
$fin->locale('fr')->diffForHumans();         // 'dans 3 mois' (rispetto ad adesso)

Mostrare una data in francese

format() usa i codici di PHP e non traduce nulla: i nomi dei giorni e dei mesi restano in inglese. Per il francese servono translatedFormat() o 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() mantiene i codici PHP e traduce le etichette. isoFormat() usa codici come LLLL, LL, dddd, che si adattano alle convenzioni di ogni lingua. Per evitare di scrivere locale('fr') ovunque, imposta una volta sola la locale dell’applicazione:

.env
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FR

Carbon nei modelli Eloquent

created_at e updated_at vengono convertite automaticamente, a patto che le colonne esistano davvero nel database. Arrivano già come istanze di Illuminate\Support\Carbon, non come stringhe.

php
$post = Post::first();

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

In una vista Blade queste chiamate si scrivono allo stesso modo, tra parentesi graffe.

Per le tue colonne, aggiunte da una migrazione, dichiarale nel metodo casts(). Attenzione: la vecchia proprietà protected $dates = [...] è stata rimossa da Laravel 10 e non ha più alcun effetto. Il codice che la usa ti lascerà delle stringhe dove ti aspettavi degli oggetti.

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

Il formato indicato dopo datetime: non cambia il salvataggio, solo la serializzazione in JSON. I confronti tra date si scrivono poi in modo naturale nelle query Eloquent:

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

Congelare il tempo nei test

Un test che dipende dall’ora reale prima o poi fallisce un 29 febbraio o subito dopo la mezzanotte. Carbon sa mentire sulla data corrente, e Laravel espone degli helper apposta:

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();   // sposta l'orologio avanti di 5 giorni
$this->travelBack();        // torna all'ora reale

Fuori dai test Laravel l’equivalente grezzo è Carbon::setTestNow(), da annullare con una chiamata senza argomenti. È il meccanismo usato per produrre i valori di questa guida, tutti calcolati a partire dal 2 settembre 2026 alle 14:30 UTC.

php
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow();   // libera l'orologio

Errori frequenti

diffInDays() restituisce un float con segno Da Carbon 3 la differenza è negativa se l'argomento è la data più vecchia, e porta dei decimali. Una condizione scritta per Carbon 2 può smettere di scattare: passa absolute: true o converti con (int).
Carbon è mutabile $date->addMonth() modifica $date. Chiama copy() prima dell'operazione, oppure porta l'applicazione su CarbonImmutable con Date::use().
I mesi vanno in overflow 31 gennaio + 1 mese dà il 3 marzo, non il 28 febbraio. Usa addMonthNoOverflow() per restare dentro il mese.
La proprietà $dates non fa più niente Rimossa in Laravel 10. Dichiara le colonne di tipo data nel metodo casts(), altrimenti maneggi delle stringhe.
createFromDate() non azzera l'ora Mantiene l'ora corrente. createMidnightDate() restituisce davvero 00:00:00.
format() non traduce I nomi dei giorni e dei mesi restano in inglese. Passa da translatedFormat() o isoFormat().

CarbonLaravelPHP

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.