Progression des processus longs en php avec ajax et les EventSource

Lorsqu'un processus prend du temps à s'exécuter, il est crucial de fournir une indication visuelle de sa progression. Sans cela, l'utilisateur peut croire que le site est figé. Heureusement, AJAX et EventSource permettent de suivre et d'afficher ces avancées en temps réel.

En java j'étais habitué à travailler avec le SwingWorker (voir la javadoc de l'AbstractBrolWorker) pour remonter les informations du modèle vers la vue lors d'utilisations de traitements longs, mais je souhaitais faire de même avec php, par exemple dans le cadre de mon serveur MediaCenter HomeCinema afin qu'il récupère les métadonnées des vieilles vidéos de famille faites au GSM, et le classement de toutes les photos, les doublons, les données de géolocalisation, etc…

Ce sont des processus extrêmement longs, dont je souhaitais pouvoir suivre la progression avec une remontée d'informations filtrées, et pouvoir interrompre le traitement automatiquement en cas d'erreur critique, ou simplement l'interrompre manuellement et pouvoir reprendre le traitement là où il s'était arrêté.

Dans cette partie, nous allons nous intéresser à la soumission des données, et à l'écoute des messages envoyés par le serveur.

Les éléments essentiels

Nous allons utiliser :

  • Un formulaire HTML pour recueillir les informations de l'utilisateur.
  • Un script JavaScript qui envoie les données et écoute les événements.
  • Un script PHP exécutant la tâche longue et envoyant des mises à jour.
  • Un EventSource pour récupérer les mises à jour et mettre à jour l'interface.

Déroulement du processus

Voici comment cela fonctionne :

  1. L'utilisateur soumet un formulaire.
  2. Les données sont envoyées via AJAX à un script PHP.
  3. Si tout est correct, un EventSource s'active pour suivre la progression.
  4. Le PHP traite la demande et envoie régulièrement des mises à jour.
  5. Chaque mise à jour ajuste l'affichage en temps réel.

Étape 1 : Création du formulaire HTML

Nous allons d'abord concevoir un formulaire simple :

  1. <form id="process-form" action="startProcess.php" method="post">
  2. <input type="text" name="parametre" placeholder="Entrez une valeur" />
  3. <button type="submit">Démarrer</button>
  4. </form>

Puis nous devons ajouter le lien vers le code à exécuter côté client :

  1. <script src="ajax-submit-form.js"></script>

Étape 2 : Envoi des paramètres en AJAX

Le JavaScript capture l'événement et envoie les données :

  1. document.getElementById("process-form").addEventListener("submit", function(event) {
  2. event.preventDefault();
  3. fetch("startProcess.php", {
  4. method: "POST",
  5. body: new FormData(this)
  6. }).then(response => response.json())
  7. .then(data => console.log("Processus démarré", data));
  8. });

Il est possible si on utilise jquery de jaire appel à $.ajax :

  1. $('#process-form').submit(function(event) {
  2. event.preventDefault();
  3. $.ajax({
  4. url: 'startProcess.php',
  5. type: 'POST',
  6. data: new FormData(this),
  7. processData: false,
  8. contentType: false,
  9. success: function(data) {
  10. console.log('Processus démarré', data);
  11. },
  12. error: function(xhr, status, error) {
  13. console.error('Erreur lors de l\'envoi du formulaire :', status, error);
  14. }
  15. });
  16. });

Lancement du traitement PHP

Le PHP exécute la tâche et envoie des mises à jour :

  1. <?php
  2. header("Content-Type: text/event-stream");
  3. header("Cache-Control: no-cache");
  4.  
  5. // Le serveur envoie des mises à jour sur la progression du processus.
  6. for ($i = 0; $i <= 100; $i += 10) {
  7. // Envoi des données sous forme d'événement, chaque message commence par 'data:'
  8. // Cela permet au client de reconnaître les messages comme étant des événements à traiter.
  9. echo "data: " . json_encode(["progress" => $i]) . "
  10.  
  11. ";
  12. ob_flush(); // Vide le tampon de sortie et envoie le contenu au client
  13. flush(); // Assure que le contenu est effectivement envoyé au client
  14. sleep(1); // Pause pour simuler la progression du traitement
  15. }
  16. ?>

