PHP 8.6 Duration : une petite classe pour corriger une erreur récurrente

· Oskar Stark · Expertise · Temps de lecture: 6 minutes
Abstract editorial concept of a Duration class, showing elapsed time and precise time intervals in modern PHP coding

PHP 8.6 ajoute une classe Duration dédiée. En apparence, cette nouveauté peut sembler mineure, mais elle permettra enfin d'avoir un type de données clair pour le temps écoulé, plutôt que de surcharger la classe DateInterval, les entiers et l’arithmétique sur les timestamps.

La RFC PHP acceptée pour la nouvelle classe Duration est l’une de ces évolutions qui paraissent modestes au premier abord. Elle n’introduit pas de nouveau paradigme de programmation et ne fera probablement pas la une des notes de version, au même titre que les property hooks ou les fibers.

Mais, dans le travail du backend au quotidien, elle répond à un problème fréquent dans les projets Symfony et PHP : les équipes utilisent les mêmes structures pour 2 notions différentes.

  • Les intervalles calendaires : « dans un mois », « le prochain jour ouvré », « renouvellement annuel de l’abonnement ».

  • Les durées écoulées : « réessayer après 500 millisecondes », « le verrou expire après 30 secondes », « le budget SLA est de 2 minutes ».

PHP dispose depuis longtemps de DateInterval, mais DateInterval n’est pas un type de durée précis et indépendant du contexte. P1M ne correspond pas à un nombre fixe de secondes. Ajouter un mois au 31 janvier est une opération calendaire, pas un calcul de temps écoulé. La nouvelle classe Duration est importante, car elle fournit au langage un type de base pour la deuxième catégorie : des quantités de temps mesurables.

Pourquoi c’est important concrètement dans les projets

Dans les applications Symfony, les valeurs de temps apparaissent partout :

  • Les délais de retry de Messenger sont exprimés en millisecondes.

  • Les TTL de cache sont généralement exprimés en secondes.

  • Les timeouts des clients HTTP sont souvent des secondes flottantes.

  • Les TTL de verrous, les limites de débit et les deadlines de jobs peuvent suivre encore une autre convention.

  • Les colonnes de base de données nommées timeout, ttl ou delay sont fréquemment de simples entiers, sans unité dans le nom.

C’est ainsi que les bugs s’introduisent dans des systèmes matures. Non pas parce que les développeurs ne comprennent pas la temporalité, mais parce que le code ne permet pas de faire la distinction.

Voici un exemple typique :

use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;

$retryDelay = 15 * 60;

$cache->get('import-status-'.$importId, function (CacheItem $item) use ($retryDelay): ImportStatus {
    $item->expiresAfter($retryDelay);

    return ImportStatus::pending();
});

$bus->dispatch(new Envelope(
    new ImportMessage($importId),
    [new DelayStamp($retryDelay)]
));

Ce code est suffisamment lisible pour être revu, mais il contient une erreur : expiresAfter() utilise des secondes, tandis que DelayStamp utilise des millisecondes. Le même entier traverse 2 API avec des conventions d’unité différentes.

Un type de durée dédié ne résout pas magiquement tous les problèmes de conversion entre les API. Il change toutefois la pression sur la conception : on cesse de passer d'« un entier quelconque » et on commence à passer à « une durée qui doit être convertie explicitement à la frontière ».

DateInterval ne doit pas disparaître, il doit être utilisé de manière plus délibérée

La mauvaise conclusion serait de se dire : « Une fois que PHP aura Duration, il faudra arrêter d’utiliser DateInterval. »

DateInterval reste l’outil adapté aux opérations sensibles au calendrier. Par exemple :

$renewalDate = $currentPeriodStart->add(new DateInterval('P1M'));

Ou autrement dit « un mois calendaire plus tard ». Le résultat dépend de la date. C’est exactement ce qu’il faut pour les périodes de facturation, les renouvellements d’abonnement ou les fenêtres de reporting.

