Data e ora in PHP: formattare, fusi orari e DateTimeImmutable

Ottenere e formattare una data in PHP: impostare il fuso orario, formattare con date(), calcolare con DateTimeImmutable, i nomi di giorni e mesi in francese con IntlDateFormatter e le funzioni deprecate da abbandonare.

Data e ora in PHP: formattare, fusi orari e DateTimeImmutable
Risposta rapida

date() formatta un timestamp, DateTimeImmutable serve appena c'è un calcolo, IntlDateFormatter dà i nomi dei giorni e dei mesi in francese. Imposta il fuso in modo esplicito: senza date.timezone, PHP usa UTC. strftime() è deprecata da PHP 8.1 e il suo sostituto non è date(), che non traduce nulla.

Mostrare una data in PHP è semplice. Mostrare la data giusta, nel fuso giusto, in francese, e calcolare un intervallo senza sbagliare al cambio dell’ora è tutt’altra cosa. Questo articolo copre le funzioni di base, poi le classi DateTimeImmutable e IntlDateFormatter che risolvono i casi che date() non sa trattare. Tutti gli output sono stati prodotti su PHP 8.5.10.

Il fuso orario, prima di ogni altra cosa

È la causa di bug numero uno, e per giunta silenziosa. Se date.timezone non è impostato in php.ini, PHP usa UTC. Un sito francese mostra allora un orario sfasato di una o due ore a seconda della stagione, senza il minimo avviso.

php
echo ini_get('date.timezone'), "\n";        // (vuoto) su molte installazioni
echo date_default_timezone_get(), "\n";     // UTC

L’impostazione si fa in php.ini, oppure all’avvio dell’applicazione:

php
date_default_timezone_set('Europe/Paris');

La regola di fondo: memorizza in UTC, mostra nel fuso dell’utente. Un database che contiene orari locali diventa inutilizzabile appena un utente cambia fuso o scatta l’ora legale.

date() e time()

time() restituisce il numero di secondi trascorsi dal 1º gennaio 1970 a mezzanotte UTC. date() formatta un timestamp di questo tipo, o l’istante corrente se non gliene passi uno.

php
date_default_timezone_set('Europe/Paris');

echo date('d/m/Y'), "\n";           // 02/09/2026
echo date('d/m/Y H:i:s'), "\n";     // 02/09/2026 21:47:03
echo time(), "\n";                  // 1788378423

echo date('d/m/Y', 1645303037), "\n";  // 19/02/2022

I caratteri di formato più usati:

Data Ora
d giorno a 2 cifre (01 a 31) H ora su 24 h (00 a 23)
j giorno senza zero iniziale (1 a 31) h ora su 12 h (01 a 12)
m mese a 2 cifre (01 a 12) i minuti (00 a 59)
n mese senza zero iniziale (1 a 12) s secondi (00 a 59)
Y anno a 4 cifre A AM o PM
N giorno della settimana (1 = lunedì) P scarto, per esempio +02:00
t numero di giorni del mese U timestamp Unix
L 1 se l’anno è bisestile c formato ISO 8601 completo

Per inserire un carattere che è anche un codice di formato bisogna fargli l’escape con una barra rovesciata:

php
echo date('\L\e d/m/Y'), "\n";      // Le 02/09/2026
// senza barra rovesciata, «L» darebbe 0 o 1 ed «e» il nome del fuso

date() non parla francese

È la seconda trappola classica. I nomi dei giorni e dei mesi restituiti da date() sono sempre in inglese, e setlocale() non cambia nulla.

php
$ts = mktime(0, 0, 0, 4, 1, 2022);

echo date('l', $ts), "\n";                    // Friday
setlocale(LC_TIME, 'fr_FR.UTF-8', 'fr_FR');
echo date('l', $ts), "\n";                    // Friday — invariato

La soluzione è IntlDateFormatter, dell’estensione intl:

php
$formateur = new IntlDateFormatter(
    'fr_FR',
    IntlDateFormatter::FULL,
    IntlDateFormatter::NONE,
    'Europe/Paris',
    IntlDateFormatter::GREGORIAN,
);

echo $formateur->format($ts), "\n";   // vendredi 1 avril 2022

Per un formato personalizzato la classe accetta un pattern ICU, i cui codici non sono quelli di date():

php
$formateur->setPattern("EEEE d MMMM y '\u{e0}' HH'h'mm");
echo $formateur->format(new DateTimeImmutable('now', new DateTimeZone('Europe/Paris')));
// mercredi 2 septembre 2026 à 21h47

