Symfony : Exécuter un callback après la réponse du Controller

Voyons ensemble comment continuer à exécuter du code après que le Contrôleur Symfony ait répondu au client.

Commençons par créer une classe AsyncReponse que l’on pourra retourner en tant que réponse de Contrôleur.
A la différence d’un objet Response classique, cette classe va prendre en paramètre de construction une fonction de callback qu’elle va pouvoir exécuter.

Si cette classe vous semble impressionnante à première vue pas d’inquiétude, il s’agit de l’élément le plus complexe du système que nous allons mettre en place et son fonctionnement est détaillé un peu plus bas.

Détaillons un peu cette classe !

La fonction send() est héritée de la classe “Response”. Elle sera appelée automatiquement lorsqu’une action de Controleur retournera un objet AsyncResponse.

Nous allons modifier l’implémentation de cette fonction pour lui faire retourner la réponse HTTP sans pour autant mettre fin au processus PHP en cours. Pour cela, nous allons utiliser les fonctions ob_* de PHP.

Les fonctions ob_* permettent de temporiser le retour de PHP. Une fois le retour temporisé, il est possible de le déclencher “manuellement”. Cela va nous permettre de le rendre asynchrone par rapport à l’exécution de la fonction callback.

Quelques informations sur les fonctions ob_* :

ob_start() : Démarre le tampon de sortie

ob_get_level() : Nous permet de déterminer si des tampons sont déjà en cours

Les fonctions sendContent() et sendHeader() sont également héritées de la classe Response et vont renvoyer au client le contenu de la réponse et son header HTTP.

Et pour finir, les fonctions flush() et session_write_close() nous permettent respectivement de nettoyer le tampon en cours et de fermer la session utilisateur afin d’éviter de bloquer les prochaines requêtes.

Nous avons également ajouté un paramètre $callback (avec un “setter” setTerminateCallback() ) ainsi qu’une fonction callTerminateCallback(). Cette dernière va nous permettre, dans un second temps, d’exécuter notre fonction callback.

Pour appeler cette fameuse fonction callback, mettons en place un EventSubscriber qui va s’exécuter automatiquement après l’envoi d’une réponse HTTP. Notre voulons que cet EventSubscriber exécute notre fonction de callback.

Comme nous avons associé notre fonction onTerminate avec l’évènement ”kernel.terminate”, celle-ci va s’exécuter à la fin de l’exécution du kernel (c’est à dire, juste après avoir envoyé la réponse HTTP au client).

En réalité il serait possible de directement placer le code que l’on désire exécuter dans cet EventSubscriber mais ça ne serait pas aussi souple que de pouvoir définir n’importe quel callback !

Dans la fonction onTerminate, nous récupérons toutes les réponses envoyées par tous les Controleurs de notre application. S’il s’agit d’une réponse de type AsyncResponse, nous appelons sa méthode callTerminateCallback().

Il ne nous reste plus qu’à créer un contrôleur et à instancier un objet AsyncResponse. Il faudra lui passer le callback que l’on désire exécuter en 4eme paramètre (ici un simple sleep() pour vérifier qu’il ne bloque pas le retour de php)

En allant sur l’url /test-async de notre application, celle-ci devrait nous répondre instantanément. Pourtant le callback sera bien appelé !

Si ce que nous avons fait jusqu’à présent n’est pas encore tout à fait clair pour vous, voici un petit résumé de l’ordre d’exécution des différentes éléments de notre système :

  1. Instanciation de l’AsyncResponse
  2. Exécution du constructeur de l’AsyncResponse
  3. Retour de l’object AsyncResponse par l’action du controleur
  4. Exécution de la fonction Send() de l’AsyncResponse => La réponse est envoyée au navigateur
  5. Exécution du subscriber onTerminate
  6. Exécution du callback

Nous sommes maintenant capables de créer un callback asynchrone dans Symfony ! Malheureusement, pour le moment ce n’est pas très pratique. Dans l’état actuel le callback n’a même pas accès au conteneur de service et nous ne pouvons même pas utiliser un logger pour prouver que le callback est réellement exécuté.

Remédions à ça tout de suite !

Modifions la classe AsyncReponse afin de pouvoir passer des paramètres supplémentaires au callback. Cela va nous permettre de lui passer le service logger ainsi qu’un message à logger.

En plus du paramètre $callback, nous avons ajouté un paramètre $callbackArgs qui va contenir un tableau d’arguments à transmettre à la fonction de callback.

Dans le constructeur de la classe, tous les arguments passés après le callback seront automatiquement transférés à ce dernier via un tableau.

Nous pouvons maintenant modifier le Controleur pour passer en paramètre au callback ce fameux logger qui va nous permettre de prouver que le callback est bien exécuté :

Maintenant, lorsque l’on fait appel à l’url /test-async, on peut se rendre dans nos logs et voir apparaître :

[2020–09–27T15:19:36.835872+02:00] app.INFO: testAsync Controller … [] []

[2020–09–27T15:19:46.836237+02:00] app.INFO: HELLO WORLD [] []

On voit même que les deux logs ont été enregistrés exactement à 10s de différence !

Voilà c’est terminé, nous avons atteint notre objectif. Nous pouvons maintenant exécuter un callback après la réponse du controleur.

Pour plus de détails techniques, vous pouvez vous rendre sur ce ticket stackoverflow (en anglais). Il m’a inspiré l’écriture de ce billet et une bonne portion du code ci-dessus est basée sur les solutions proposées.

J’espère que ce billet vous a aidé. Si c’est le cas ou si vous avez des remarques, n’hésitez pas à me le faire savoir en commentaire !

Written by

Lead Développeur PHP/Symfony, Chargé de recrutement technique

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store