PHP 8.6 introduit une nouvelle classe nommée Duration, pensée pour représenter des durées de temps de type "chronomètre" (un timeout, un délai de retry) plutôt qu'une date précise. Jusqu'à présent, ce genre de valeur était généralement représenté par un simple entier (secondes ou millisecondes), avec les ambiguïtés que cela peut entraîner. Voici comment fonctionne cette nouvelle classe et dans quels cas elle est utile.

En PHP, il n'existait pas de type clair pour représenter une durée "chronomètre" : un simple laps de temps écoulé ou à attendre, sans lien avec une date calendaire précise. Un timeout, un délai de retry ou une expiration de verrou étaient généralement stockés sous forme d'entier brut, sans que le type garantisse l'unité utilisée. Par exemple, il était possible de confondre, à la relecture, un retryAfter(500) exprimé en secondes avec un retryAfter(500) exprimé en millisecondes.

DateInterval aurait pu sembler être la solution, mais cette classe est plutôt conçue pour représenter un écart entre deux dates calendaires (jours, mois, années), pas pour de l'arithmétique précise sur des durées courtes.

C'est ce vide que comble Duration : une classe qui représente ce que la RFC décrit comme une durée "chronomètre" ou "minuteur" (par opposition à une date précise), avec une exactitude allant jusqu'à la nanoseconde. Elle a d'ailleurs été proposée en partie pour améliorer l'ergonomie de la nouvelle API de polling de PHP 8.6, mais elle reste tout à fait utilisable dans du code applicatif classique.

Cette classe fait partie du namespace Time et se nomme donc Time\Duration.

Créer une durée

Duration est une classe final et readonly : final empêche de la sous-classer, readonly rend chaque instance immuable une fois créée, ses propriétés ne pouvant plus être modifiées après coup. Toute opération renvoie donc une nouvelle instance plutôt que de modifier l'objet existant. On ne l'instancie jamais directement avec new, mais via des méthodes statiques :

use Time\Duration;

$delai = Duration::fromSeconds(30);
$timeout = Duration::fromMilliseconds(500);
$pause = Duration::fromMinutes(5);
$expiration = Duration::fromHours(1);

Les méthodes disponibles sont :

  • fromSeconds(int $secondes, int $nanosecondes = 0)
  • fromMilliseconds(int $millisecondes)
  • fromMicroseconds(int $microsecondes)
  • fromNanoseconds(int $nanosecondes)
  • fromMinutes(int $minutes)
  • fromHours(int $heures)
  • fromIso8601DurationString(string $duree) (une chaîne au format ISO 8601, sans composant calendaire comme les jours ou les mois)

Une fois créée, une instance de Duration expose trois propriétés publiques en lecture seule : $seconds, $nanoseconds (entre 0 et 999 999 999) et $negative (un booléen indiquant si la durée est négative). Ce dernier point est une particularité de conception : plutôt que de gérer le signe directement dans les secondes, Duration l'isole dans une propriété dédiée, ce qui simplifie le calcul de valeur absolue.

Effectuer des opérations arithmétiques

Duration ne surcharge pas les opérateurs + et - : il faut passer par des méthodes dédiées, qui renvoient chacune une nouvelle instance (l'objet d'origine n'est jamais modifié) :

$baseDelay = Duration::fromMilliseconds(100);

// Backoff exponentiel pour une tentative de reconnexion
$tentative = 5;
$delaiAttente = $baseDelay->multiplyBy(2 ** $tentative);

$total = $delaiAttente->add(Duration::fromSeconds(2));
$reste = $total->sub(Duration::fromMilliseconds(500));

$negatif = $reste->negate();
$positif = $negatif->absolute();

Les méthodes disponibles sont add(), sub(), multiplyBy(int $facteur), divideBy(int $diviseur), negate() et absolute(). À noter : multiplyBy() et divideBy() n'acceptent qu'un facteur ou un diviseur entier, et divideBy() tronque les nanosecondes restantes plutôt que de les arrondir.

Comparer deux durées

Contrairement aux opérations arithmétiques, la comparaison fonctionne directement avec les opérateurs classiques (<, >, ==), grâce à l'ordre dans lequel les propriétés internes sont définies :

$courte = Duration::fromSeconds(10);
$longue = Duration::fromMinutes(1);

if ($courte < $longue) {
    echo "La première durée est plus courte.";
}

Une méthode statique compare(Duration $a, Duration $b) est également disponible et retourne -1, 0 ou 1, pratique notamment pour trier un tableau de durées avec usort().

Gérer les erreurs

Toute opération invalide (nanosecondes hors de la plage 0-999 999 999, diviseur négatif ou nul, dépassement de la plage représentable) lève une nouvelle exception dédiée, Time\TimeException :

try {
    $duree = Duration::fromSeconds(10, 2_000_000_000); // nanosecondes invalides
} catch (\Time\TimeException $e) {
    echo "Durée invalide : " . $e->getMessage();
}

Un exemple concret avec l'API de polling

L'un des cas d'usage mis en avant par la RFC est l'API de polling de PHP 8.6, qui accepte désormais un objet Duration plutôt qu'un entier ambigu :

use Time\Duration;

// Timeout explicite de 500 ms plutôt qu'un entier sans unité
$watchers = $context->wait(Duration::fromMilliseconds(500));

L'intérêt est immédiat : Duration::fromMilliseconds(500) est nettement plus explicite que 500 tout court, sans risque de confondre secondes et millisecondes lors d'une relecture de code.

Cette classe est présentée comme la première pierre d'une modernisation plus large des API de date et heure de PHP. Les détails complets de la RFC, y compris les débats sur le nommage des méthodes, sont consultables sur la page RFC Duration class du wiki PHP.

Si vous gérez des timeouts, des délais de retry ou des expirations dans vos projets, cela vaut le coup de garder un œil sur cette classe d'ici la sortie de PHP 8.6.