Page d'accueil Mes articles

Symfony Clock Component

Clock Component sera disponible dans la version 6.2 de Symfony. Il a été développé par Nicolas Grekas afin de pouvoir découpler nos applications de l'horloge système.

Publié en September 2022.

Symfony Clock va venir compléter sur la version 6.2 la grande famille des composants Symfony. Sur sa pull request, Nicolas Grekas exprime le besoin de ce nouveau composant : Pouvoir découpler nos applications de l'horloge système. Cette idée fait suite à une conférence d'Anna Filina au SymfonyWorld Online Summer qui a eu lieu le 16 et 17 Juin 2022 et où elle utilisait son propre "clock" service afin d'améliorer la testabilité de son code. Je n'étais pas présent à cette conférence, mais je pense que cela fait référence à un de ses articles.

Pourquoi découpler nos services de l'horloge système ?

Il peut exister plusieurs cas dans lesquels nous pouvons avoir besoin de découper nos services de l'horloge système. Dans son article, Anna Filina présente la création d'un service retournant l'âge d'une personne. Une méthode getAge() fait tout simplement une comparaison entre la date de naissance de la personne et la date du jour. Anna fait ensuite évoluer son service en remplaçant la méthode getAge() par une méthode getAgeOn() afin de pouvoir connaitre l'âge de la personne à des moments marquants de sa vie ( date de l'obtention de son diplôme par exemple ). La méthode getAgeOn() contrairement à la méthode getAge() n'est plus couplée avec l'horloge système. Si nous souhaitons connaitre l'âge de la personne, nous devrons passer en paramètre de la méthode getAgeOn() la date du jour. Ce découplage réalisé à la base pour répondre à un besoin fonctionnel, va nous permettre de rendre notre service testable. Car si nous devions ajouter un test unitaire pour vérifier que getAge() retourne bien 35, cela fonctionnerait très bien en 2022, mais en 2023 notre test tomberait en erreur. Avec getAgeOn(), notre test fonctionnera de manière indépendante par rapport à l'horloge système.

Pour le moment toujours en brouillon, la PSR-20 a pour but de répondre à cette problématique récurrente lors de la réalisation de nos tests unitaires.

PSR-20 : Common Interface for Accessing the Clock

Lorsque dans notre code, nous utilisons directement les fonctions \date(), \time() ou encore new \DateTimeImmutable('now'), nous créons un couplage fort avec l'horloge système qui va nous empêcher de pouvoir écrire nos tests sans difficultés. La proposition de la PSR-20 consiste donc à avoir recours à une nouvelle interface ClockInterface lorsque l'on souhaite accéder à la date du jour.

<?php

namespace Psr\Clock;

interface ClockInterface
{
    /**
     * Returns the current time as a DateTimeImmutable Object
     */
    public function now(): \DateTimeImmutable;

}

Pour le moment, cette PSR est en brouillon, et il y a fort à parier que la version 6.2 de Symfony sorte avant la publication de cette PSR-20.

Symfony Clock Component

L'interface proposée par Nicolas est la suivante

interface ClockInterface
{
    public function now(): \DateTimeImmutable;

    public function sleep(float|int $seconds): void;

    public function withTimeZone(\DateTimeZone|string $timezone): static;
}

Elle répond en partie à la PSR-20 avec la méthode now() et propose deux autres méthodes. sleep et withTimeZone qui permet d'obtenir l'horloge sur une autre time zone.

Concernant ses implémentations, nous en avons trois.

Création d'un filtre Twig pour illustrer son utilisation

Pour illustrer l'utilisation du Clock Component, j'ai ajouté une extension Twig me permettant de savoir si un devis est valide ou non. Pour cela, je dois vérifier que le devis est publié et que sa date de validité n'est pas dépassée.

// App\Twig\AppRuntime.php

    public function isValidQuote(Quote $quote): bool
    {
        return (
            $quote->getValidityDate() > new \DateTimeImmutable('now') &&
            $quote->isPublished()
        );
    }

Pour m'assurer que cela fonctionne durablement dans le temps, je décide d'ajouter un test unitaire.

// App\Tests\Twig\AppRuntimeTest.php

    public function testQuoteIsValid(): void
    {
        $quote = new Quote();
        $quote->setPublished(true);
        $quote->setValidityDate(new \DateTimeImmutable('2022-09-30'));
        $this->assertTrue(
            (new AppRuntime())->isValidQuote($quote)
        );
    }

phpUnit testQuoteIsValid

Aujourd'hui, le 22 septembre 2022, cela fonctionne.

Mais en octobre, un test unitaire qui était bon aujourd'hui tombera alors en erreur. La raison étant que mon filtre Twig est couplé avec l'horloge système. C'est là que le Clock Component intervient.

class AppRuntime implements RuntimeExtensionInterface
{
    private ClockInterface $clock;

    public function __construct(ClockInterface $clock)
    {
        $this->clock = $clock;
    }

    public function isValidQuote(Quote $quote): bool
    {
        return (
            $quote->getValidityDate() > $this->clock->now() &&
            $quote->isPublished()
        );
    }
}

Grâce au Clock Component, je n'ai plus le couplage fort avec l'horloge système. Dans le FrameworkBundle, le service clock est construit à partir de la classe NativeClock. Au moment de la définition du service, l'interface ClockInterface est définie en tant qu'alias ce qui permet à Symfony de faire l'auto-câblage sans difficultés.

Symfony command debug:container

    public function testQuoteIsValid(): void
    {
        $quote = new Quote();
        $quote->setPublished(true);
        $quote->setValidityDate(new \DateTimeImmutable('2022-09-30'));
        $this->assertTrue(
            (new AppRuntime(new MockClock('2022-09-20')))->isValidQuote($quote)
        );
    }

Je peux ainsi faire évoluer mon test de façon à ne pas être lié à l'horloge système. Maintenant que nous en avons la possibilité très facilement sur Symfony, je trouve que c'est une bonne pratique de pouvoir découpler nos services de l'horloge système afin de rendre notre code plus testable.

Un grand merci à Nicolas Grekas pour sa contribution 😍

Cet article vous a été utile ? Faites le connaître sur les réseaux sociaux Twitter twitter LinkedIn linkedin

Un commentaire ?

codeur
Ou connectez-vous avec GitHub