Pour un timeout, en revanche, DateInterval('P1M') est généralement un signal d’alerte. Un timeout devrait normalement pouvoir être mesuré sans connaître la date de départ, le fuseau horaire ou le mois du calendrier. C’est dans cet espace conceptuel que se situe la classe Duration de PHP 8.6.

Voici une règle utile pour les revues de code :

Si la valeur répond à « combien de temps cela a-t-il pris ? » ou « combien de temps devons-nous attendre ? », modélisez-la comme une durée. Si elle répond à « quelle date du calendrier vient ensuite ? », modélisez-la comme un intervalle calendaire.

Préparez votre base de code avant PHP 8.6

Vous n’avez pas besoin d’attendre PHP 8.6 pour améliorer vos développements. En réalité, la voie pour se préparer la plus sûre consiste à introduire dès maintenant des frontières explicites pour les durées, puis à adapter plus tard leur implémentation à la classe native.

Un petit objet de valeur au niveau du projet supprime déjà la plupart des ambiguïtés :

namespace App\Time;

use InvalidArgumentException;

final readonly class TimeSpan
{
    private function __construct(private int $milliseconds)
    {
        if ($milliseconds < 0) {
            throw new InvalidArgumentException('A time span cannot be negative.');
        }
    }

    public static function milliseconds(int $milliseconds): self
    {
        return new self($milliseconds);
    }

    public static function seconds(int $seconds): self
    {
        return new self($seconds * 1_000);
    }

    public static function minutes(int $minutes): self
    {
        return self::seconds($minutes * 60);
    }

    public function toMilliseconds(): int
    {
        return $this->milliseconds;
    }

    public function toWholeSeconds(): int
    {
        if ($this->milliseconds % 1_000 !== 0) {
            throw new InvalidArgumentException('The time span cannot be represented as whole seconds.');
        }

        return intdiv($this->milliseconds, 1_000);
    }
}

Rendez ensuite la conversion d’unité visible aux frontières du framework :

use App\Time\TimeSpan;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;

$retryDelay = TimeSpan::minutes(15);

$cache->get('import-status-'.$importId, function (CacheItem $item) use ($retryDelay): ImportStatus {
    $item->expiresAfter($retryDelay->toWholeSeconds());

    return ImportStatus::pending();
});

$bus->dispatch(new Envelope(
    new ImportMessage($importId),
    [new DelayStamp($retryDelay->toMilliseconds())]
));

Ce n’est volontairement pas sophistiqué. L’avantage est que la conversion devient désormais explicite et vérifiable lors de la revue de code. Si quelqu’un change le délai en TimeSpan::milliseconds(500), la frontière du cache échouera immédiatement au lieu d'être arrondie à zéro seconde, sans qu'on s'en rende compte.

Une fois que la version minimale de votre runtime est PHP 8.6, cet objet de valeur pourra soit encapsuler la classe native Duration, soit disparaître, car les API du framework prennent directement en charge le type natif. L'essentiel est que votre domaine et vos services applicatifs ne dépendent plus d’entiers sans libellé.

Stockez les durées avec leur unité dans le modèle

Un autre anti-pattern courant consiste à avoir une colonne de base de données nommée duration ou ttl avec un type INT, et l’unité documentée uniquement dans un commentaire de migration. Six mois plus tard, quelqu’un lit la propriété de l’entité et doit deviner l'unité.

Préférez plutôt nommer l’unité là où la valeur est stockée :

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
final class ImportJob
{
    #[ORM\Column(type: 'integer')]
    private int $processingBudgetMilliseconds;

    public function setProcessingBudget(TimeSpan $budget): void
    {
        $this->processingBudgetMilliseconds = $budget->toMilliseconds();
    }

    public function processingBudget(): TimeSpan
    {
        return TimeSpan::milliseconds($this->processingBudgetMilliseconds);
    }
}

