Fuseaux horaires avec PHP et MySQL

Représentation des dates

Pour localiser un événement précisément dans le temps, on a besoin de trois informations : la date (16/08/2011 par exemple), l'heure (18:12:00) et le décalage par rapport au temps universel coordonné (UTC). Dans un entête de courrier électronique, par exemple, la date d'expédition est encodée de la façon suivante :

Date: Mon, 8 Aug 2011 09:59:08 +0200

A partir de cette information, je sais que l'E-mail est parti le lundi 8 août 2011 à 9 h 59 minutes et 8 secondes. Le +0200 indique que cette heure est décalée de 2 heures par rapport au temps UTC. Je peut donc convertir cette valeur en temps UTC : 08/08/2011 07:59:08 et ensuite lui additionner un autre décalage horaire pour l'afficher dans le fuseau horaire courant de l'utilisateur. C'est que fait d'ailleurs votre client de messagerie.

TIMESTAMP ou DATETIME ?

Si je veux stocker cette date dans une base de données MySQL, j'ai deux possibilités : utiliser le type de colonne TIMESTAMP ou le type DATETIME.

Le premier est en fait un simple entier signé sur 32 bits. Il correspond au nombre de secondes écoulées depuis le 1er janvier 1970 00:00:00 UTC. Notez bien le UTC. Donc un timestamp ne contient aucune information sur le fuseau horaire. Par contre, MySQL convertit le timestamp du fuseau horaire courant du serveur en UTC quand vous insérez/modifiez une valeur, et reconvertit dans l'autre sens quand vous lisez :

-- Affiche le fuseau horaire utilisé par le serveur MySQL (Paris UTC+2)
SELECT @@system_time_zone;
-- Insère une date
INSERT INTO  `test`.`test` (`id`,`date`) VALUES (NULL, '2011-08-16 18:45:00');
-- Affiche 2011-08-16 18:45:00
SELECT DATE FROM test;
-- Passe en UTC
SET GLOBAL time_zone = UTC;
-- Affiche 2011-08-16 16:45:00
SELECT DATE FROM test;

Donc faite attention aux réglages de votre serveur, car si vous les changez, vous risquez d'être surpris. Ça peut aussi être problématique si vous exportez des données entre deux serveurs qui n'ont pas le même fuseau horaire (c'est d'ailleurs pour ça que quand vous faites un export avec mysqldump, il passe temporairement le fuseau horaire à UTC).

Comme le type TIMESTAMP, le type DATETIME ne stocke aucune information sur le fuseau horaire. Par contre un changement du fuseau horaire du serveur n'affecte pas l'affichage des données. Quand vous écrivez une valeur, vous la retrouvez à l'identique à la lecture, quelle que soit la configuration du serveur MySQL.

Utiliser UTC

Si vous avez une application ou les utilisateurs sont répartis sur plusieurs fuseaux horaires, vos dates doivent toutes être exprimées dans le même fuseau horaire pour permettre de les trier et de les comparer. Le choix le plus évident est d'utiliser le fuseau horaire de référence, c'est à dire UTC.

Pour cela, vous devez connaitre le fuseau horaire de l'utilisateur, ce qui permet à PHP d'en déduire le nombre d'heures à additionner ou à soustraire. Le mieux c'est de permettre à l'utilisateur de le spécifier dans ses préférences car il n'y a pas de moyen fiable à 100% pour déterminer le fuseau horaire où se trouve l'utilisateur.

Avant insertion dans la base, vous convertissez donc toutes vos dates en UTC et vous utilisez le type DATETIME pour ne pas vous soucier de la configuration de MySQL.

Lorsque vous affichez la date, vous devez alors reconvertir votre date dans le fuseau horaire de l'utilisateur. Ce dernier peut éventuellement modifier ses préférences pour afficher la date locale si il est en déplacement.

Pour les opérations de tri, de filtrage ou d’agrégation sur la base de données, vous devez penser à convertir en UTC toutes les dates utilisées dans les clauses WHERE, GROUP BY, HAVING...

Gérer correctement les opérations sur les dates

Dans la majorité des cas, cette approche est satisfaisante, mais il peut y avoir des situations comme l'explique Derick Rethans dans cet article où ça ne suffit pas.

Le problème se pose lorsqu'on veut effectuer des opérations sur les dates. Si par exemple vous voulez ajouter une période de temps à une date, mais au moment ou vous stockez la date de référence, vous ne connaissez pas encore la période. Vous allez stocker la date dans MySQL en utilisant un TIMESTAMP :

<?php
// L'utilisateur saisit une date. On sait que son fuseau horaire est Montréal.
$date = new DateTime('2010-03-25 19:03 America/Montreal');

// On stocke uniquement le timestamp UTC (1269558180)
mysql_query('INSERT INTO my_table SET date = '.$date->getTimestamp());
?>

Plus tard, vous voulez ajouter 8 mois à cette date :

<?php
// La valeur est lue depuis la base et stockée dans la variable $ts.
// Comme on instancie l'objet DateTime avec un timestamp, le fuseau
// horaire est réglé sur UTC.
$dateWithTsOnly = new DateTime("@$ts");
$dateWithTsOnly->modify('+ 8 month');

// On règle le fuseau horaire pour l'affichage (dans cet exemple,
// on utilise le même que celui utilisé pour la saisie, mais dans un
// cas réel, il pourrait être différent)
$dateWithTsOnly->setTimezone(new DateTimeZone('America/Montreal'));

// Quand on affiche la date calculée, on n'obtient pas le résultat attendu
// 2010-11-25 18:03:00 America/Montreal 
// au lieu de 2010-11-25 19:03:00 America/Montreal
echo $dateWithTsOnly->format('Y-m-d H:i:s e');
?>

Le problème ici, c'est que comme on n'a pas mémorisé le fuseau horaire dans lequel la date était initialement saisie, on réalise le calcul en UTC. On ne prend donc pas en compte le changement d'heure qui est survenu durant la période de 8 mois et la date est donc affichée avec un décalage d'une heure.

Si par contre, on stocke dans une colonne de type CHAR le fuseau horaire (sous la forme Zone/Ville), on peut instancier un objet DateTime avec le bon fuseau horaire et ainsi PHP peut tenir compte du changement d'heure dans son calcul :

<?php
// Si on stocke le fuseau horaire dans MySQL, on peut récupérer ce dernier dans
// la variable $tz et ainsi recréer la date avec le fuseau horaire initial.
$dateWithTsAndTz = new DateTime("@$ts");
$dateWithTsAndTz->setTimezone(new DateTimeZone($tz));
$dateWithTsAndTz->modify('+ 8 month');
// Et là on affiche la date correcte (2010-11-25 19:03:00 America/Montreal)
echo $dateWithTsAndTz->format('Y-m-d H:i:s e');
?>

Pour finir, il faut de préférence toujours avoir une version récente de PHP, car comme les changements d'heure ne sont pas déterminés avec des règles précises, PHP ne peut pas les calculer et doit donc utiliser une base de données qui est mise à jour à chaque nouvelle version de PHP.

Etiquettes:

Ajouter un commentaire