Accueil /  Blog / Symfony / Scheduler, Webhook et RemoteEvent : des composants prometteurs attendus pour Symfony 6.3

Scheduler, Webhook et RemoteEvent : des composants prometteurs attendus pour Symfony 6.3

Publié le jeudi 6 avril 2023

La version 6.3 de Symfony qui sortira en mai prochain va apporter son lot d’améliorations et de nouvelles possibilités sur des composants existants comme chaque nouvelle version de Symfony. Elle apportera également de nouveaux composants qui vont repousser encore plus loin les capacités que nous offre Symfony.

C'est encore lors des keynotes de conférences Symfony que ces composants ont été annoncés. Les plus attentifs auront eu l'info avant en suivant l'activité du Github de Symfony. Et pour ceux qui ont donc loupé jusqu'à aujourd'hui l'existence de ces composants encore expérimentaux et disponibles dans la future version 6.3, je vous en propose donc une petite présentation.

C’est quoi un Webhook ?

Moins connu que les API Rest, GraphQL ou même ce bon vieux SOAP, mais allant souvent de pair avec, voire inclue dans la documentation de l’API elle-même, les webhooks sont pourtant très répandus, et même très anciens sans qu’il s’agisse pour autant d’une technologie définie formellement.

Une application peut consommer une API pour récupérer de la data ou avoir une action. Mais si l'application a besoin par exemple d'être informée d'une mise à jour du système tiers, et que seul l’API soit disponible, on aura alors besoin de venir périodiquement consommer l’API pour vérifier si l’information est mise à jour, ce qui peut être peu performant voire peu pratique si on a besoin d’être mis au courant rapidement du changement d’une information. Du coup, il est souvent possible sur le système tiers de définir un webhook en paramétrant une URL de callback de votre application qui sera appelée lorsque nécessaire par le système tiers. La communication se fait bien alors du coup à l’initiative du système tiers dès que c’est nécessaire.

En résumé imaginez que vous vouliez que votre application soit informée, à chaque fois qu'un événement précis se passe dans un système extérieur, vous allez alors configurer une URL de callback sur le système en question, qui fera lui-même l'appel http sur votre application.

On l’a dit, les webhooks ce n’est pas normalisé, la communication peut être sécurisée de différentes façons, voire pas du tout, le contenu de la requête peut être du json, c’est souvent le cas, mais pourrait être n’importe quoi. C'est ce qui rend parfois si compliqué l'utilisation de ces webhooks c'est qu'il faut s'adapter à ce que le système extérieur au votre a défini.

Le composant Webhook

Annoncé par Fabien Potencier au dernier SymfonyCon 2022 à Paris, et mergé dans la branche 6.3 en décembre dernier, il est donc possible de tester dès à présent les composants Webhook et RemoteEvent qui vont souvent aller de pair.

Jusque-là en Symfony pour pouvoir gérer un webhook, vous deviez tout prendre à votre charge, la réception de la requête dans un controller, son routage donc, son traitement, la vérification de l’appel, etc. Le composant Webhook va permettre d’uniformiser le traitement des webhooks en définissant déjà un seul point de routage unique, par exemple ici /webhook et qui directement sera géré par le composant.

webhook:
       resource: @FrameworkBundle/Resources/config/routing/webhook.xml'
       prefix: /webhook

La configuration du composant permettra de déclarer les services pris en charge et une clé va définir l’url à communiquer pour le callback, par exemple ici l’url sera du coup /webhook/mailgun.

framework:
    webhook:
        routing:
            mailgun:
                service: mailer.webhook.request_parser.mailgun

Un cas classique d’API qui envoie des mises à jour de statut via des webhooks sont les systèmes d’envoi de mail ou SMS comme Mailjet ou ici dans l’exemple Mailgun. Ces systèmes sont déjà très bien intégrés dans l’écosystème Symfony grâce au composant Notifier et de nombreux bridges existants. C’est donc tout naturellement que ces bridges vont être enrichis pour pouvoir non seulement envoyer des mails par exemple mais aussi obtenir des statuts en retour, par exemple est-ce que le mail a bien été envoyé.

Dans notre exemple, le bridge mailgun-mailer s’est donc vu ajouter dans sa future version 6.3 un service mailer.webhook.request_parser.mailgun pour pouvoir parser le webhook reçu.