Vous gardez ainsi un mapping Doctrine simple et portable, tout en empêchant le reste de l’application de traiter la valeur comme un entier arbitraire. Cela vous permet également d'identifier facilement un point de migration si vous décidez ultérieurement de stocker des microsecondes, des nanosecondes ou un type d'intervalle natif de la base de données.

N’utilisez pas l’heure murale pour mesurer des durées

La nouvelle classe Duration rend aussi plus visible une autre distinction : mesurer un temps écoulé n’est pas la même chose que lire la date courante.

Pour les mesures de performance, utilisez une source d’horloge monotone comme hrtime() plutôt que de soustraire deux instances de DateTimeImmutable créées à partir de l’heure murale. En effet, l’heure murale peut changer à cause des ajustements NTP, de modifications manuelles de l’horloge ou d’hypothèses liées au fuseau horaire. Un timer monotone est conçu pour les mesures de temps écoulé.

$startedAt = hrtime(true);

$processor->process($message);

$elapsedNanoseconds = hrtime(true) - $startedAt;
$elapsed = TimeSpan::milliseconds(intdiv($elapsedNanoseconds, 1_000_000));

Dans les applications Symfony, le composant Clock mérite aussi d’être envisagé pour les services qui ont besoin d’un accès testable au temps. Il aide à éviter les appels codés en dur à new DateTimeImmutable() dans la logique métier et rend les tests dépendants du temps déterministes.

Les conséquences pour le style de code Symfony et PHP

La conséquence la plus utile de la classe Duration de PHP 8.6 ne sera pas un code plus court, mais de meilleures interfaces.

Aujourd’hui, de nombreux services applicatifs ressemblent à ceci :

final readonly class ImportScheduler
{
    public function schedule(string $importId, int $delay): void
    {
        // Is $delay seconds, milliseconds, minutes?
    }
}

Une meilleure interface dit ce qu’elle signifie :

use App\Time\TimeSpan;

final readonly class ImportScheduler
{
    public function schedule(string $importId, TimeSpan $delay): void
    {
        // The unit is no longer part of the caller's guesswork.
    }
}

Avec PHP 8.6, la classe native offre aux auteurs de bibliothèques et de frameworks une cible commune. C’est là que cette RFC devient stratégiquement intéressante : une fois que l’écosystème peut définir des types de données en fonction d'un concept de durée partagé, il y a moins de risques que les API inventent leurs propres conventions d’entiers..

Notre recommandation pour les projets existants est donc pragmatique :

  1. Continuez à utiliser DateInterval pour la logique calendaire.

  2. Arrêtez de passer des entiers bruts pour les timeouts, TTL et délais dans votre couche applicative.

  3. Convertissez aux unités spécifiques au framework uniquement à la frontière.

  4. Utilisez du temps monotone pour les mesures.

  5. Prévoyez une petite couche de compatibilité afin que Duration de PHP 8.6 puisse être adoptée sans réécrire la logique métier.

Il ne s'agit pas d'un simple refactoring cosmétique. Il élimine une classe de bugs de production qui ne tendent à apparaître que sous charge, lors des retries, dans les queues ou autour d’intégrations sensibles au temps.

Sources

Préparez votre montée de version vers PHP 8.6

SensioLabs accompagne les équipes dans la modernisation de leurs bases Symfony et PHP avec des revues d’architecture, la planification de la migration et de l'assistance technique.

Cela pourrait aussi vous intéresser

Abstract editorial illustration of PHP 8.6 JSON parsing with a highlighted error position and subtle purple accents
Oskar Stark

PHP 8.6 : Trouver la position d’une erreur JSON lors du décodage

PHP 8.6 apporte une amélioration mineure mais pratique pour le décodage JSON : il est désormais possible d'obtenir la position exacte de l'erreur en cas d'échec. Le débogage des payloads incorrects est ainsi grandement facilité.