Se intl non è disponibile, una tabella di corrispondenze toglie d’impaccio per il francese, ma non regge su più lingue.

DateTimeImmutable, la classe da usare

Per tutto ciò che va oltre la semplice visualizzazione, le classi valgono più delle funzioni. DateTimeImmutable è preferibile a DateTime: i suoi metodi restituiscono un nuovo oggetto invece di modificare quello corrente, il che elimina le modifiche a distanza.

php
$paris = new DateTimeZone('Europe/Paris');
$date  = new DateTimeImmutable('2026-09-02 14:30:00', $paris);

echo $date->format('d/m/Y H:i P'), "\n";                     // 02/09/2026 14:30 +02:00
echo $date->modify('+3 days')->format('d/m/Y'), "\n";        // 05/09/2026
echo $date->format('d/m/Y'), "\n";                           // 02/09/2026 — invariato

// Con DateTime (mutabile), la terza riga mostrerebbe 05/09/2026
php
// Aggiungere e togliere durate
$dans30Mois = $date->add(new DateInterval('P30M'));
$ilYa2Sem   = $date->sub(new DateInterval('P2W'));

// Differenza tra due date
$ecart = $date->diff(new DateTimeImmutable('2026-12-25', $paris));
echo $ecart->days, " jours\n";
echo $ecart->format('%m mois et %d jours'), "\n";

// Una serie di date
$periode = new DatePeriod(
    $date,
    new DateInterval('P1D'),
    new DateTimeImmutable('2026-09-07', $paris),
);
foreach ($periode as $jour) {
    echo $jour->format('D d/m'), "\n";
}

La trappola dell’ultimo giorno del mese

Aggiungere un mese a una fine mese non dà quello che ti aspetti, e il comportamento è lo stesso con DateInterval e con l’aritmetica su mktime().

php
$fin = new DateTimeImmutable('2026-01-31');
echo $fin->add(new DateInterval('P1M'))->format('d/m/Y'), "\n";  // 03/03/2026

Il 31 febbraio non esiste: PHP lo fa traboccare su marzo. Per ottenere l’ultimo giorno del mese successivo devi chiederlo esplicitamente:

php
echo $fin->modify('last day of next month')->format('d/m/Y'), "\n";  // 28/02/2026

Un giorno non dura sempre 24 ore

Al passaggio all’ora legale un giorno ne dura 23, al ritorno all’ora solare, 25. Aggiungere 86400 secondi a un timestamp dà quindi un orario sbagliato.

php
$paris = new DateTimeZone('Europe/Paris');
$avant = new DateTimeImmutable('2026-03-28 12:00:00', $paris);
$apres = $avant->add(new DateInterval('P1D'));

echo $apres->format('d/m/Y H:i P'), "\n";   // 29/03/2026 12:00 +02:00
echo ($apres->getTimestamp() - $avant->getTimestamp()) / 3600, " heures\n";  // 23

DateInterval ragiona in giorni di calendario e restituisce correttamente mezzogiorno del giorno dopo, anche se sono passate solo 23 ore. È il comportamento giusto per un promemoria «domani a mezzogiorno», e quello sbagliato per una durata fatturata. Sono due nozioni distinte: P1D per un giorno di calendario, PT24H per ventiquattro ore.

Leggere una data inserita dall’utente

strtotime() capisce molte cose, ma interpreta i formati ambigui all’americana: 03/04/2026 diventa il 4 marzo, non il 3 aprile. Quando il formato è noto, createFromFormat() non lascia spazio a dubbi.

php
$date = DateTimeImmutable::createFromFormat(
    '!d/m/Y',                        // il «!» azzera l'ora a 00:00:00
    '03/04/2026',
    new DateTimeZone('Europe/Paris'),
);
echo $date->format('d F Y'), "\n";   // 03 April 2026

Senza il ! iniziale, i campi che non fornisci prendono il valore dell’istante corrente: due esecuzioni dello stesso codice danno due orari diversi.

Il punto più pericoloso: una data non valida non fallisce, trabocca in silenzio.

php
$saisie = '31/02/2026';
$date   = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $paris);

echo $date->format('d/m/Y'), "\n";                    // 03/03/2026
print_r(DateTimeImmutable::getLastErrors());          // ['warnings' => [10 => 'The parsed date was invalid'], …]
var_dump(checkdate(2, 31, 2026));                     // bool(false)

Devi quindi controllare getLastErrors() dopo ogni analisi, oppure confrontare la data riformattata con quella inserita:

php
function lireDate(string $saisie, DateTimeZone $fuseau): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $fuseau);

    if ($date === false) {
        return null;
    }

    $erreurs = DateTimeImmutable::getLastErrors();
    if ($erreurs !== false && ($erreurs['warning_count'] || $erreurs['error_count'])) {
        return null;
    }

    return $date->format('d/m/Y') === $saisie ? $date : null;
}

Da PHP 8.2, getLastErrors() restituisce false quando non c’è nulla da segnalare, invece di un array con i contatori a zero. Il controllo deve tenerne conto.

Novità recenti

PHP 8.4 ha aggiunto createFromTimestamp(), che accetta i timestamp frazionari:

php
$d = DateTimeImmutable::createFromTimestamp(1645303037.5);
echo $d->format('d/m/Y H:i:s.u P'), "\n";   // 19/02/2022 20:37:17.500000 +00:00

Prima bisognava passare da '@' . $ts o setTimestamp(), e i microsecondi andavano persi.

Le funzioni deprecate da non usare più

Diverse funzioni di data ancora presenti emettono un avviso di deprecazione. Su PHP 8.5.10 funzionano, ma la loro rimozione è già programmata.

Funzione Deprecata da Sostituto
strftime() PHP 8.1 IntlDateFormatter::format()
gmstrftime() PHP 8.1 IntlDateFormatter::format()
strptime() PHP 8.2 date_parse_from_format() o IntlDateFormatter::parse()
date_sunrise() / date_sunset() PHP 8.1 date_sun_info()
utf8_encode() PHP 8.2 mb_convert_encoding()

I messaggi esatti, rilevati sia su PHP 8.5.10 sia su PHP 8.4.25:

code
Deprecated: Function strftime() is deprecated since 8.1,
use IntlDateFormatter::format() instead

Deprecated: Function strptime() is deprecated since 8.2,
use date_parse_from_format() (for locale-independent parsing),
or IntlDateFormatter::parse() (for locale-dependent parsing) instead

Deprecated: Constant SUNFUNCS_RET_STRING is deprecated since 8.4,
as date_sunrise() and date_sunset() were deprecated in 8.1

strftime() era il modo abituale di ottenere una data in francese. Il suo sostituto non è date(), che non traduce nulla, ma IntlDateFormatter.

Al contrario, mktime(), checkdate(), getdate() e idate() non sono deprecate e funzionano normalmente. Restano utili per i casi semplici, anche se le classi coprono tutto quello che fanno.

Da ricordare

  • Imposta il fuso in modo esplicito, memorizza in UTC, mostra in ora locale.
  • date() per formattare un istante, DateTimeImmutable per qualsiasi calcolo.
  • IntlDateFormatter per i nomi dei giorni e dei mesi in francese.
  • Dopo createFromFormat(), controlla getLastErrors(): una data non valida trabocca in silenzio.
  • Un giorno di calendario non è 24 ore. P1D e PT24H non sono intercambiabili.

Per analizzare date a milioni in un file di importazione, vedi leggere file di grandi dimensioni con PHP, e la connessione a un database per memorizzarle in UTC.

Vedi anche validare una data con un’espressione regolare, gestire le date con Carbon in Laravel e l’hub Sviluppo web.

Errori frequenti

Fuso orario non impostato Senza date.timezone in php.ini, PHP ricade su UTC e mostra un orario sfasato senza il minimo avviso.
date('l') resta in inglese setlocale() non ha alcun effetto su date(). Il sostituto di strftime() è IntlDateFormatter, non date().
createFromFormat accetta una data non valida 31/02/2026 diventa 03/03/2026 con un semplice avviso in getLastErrors(). Va controllato dopo ogni analisi.
createFromFormat senza il «!» I campi non forniti prendono l'ora corrente: due esecuzioni dello stesso codice danno due risultati.
strtotime legge le date all'americana 03/04/2026 diventa il 4 marzo, non il 3 aprile.
Aggiungere un mese a una fine mese 31/01/2026 + P1M03/03/2026. Per l'ultimo giorno del mese, usa modify('last day of next month').
Un giorno non dura 24 ore Al cambio dell'ora, P1D avanza di un giorno di calendario anche se sono passate solo 23 ore. P1D e PT24H non sono intercambiabili.
getLastErrors restituisce false Da PHP 8.2, il metodo restituisce false quando non c'è nulla da segnalare, non più un array di contatori a zero.

DateDateTimeIntlPHP

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.