Évidemment, rien ne vous empêchera d’écrire vos propres parsers pour des webhooks non encore prévus, que ce soit pour des services d’envoi de mail ou de SMS mais aussi d’autres ! Un terrain de jeu idéal d’ailleurs serait de pouvoir traiter plus facilement les webhooks des systèmes de paiement par exemple.

Le composant RemoteEvent

Une fois le webhook reçu et parsé, faut-il encore pouvoir réagir dans votre application en fonction de son contenu. Si par exemple, nous reprenons notre exemple de mailgun, nous allons peut-être vouloir marquer des mails comme envoyés ou incrémenter un compteur pour gérer des statistiques par exemple.

L’idée ici est de pouvoir transformer les données reçues sur le webhook en un RemoteEvent qui devra être consommé par votre application pour être traité. Le RemoteEvent va être généré par un PayloadConverter.

Dans notre cas de Postgun, un MailgunPayloadConverter est bien présent dans le code et sera appelé par le parser pour générer un RemoteEvent. Mais pour standardiser un peu les choses, le composant RemoteEvent prévoit déjà des abstractions pour des événements liés aux mails et aux SMS. Il permet par exemple d’avoir un événement MailerDeliveryEvent qui peut être déclenché quand le système d’envoi de mail retourne une information sur l’état d’envoi du mail, ce qui est très souvent le cas.

namespace Symfony\Component\RemoteEvent\Event\Mailer;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @experimental in 6.3
 */
final class MailerDeliveryEvent extends AbstractMailerEvent
{
    public const RECEIVED = 'received';
    public const DROPPED = 'dropped';
    public const DELIVERED = 'delivered';
    public const DEFERRED = 'deferred';
    public const BOUNCE = 'bounce';

    private string $reason = '';

    public function setReason(string $reason): void
    {
        $this->reason = $reason;
    }

    public function getReason(): string
    {
        return $this->reason;
    }
}

Le fait de standardiser ainsi l’événement que les bridges retourneront lors du changement de statut d’un mail fait en sorte que le code que vous allez écrire pour consommer cet événement puisse être complètement agnostique du service d’envoi de mail et rester le même en cas de changement de l'envoi du mail.

Dans notre cas ici, le seul travail que vous aurez du coup à faire pour réellement traiter le Webhook reçu sera donc de consommer le RemoteEvent en écrivant un consumer ainsi.

use Symfony\Component\RemoteEvent\Attribute\AsRemoteEvent Consumer;
use Symfony\Component\RemoteEvent\Event Mailer\MailerDeliveryEvent;

#AsRemoteEvent Consumer(name: 'postgun')]
class MailSentConsumer
{
    public function consume(MailerDeliveryEvent $event): void {
        # Ici votre code peut traiter les données de l'$event
    }
}

L’attribut AsRemoteEventConsumer permet de déclarer sa classe comme devant consommer des RemoteEvent et recevra donc sur sa fonction consume() un MailerDeliveryEvent. Vous pourrez alors réagir en fonction du contenu de cet événement.

Le composant Scheduler

Réinventer la crontab et pouvoir enfin se passer de devoir programmer en dehors de votre application ces tâches récurrentes, beaucoup de projets ont essayé mais sans grand succès pour le moment. Souvent trop complexes dans leur mise en œuvre ou trop imprécis, pas assez généralisés aussi sans doute, on finit toujours par se dire qu’une bonne crontab c’est mieux. Mais force est de constater que ça comporte aussi des désavantages : les logs sont gérés à part, difficilement administrables depuis l’application, la définition même de la crontab se fait en dehors du projet…

C’est lors du SymfonyLive Paris 2023, très récemment donc, qu’un Fabien Potencier tout heureux de nous présenter le travail d’un autre que lui, nous a introduit au composant Scheduler avec des slides au titre improbable de “Shampoo Algorithm”. Ce composant est issu d’une proposition de Sergey Rabochiy en juillet 2022 et mergé dans la branche 6.3 de Symfony il y a quelques semaines.

Et plutôt que de vouloir réinventer cron en le copiant et en essayant de le reproduire en PHP, le composant Scheduler réutilise beaucoup de concepts et de composants Symfony existants pour pouvoir effectuer sa tâche, et à commencer par le composant Messenger.