EventSource (ou ESS pour Server-Sent Events) est une technologie permettant au serveur d'envoyer des données en continu vers le client via une connexion HTTP persistante. Contrairement à AJAX, où le client envoie des requêtes régulières, avec l'EventSource, c'est le serveur qui envoie les données en temps réel, ce qui est plus efficace pour des mises à jour en continu (comme pour la progression d'un processus).

Sur la ligne echo "data: " . json_encode(["progress" => $i]) . "\n\n"; :

  • data: est un préfixe nécessaire pour que l'événement envoyé au client soit compatible avec l'EventSource. Ce préfixe permet de signaler au client que la donnée qui suit est un message à traiter.
  • Le format des données envoyées doit respecter ce préfixe data: suivi des données encodées en JSON. Dans cet exemple, on envoie un objet JSON contenant la progression sous forme de pourcentage ("progress" => $i).
  • Chaque message envoyé par le serveur doit être suivi de \n\n pour être correctement interprété par l'EventSource.
    Ici comme on initie l'appel depuis ajax, on ne récupère les messages qu'en un seul bloc, donc la séparation avec les retours à la ligne aide beaucoup.
    Plus tard dans la partie avec les évènements, si le serveur est mal configuré ou ne prend pas en charge, le flush risque de ne pas se faire et au lieu de récupérer un seul json on doit traiter une chaîne de caractère qui contient plsusieurs json à la suite, séparés par ces retours à la ligne.

Explication générale des envois depuis le serveur :

Que vous capturiez les résultats en un seul bloc après l'appel $.ajax() ou que vous écoutiez les événements via onmessage, la méthode utilisée pour envoyer les données du côté serveur sera toujours la même. Les données seront envoyées sous forme d'événements préfixés par data:, que ce soit dans le cas d'un EventSource ou d'un appel AJAX. Ce mécanisme assure que le client pourra traiter les données de manière uniforme, peu importe le mode de récupération choisi.

Étape 4 : Mise en place de l'EventSource

Il faut ajouter à notre page le lien vers un nouveau script :

  1. <script src="events-listener.js"></script>

Le navigateur écoute les événements :

  1. const eventSource = new EventSource("progress.php");
  2. eventSource.onmessage = function(event) {
  3. console.log("Progression : ", event.data.progress);
  4. };

Étape 5 : Mise à jour dynamique de la page

Enfin, nous affichons la progression :

  1. eventSource.onmessage = function(event) {
  2. document.getElementById("progress-bar").style.width = event.data.progress + "%";
  3. };

Grâce à cette méthode, l'utilisateur peut voir en temps réel l'évolution du traitement.

Pourquoi séparer l'appel AJAX pour soumettre le formulaire et l'écoute du processus long ?

Le principe

Information

Lorsqu'on travaille avec des formulaires et des processus longs, il est important de séparer deux actions distinctes : envoyer les données du formulaire et suivre l'évolution du processus. Cela permet de ne pas bloquer l'interface et d'offrir une expérience utilisateur plus fluide. Voici les deux méthodes utilisées :

  • AJAX permet d'envoyer les données au serveur et d'obtenir une réponse en un seul bloc. C'est pratique pour envoyer un formulaire.
  • onmessage permet d'écouter des événements en temps réel, comme l'avancement du processus, sans interrompre l'utilisateur.

Dans une application web, lorsque vous envoyez des données au serveur, vous devez d'abord récupérer ces informations, puis les envoyer pour traitement. Cependant, pour des processus longs, l'attente de la réponse du serveur peut rendre l'interface utilisateur lente. C'est pourquoi il est préférable de séparer l'envoi du formulaire et l'écoute des événements de progression. Les deux méthodes suivantes sont souvent utilisées :

  • AJAX est très utile pour envoyer des données au serveur et obtenir une réponse en un seul bloc. Cependant, une fois la demande envoyée, AJAX récupère une réponse complète (généralement après que tout le processus est terminé).
  • onmessage, par contre, est utilisé pour écouter des événements envoyés par le serveur en temps réel. Cette méthode ne permet pas de récupérer les informations envoyées par le formulaire, sauf si vous les incluez dans l'URL en GET, ce qui présente des limitations (comme la taille de l'URL et des risques de sécurité).

