
date() met en forme un horodatage, DateTimeImmutable sert dès qu'il y a un calcul, IntlDateFormatter donne les noms de jours et de mois en français. Réglez le fuseau explicitement : sans date.timezone, PHP utilise UTC. strftime() est déprécié depuis PHP 8.1 et n'est pas remplacé par date(), qui ne traduit pas.
Afficher une date en PHP est simple. Afficher la bonne date, dans le bon fuseau, en français, et calculer un intervalle sans se tromper au passage à l’heure d’été, l’est nettement moins. Cet article couvre les fonctions de base, puis les classes DateTimeImmutable et IntlDateFormatter qui règlent les cas que date() ne sait pas traiter. Toutes les sorties ont été produites sur PHP 8.5.10.
Le fuseau horaire, avant tout le reste
C’est la source de bug numéro un, et elle est silencieuse. Si date.timezone n’est pas réglé dans php.ini, PHP utilise UTC. Un site français affiche alors une heure décalée d’une ou deux heures selon la saison, sans le moindre avertissement.
echo ini_get('date.timezone'), "\n"; // (vide) sur beaucoup d'installations
echo date_default_timezone_get(), "\n"; // UTCLe réglage se fait dans php.ini, ou en tête d’application :
date_default_timezone_set('Europe/Paris');La règle de fond : stocker en UTC, afficher dans le fuseau de l’utilisateur. Une base de données qui contient des heures locales devient inexploitable dès qu’un utilisateur change de fuseau ou que l’heure d’été passe.
date() et time()
time() renvoie le nombre de secondes écoulées depuis le 1er janvier 1970 à minuit UTC. date() met en forme un tel horodatage, ou l’instant présent si on ne lui en donne pas.
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/2022Les caractères de format les plus utilisés :
| Date | Heure | ||
|---|---|---|---|
d | jour sur 2 chiffres (01 à 31) | H | heure sur 24 h (00 à 23) |
j | jour sans zéro (1 à 31) | h | heure sur 12 h (01 à 12) |
m | mois sur 2 chiffres (01 à 12) | i | minutes (00 à 59) |
n | mois sans zéro (1 à 12) | s | secondes (00 à 59) |
Y | année sur 4 chiffres | A | AM ou PM |
N | jour de la semaine (1 = lundi) | P | décalage, par exemple +02:00 |
t | nombre de jours dans le mois | U | horodatage Unix |
L | 1 si année bissextile | c | format ISO 8601 complet |
Pour insérer un caractère qui est aussi un code de format, il faut l’échapper avec un antislash :
echo date('\L\e d/m/Y'), "\n"; // Le 02/09/2026
// sans antislash, « L » donnerait 0 ou 1 et « e » le nom du fuseaudate() ne parle pas français
C’est le deuxième piège classique. Les noms de jours et de mois renvoyés par date() sont toujours en anglais, et setlocale() n’y change rien.
$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 — inchangéLa solution est IntlDateFormatter, de l’extension intl :
$formateur = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN,
);
echo $formateur->format($ts), "\n"; // vendredi 1 avril 2022Pour un format personnalisé, la classe accepte un motif ICU, dont les codes diffèrent de ceux de date() :
$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 à 21h47Si intl n’est pas disponible, un tableau de correspondance dépanne pour le français, mais ne passera pas à l’échelle sur plusieurs langues.
DateTimeImmutable, la classe à utiliser
Pour tout ce qui dépasse l’affichage, les classes de date valent mieux que les fonctions. DateTimeImmutable est préférable à DateTime : ses méthodes renvoient un nouvel objet au lieu de modifier l’objet courant, ce qui évite les modifications à distance.
$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 — inchangé
// Avec DateTime (muable), la troisième ligne afficherait 05/09/2026// Ajouter et retirer des durées
$dans30Mois = $date->add(new DateInterval('P30M'));
$ilYa2Sem = $date->sub(new DateInterval('P2W'));
// Écart entre deux dates
$ecart = $date->diff(new DateTimeImmutable('2026-12-25', $paris));
echo $ecart->days, " jours\n";
echo $ecart->format('%m mois et %d jours'), "\n";
// Une série de dates
$periode = new DatePeriod(
$date,
new DateInterval('P1D'),
new DateTimeImmutable('2026-09-07', $paris),
);
foreach ($periode as $jour) {
echo $jour->format('D d/m'), "\n";
}Le piège du dernier jour du mois
Ajouter un mois à une fin de mois ne donne pas ce qu’on attend, et ce comportement est le même avec DateInterval qu’avec l’arithmétique sur mktime().
$fin = new DateTimeImmutable('2026-01-31');
echo $fin->add(new DateInterval('P1M'))->format('d/m/Y'), "\n"; // 03/03/2026Le 31 février n’existe pas : PHP le fait déborder sur mars. Pour obtenir le dernier jour du mois suivant, il faut le demander explicitement :
echo $fin->modify('last day of next month')->format('d/m/Y'), "\n"; // 28/02/2026Une journée ne fait pas toujours 24 heures
Au passage à l’heure d’été, une journée en fait 23, à l’heure d’hiver, 25. Ajouter 86400 secondes à un horodatage donne alors une heure fausse.
$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"; // 23DateInterval raisonne en jours calendaires et donne bien midi le lendemain, alors que seules 23 heures se sont écoulées. C’est le comportement correct pour un rappel « demain à midi », et le mauvais pour une durée facturée. Les deux notions sont distinctes : P1D pour un jour calendaire, PT24H pour vingt-quatre heures.
Lire une date saisie par l’utilisateur
strtotime() comprend beaucoup de choses, mais interprète les formats ambigus à l’américaine : 03/04/2026 devient le 4 mars, pas le 3 avril. Pour un format connu, createFromFormat() est sans ambiguïté.
$date = DateTimeImmutable::createFromFormat(
'!d/m/Y', // le « ! » remet l'heure à 00:00:00
'03/04/2026',
new DateTimeZone('Europe/Paris'),
);
echo $date->format('d F Y'), "\n"; // 03 April 2026Sans le ! initial, les champs non fournis prennent la valeur de l’instant courant : deux exécutions du même code donnent deux heures différentes.
Le point le plus dangereux : une date invalide n’échoue pas, elle déborde silencieusement.
$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)Il faut donc vérifier getLastErrors() après chaque analyse, ou comparer la date reformatée à la saisie d’origine :
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;
}Depuis PHP 8.2, getLastErrors() renvoie false quand il n’y a rien à signaler, au lieu d’un tableau avec des compteurs à zéro. Le test doit en tenir compte.
Nouveautés récentes
PHP 8.4 a ajouté createFromTimestamp(), qui accepte les horodatages fractionnaires :
$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:00Auparavant il fallait passer par '@' . $ts ou setTimestamp(), et les microsecondes se perdaient.
Les fonctions dépréciées à ne plus utiliser
Plusieurs fonctions de date encore présentes émettent un avis de dépréciation. Elles fonctionnent sur PHP 8.5.10, mais leur retrait est programmé.
| Fonction | Dépréciée depuis | Remplacement |
|---|---|---|
strftime() | PHP 8.1 | IntlDateFormatter::format() |
gmstrftime() | PHP 8.1 | IntlDateFormatter::format() |
strptime() | PHP 8.2 | date_parse_from_format() ou IntlDateFormatter::parse() |
date_sunrise() / date_sunset() | PHP 8.1 | date_sun_info() |
utf8_encode() | PHP 8.2 | mb_convert_encoding() |
Les messages exacts, relevés sur PHP 8.5.10 comme sur PHP 8.4.25 :
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.1strftime() était la façon habituelle d’obtenir une date en français. Son remplacement n’est pas date(), qui ne traduit pas, mais bien IntlDateFormatter.
À l’inverse, mktime(), checkdate(), getdate() et idate() ne sont pas dépréciés et fonctionnent normalement. Ils restent utiles pour des cas simples, même si les classes couvrent tout ce qu’ils font.
À retenir
- Régler le fuseau explicitement, stocker en UTC, afficher en local.
date()pour formater un instant,DateTimeImmutablepour tout calcul.IntlDateFormatterpour les noms de jours et de mois en français.- Après
createFromFormat(), vérifiergetLastErrors(): une date invalide déborde en silence. - Un jour calendaire n’est pas 24 heures.
P1DetPT24Hne sont pas interchangeables.
Pour analyser des dates par millions dans un fichier d’import, voir lire les gros fichiers avec PHP, et la connexion à une base de données pour les stocker en UTC.
Voir aussi valider une date avec une expression régulière, gérer les dates avec Carbon dans Laravel et le hub Développement web.
Erreurs fréquentes
date.timezone dans php.ini, PHP retombe sur UTC et affiche une heure décalée sans le moindre avertissement.setlocale() n'a aucun effet sur date(). Le remplacement de strftime() est IntlDateFormatter, pas date().31/02/2026 devient 03/03/2026 avec un simple avertissement dans getLastErrors(). Il faut le vérifier après chaque analyse.03/04/2026 devient le 4 mars, pas le 3 avril.31/01/2026 + P1M donne 03/03/2026. Pour le dernier jour du mois, utiliser modify('last day of next month').P1D avance bien d'un jour calendaire alors que 23 heures seulement se sont écoulées. P1D et PT24H ne sont pas interchangeables.false quand il n'y a rien à signaler, plus un tableau de compteurs à zéro.