Pour l’exemple, nous partirons d’une tâche dont le périmètre est de nettoyer des archives toutes les semaines. Nous voulons du coup avoir le message CleanArchiveMessage dispatché via Messenger et qu’un worker soit en mesure de le traiter. Le périmètre du composant Scheduler sera alors de dispatcher ce message au bon moment.

Pour cela, le composant Scheduler attend de votre code un Schedule qui liste la ou les tâches programmées et vous fournit une fonction RecurringMessage:every() pour pouvoir facilement programmer la récurrence des messages.

#[AsSchedule('default')]
class DefaultSchedule Provider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())
            ->add(RecurringMessage::every('1 week', new CleanArchiveMessage()))
            ;
}

La fréquence en premier argument ‘1 week’ est un format de date relatif de PHP et qui vous permet d’exprimer cette fréquence avec plein de possibilités, comme par exemple First saturday of next month pour le premier samedi du prochain mois. Le deuxième argument est du coup votre message.

Le Scheduler va alors se comporter pour Messenger comme un transport synchrone et émettre le message si les conditions de la programmation sont remplies, c'est-à-dire si le moment du prochain message est atteint.

Pour pouvoir consommer ces messages, vous devez seulement avoir un worker comme c’est déjà le cas pour Messenger mais dédié du coup à ce transport Scheduler.

bin/console messenger:consume -v scheduler_default

Vous aurez remarqué que vous pouvez du coup avoir plusieurs types de scheduler en les nommant, d’où ici le default, et c’est ainsi que pourrez organiser au mieux vos tâches entre différents schedules suivant votre besoin, voire plusieurs workers si vous en avez besoin. Le Scheduler ne gère pas lui-même d’état pour savoir si le dernier message qui aurait été envoyé l’a bien été ou pour être sûr qu’un seul worker traite le message vous aurez alors besoin de pouvoir cacher l’état et verrouiller l’exécution en faisant appel aux composants Cache et Lock.

Le périmètre de responsabilité du composant Schedule est alors bien défini : gérer la liste des messages à être envoyés et la date d’envoi du prochain message. Le composant se concentre précisément sur cela et s’appuie sur les autres composants de Symfony pour le reste.

Pour pouvoir programmer des messages vous pouvez également utiliser des expressions Cron si vous êtes plus habitué, avec RecurringMessage::cron() par exemple

#[AsSchedule('default')]
class DefaultScheduleProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return (new Schedule())
            ->add(RecurringMessage::cron('* 0 * * 0', new CleanArchiveMessage()));
    }
}

Et évidemment, si ça ne suffit pas, libre à vous d’implémenter votre propre logique pour pouvoir déclencher des messages à une date donnée en écrivant une classe qui doit implémenter l'interface TriggerInterface prévue pour cela.

Tout cela, et bien plus encore, est très bien décrit dans les slides mais je ne saurais que trop vous conseiller de regarder le replay de la keynote, pas ennuyante du tout par ailleurs, et qui en plus est disponible pour tous du moment que vous êtes connecté.

Est-ce que le composant Scheduleur va remplacer les crons ? Pas forcément, déjà bien sûr car la résistance au changement est grande. Parce qu'on trouvera bien des cas d'usage que le Scheduler n'aura pas encore prévus comme actuellement le lancement de script ou binaire n'est pas possible.

Mais le fait de pouvoir intégrer complément dans l'application les tâches programmées est un réel atout. Plus facile à configurer, à déployer, à monitorer, à loguer, les tâches automatisées deviendront partie intégrante de l'application. Il deviendra alors même possible de proposer plus facilement la configuration de ses tâches depuis un backoffice, et pourquoi pas imaginer l'administration des fréquences même.

Une version 6.3 prometteuse

En proposant des composants intelligents et offrant aux développeurs la possibilité de se concentrer sur le code métier en laissant le framework s'occuper de ce qui est plus technique au final, Symfony devient de plus en plus attractif dans ses dernières versions. La version 6.3 va apporter son lot d'améliorations dans ce sens et ces nouveaux composants sont de réelles avancées dont les développeurs devraient s'emparer très vite.

Suivez notre actualité en avant première. Pas plus d’une newsletter par mois.