Pour aller plus loin

La séparation des préoccupations entre l'envoi des données et l'écoute des événements de progression d'une tâche permet d'éviter de bloquer le processus et d'améliorer l'expérience utilisateur. Voici une explication détaillée des deux approches :

  • AJAX permet une soumission de formulaire asynchrone, où les données sont envoyées au serveur en une seule requête HTTP, et la réponse est récupérée dans son ensemble après le traitement. Cela signifie que le serveur effectue tout le travail et renvoie un seul bloc de données. C'est une solution simple et efficace pour des tâches qui ne nécessitent pas de suivi en temps réel, mais le client ne peut pas recevoir de mises à jour progressives. Il doit attendre que tout le processus soit terminé. Par exemple, l'envoi d'un formulaire avec $.ajax() :
  • 
            $.ajax({
                url: 'startProcess.php',
                type: 'POST',
                data: $(this).serialize(),
                success: function(response) {
                    console.log("Processus terminé : ", response);
                }
            });
        
  • onmessage permet d'écouter les événements en temps réel via un EventSource. Cependant, cette méthode ne peut pas récupérer les données envoyées initialement dans le formulaire. Pour transmettre ces informations à l'EventSource, une solution serait d'utiliser des paramètres en GET dans l'URL, mais cela présente des risques, notamment :
    • Limitation de taille d'URL : Les navigateurs ont une limite de taille d'URL (souvent autour de 2000 caractères). Si vous devez transmettre de grandes quantités de données via l'URL, cela peut poser problème.
    • Problèmes de sécurité : Passer des informations sensibles dans l'URL via GET peut être risqué, car les données sont visibles dans la barre d'adresse et peuvent être interceptées ou stockées dans les journaux serveur.
    
            const eventSource = new EventSource("progress.php?parametre=valeur");
            eventSource.onmessage = function(event) {
                console.log("Progression : ", event.data);
            };
        

Ainsi, bien que $.ajax() offre un moyen simple d'envoyer les données et récupérer une réponse après le traitement, onmessage fournit une solution plus souple pour écouter des événements en temps réel. Toutefois, la combinaison des deux méthodes permet de maximiser la réactivité de l'application, en envoyant les données via AJAX et en écoutant la progression via EventSource.

Et ensuite?

  • j'ai créé une enum (à partir de php 8.1.0) pour avoir une choix cohérent de paramètres à passer au json (par exemple status, message, file, line, stacktrace) ainsi qu'une enum avec les différents status possibles (par exemple success, progress, error, debug). Cela permet de nous assurer que les envois des messages respectent le même format;
  • j'ai délégué l'envoi des messages à une classe qui peut déterminer si les messages sont sollicités par le côté client (ajax ou évènements) ou si ils sont demandés par un script qui effectue des affichages traditionnels et alors il bascule vers des echo();
  • cette page ne décrit pas en détail le mécanisme de transmission des donnés, mais j'ai choisi l'option de sérialiser les informations du formulaire après soumission de la requête ajax, et je les lis dans le handler de messages qui démarre en cas de success de l'ajax

English translation

You have asked to visit this site in English. For now, only the interface is translated, but not all the content yet.

If you want to help me in translations, your contribution is welcome. All you need to do is register on the site, and send me a message asking me to add you to the group of translators, which will give you the opportunity to translate the pages you want. A link at the bottom of each translated page indicates that you are the translator, and has a link to your profile.

Thank you in advance.

Document created the 15/02/2025, last modified the 15/02/2025
Source of the printed document:https://www.gaudry.be/en//php-progression-processus-long.html

The infobrol is a personal site whose content is my sole responsibility. The text is available under CreativeCommons license (BY-NC-SA). More info on the terms of use and the author.