En savoir plus : PHP 8.6 : Trouver la position d’une erreur JSON lors du décodage
Modern abstract editorial scene with cleaner, safer sorting API concepts and subtle purple accents
Oskar Stark

PHP 8.6 SortDirection : des API de tri plus claires et plus sûres pour PHP

PHP 8.6 introduit `SortDirection`, une évolution utile qui rend les API de tri plus claires. Voici ce que cela change, pourquoi c'est important et comment l'utiliser en pratique.

En savoir plus : PHP 8.6 SortDirection : des API de tri plus claires et plus sûres pour PHP
Large tree under the sunlight
Mathieu Santostefano

Multipliez votre vitesse de développement avec l’IA grâce aux Git Worktrees

Fini le jonglage fastidieux entre git stash et les réinstallations de dépendances. Découvrez comment les Git Worktrees révolutionnent votre expérience développeur en permettant à vos agents IA de travailler sur une branche isolée pendant que vous restez concentré sur votre code. Une méthode indispensable pour mener vos tâches en parallèle et multiplier votre vitesse d'exécution sans friction.

En savoir plus : Multipliez votre vitesse de développement avec l’IA grâce aux Git Worktrees
A man sculpting a rock with PDF written on it
Steven Renaux

Créer un Custom Builder - L'histoire du GotenbergBundle

Nous avons déjà vu comment générer un fichier PDF en quelques lignes de code à l'aide de Gotenberg et de GotenbergBundle, un bundle Symfony. Mais que faire lorsque votre application doit générer plusieurs fichiers PDF différents, chacun avec sa propre mise en page, ses propres styles et ses propres données ?

En savoir plus : Créer un Custom Builder - L'histoire du GotenbergBundle
Nicolas Grekas standing on stage at SymfonyLive Paris 2026
Jules Daunay

SymfonyLive Paris 2026 : IA et retrouvailles au sommet pour la Team SensioLabs

Le rideau vient de tomber sur le SymfonyLive Paris 2026, et on a encore des étoiles ✨ (et des lignes de code) plein les yeux. En tant que créateur de Symfony et sponsor historique, SensioLabs ne pouvait rêver d'un meilleur moment pour célébrer l'open source, l'innovation et, surtout, l'incroyable communauté qui nous entoure.

En savoir plus : SymfonyLive Paris 2026 : IA et retrouvailles au sommet pour la Team SensioLabs
Paper notes on a wall
Imen Ezzine

Plongée dans les coulisses de trois cérémonies collaboratives

A la suite d’un post sur LinkedIn, j’ai pensé à écrire cet article pour décrire 3 cérémonies qui m’ont marquée et que j’ai adorées pendant l’une de mes dernières missions : Event Storming, Example Mapping et Domain Storytelling.

En savoir plus : Plongée dans les coulisses de trois cérémonies collaboratives
Illustration of Developer
Silas Joisten

La révolution de l’expérience développeur en 2026

L’expérience développeur est plus importante que jamais. Découvrez comment de meilleurs outils, des flux de travail plus intelligents et une culture de l’apprentissage peuvent transformer la manière dont les équipes conçoivent des logiciels.

En savoir plus : La révolution de l’expérience développeur en 2026
Nicolas Grekas with a mic in his right hand raising his left hand on stage at SymfonyCon Amsterdam 2025
Jules Daunay

Symfony 8 : Stabilité, sécurité et innovation au service des développeurs

À l’occasion du lancement de Symfony 8, nous avons rencontré Nicolas Grekas, figure emblématique de l'open-source et contributeur majeur du framework. Entre nouveaux composants JSON, durcissement de la sécurité et intégration native avec PHP 8.4, Nicolas nous explique pourquoi cette version 8 s'inscrit dans la continuité des versions précédentes de Symfony, sans bousculer les entreprises. Un point complet pour comprendre les nouveautés et aborder votre montée de version sereinement.

En savoir plus : Symfony 8 : Stabilité, sécurité et innovation au service des développeurs