web-dev-qa-db-fra.com

Afficher la progression pour un fonctionnement long PHP

J'ai un script PHP qui prendra probablement au moins 10 secondes pour s'exécuter. Je voudrais en afficher la progression pour l'utilisateur.

Dans la classe en cours d'exécution, j'ai une propriété $progress Qui est mise à jour avec la progression (en 1-100), et une méthode get_progress() (dont le but devrait être évident).

La question est de savoir comment mettre à jour l'élément <progress> Sur le frontal pour que l'utilisateur puisse le voir?

Je pense que AJAX est la solution, mais je n'arrive pas à comprendre. Je ne peux pas accéder à la même instance d'objet.

43
Madara Uchiha

Si votre tâche consiste à télécharger un énorme ensemble de données ou à le traiter sur le serveur, tout en mettant à jour la progression sur le serveur, vous devez envisager d'utiliser une sorte d'architecture de travaux, où vous lancez le travail et le faites avec un autre script exécuté sur le serveur. serveur (par exemple, mise à l'échelle/traitement d'images, etc.). En cela, vous faites une chose à la fois, formant ainsi un pipeline de tâches où il y a une entrée et une sortie finale traitée.

À chaque étape du pipeline, l'état de la tâche est mis à jour à l'intérieur de la base de données qui peut ensuite être envoyée à l'utilisateur par n'importe quel mécanisme serveur-Push qui existe de nos jours. L'exécution d'un script unique qui gère les téléchargements et les mises à jour met la charge sur votre serveur et vous limite également (que se passe-t-il si le navigateur se ferme, et si une autre erreur se produit). Lorsque le processus est divisé en étapes, vous pouvez reprendre une tâche ayant échoué à partir du point où elle a réussi en dernier.

Il existe de nombreuses façons de procéder. Mais le flux de processus global ressemble à ceci

enter image description here

La méthode suivante est ce que j'ai fait pour un projet personnel et ce script a bien fonctionné pour télécharger et traiter des milliers d'images haute résolution sur mon serveur qui ont ensuite été réduites en plusieurs versions et téléchargées sur Amazon s3 tout en reconnaissant les objets à l'intérieur. (Mon code d'origine était en python)

Étape 1 :

Initier le transport ou la tâche

Téléchargez d'abord votre contenu, puis renvoyez immédiatement un identifiant de transaction ou un uuid pour cette transaction via une simple demande POST. Si vous effectuez plusieurs fichiers ou plusieurs tâches dans la tâche, vous pouvez également gérer cette logique dans cette étape

Étape 2:

Faites le travail et renvoyez la progression.

Une fois que vous avez compris comment les transactions se produisent, vous pouvez utiliser n'importe quelle technologie Push côté serveur pour envoyer un paquet de mise à jour. Je choisirais WebSocket ou les événements envoyés par le serveur, selon le cas, pour revenir à l'interrogation longue sur les navigateurs non pris en charge. Une simple méthode SSE ressemblerait à ceci.

function TrackProgress(upload_id){

    var progress = document.getElementById(upload_id);
    var source = new EventSource('/status/task/' + upload_id );

    source.onmessage = function (event) {
        var data = getData(event); // your custom method to get data, i am just using json here
        progress.setAttribute('value', data.filesDone );
        progress.setAttribute('max', data.filesTotal );
        progress.setAttribute('min', 0);
    };
}

request.post("/me/photos",{
    files: files
}).then(function(data){
     return data.upload_id;
}).then(TrackProgress);

Côté serveur, vous devrez créer quelque chose qui garde une trace des tâches, une architecture de Jobs simple avec job_id et la progression envoyée à la base de données suffira. Je laisserais la planification des travaux à vous et au routage également, mais après cela, le code conceptuel (pour le plus simple SSE qui suffira au code ci-dessus) est le suivant).

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
/* Other code to take care of how do you find how many files are left 
   this is really not required */
function sendStatusViaSSE($task_id){
    $status = getStatus($task_id);
    $json_payload = array('filesDone' => $status.files_done,
                          'filesTotal' => $status.files_total);
    echo 'data: ' . json_encode( $json_payload ) . '\n\n';
    ob_flush();
    flush();

    // End of the game
    if( $status.done ){
        die();
    }

}

while( True ){
     sendStatusViaSSE( $request.$task_id );
     sleep(4);
}

?>

Un bon tutoriel sur SSE peut être trouvé ici http://html5doctor.com/server-sent-events/

et vous pouvez en savoir plus sur l'envoi de mises à jour depuis le serveur sur cette question Envoi de mises à jour depuis le serveur

Ce qui précède était une explication conceptuelle, il existe d'autres moyens d'y parvenir, mais c'était la solution qui a pris en charge une tâche assez énorme dans mon cas.

42
ShrekOverflow

Je vais mettre cela ici comme référence pour tous ceux qui recherchent - cela ne repose sur aucun javascript du tout ..

<?php

/**
 * Quick and easy progress script
 * The script will slow iterate through an array and display progress as it goes.
 */

#First progress
$array1  = array(2, 4, 56, 3, 3);
$current = 0;

foreach ($array1 as $element) {
    $current++;
    outputProgress($current, count($array1));
}
echo "<br>";

#Second progress
$array2  = array(2, 4, 66, 54);
$current = 0;

foreach ($array2 as $element) {
    $current++;
    outputProgress($current, count($array2));
}

/**
 * Output span with progress.
 *
 * @param $current integer Current progress out of total
 * @param $total   integer Total steps required to complete
 */
function outputProgress($current, $total) {
    echo "<span style='position: absolute;z-index:$current;background:#FFF;'>" . round($current / $total * 100) . "% </span>";
    myFlush();
    sleep(1);
}

/**
 * Flush output buffer
 */
function myFlush() {
    echo(str_repeat(' ', 256));
    if (@ob_get_contents()) {
        @ob_end_flush();
    }
    flush();
}

?>
52
l0ft13

C'est un peu difficile, (FYI) PHP et votre demande AJAX sont traités par un thread séparé, donc vous ne pouvez pas obtenir le $progress valeur.

Une solution rapide: vous pouvez écrire la valeur de progression dans $_SESSION['some_progress'] À chaque mise à jour, puis votre demande AJAX peut obtenir la valeur de progression en accédant au $_SESSION['some_progress'].

Vous aurez besoin de la fonction setInterval() ou setTimeout() de JavaScript pour continuer d'appeler le gestionnaire AJAX, jusqu'à ce que vous obteniez le retour sous la forme 100.

Ce n'est pas la solution parfaite, mais c'est rapide et facile.


Parce que vous ne pouvez pas utiliser la même session deux fois en même temps, utilisez plutôt une base de données. Écrivez l'état dans une base de données et lisez-le avec l'intervalle'd AJAX call.

26
Jarod DY Law

C'est une vieille question mais j'avais un besoin similaire. Je voulais exécuter un script avec la commande php system() et afficher la sortie.

Je l'ai fait sans sondage.

Pour le deuxième cas Rikudoit, cela devrait ressembler à ceci:

Javascript

document.getElementById("formatRaid").onclick=function(){
    var xhr = new XMLHttpRequest();     
    xhr.addEventListener("progress", function(evt) {
        var lines = evt.currentTarget.response.split("\n");
        if(lines.length)
           var progress = lines[lines.length-1];
        else
            var progress = 0;
        document.getElementById("progress").innerHTML = progress;
    }, false);
    xhr.open('POST', "getProgress.php", true);
    xhr.send();
}

PHP

<?php 
header('Content-Type: application/octet-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

// Turn off output buffering
ini_set('output_buffering', 'off');
// Turn off PHP output compression
ini_set('zlib.output_compression', false);
// Implicitly flush the buffer(s)
ini_set('implicit_flush', true);
ob_implicit_flush(true);
// Clear, and turn off output buffering
while (ob_get_level() > 0) {
    // Get the curent level
    $level = ob_get_level();
    // End the buffering
    ob_end_clean();
    // If the current level has not changed, abort
    if (ob_get_level() == $level) break;
}   

while($progress < 100) {
    // STUFF TO DO...
    echo '\n' . $progress;
}
?>
13
Ramon La Pietra

Les solutions sont:

  1. Interrogation Ajax - Du côté serveur, enregistrez la progression quelque part, puis utilisez un appel ajax pour récupérer la progression à intervalles réguliers.

  2. Événements envoyés par le serveur - Une fonctionnalité html5 qui permet de générer des événements dom à partir de la sortie envoyée par le serveur. C'est la meilleure solution pour un tel cas, mais IE 10 ne le prend pas en charge.

  3. Streaming de script/iframe - Utilisez un iframe pour diffuser la sortie du script de longue durée qui produirait des balises de script sous forme d'intervalles pouvant générer une réaction dans le navigateur.

7
Silver Moon

Ici, je voulais juste ajouter 2 préoccupations en plus de ce que @Jarod Law a écrit ci-dessus https://stackoverflow.com/a/7049321/2012407

Très simple et efficace en effet. J'ai peaufiné et utilisé :) Donc mes 2 préoccupations sont:

  1. plutôt que d'utiliser setInterval() ou setTimeout() utilisez un appel récursif en rappel comme:

    function trackProgress()
    {
        $.getJSON(window.location, 'ajaxTrackProgress=1', function(response)
        {
            var progress = response.progress;
            $('#progress').text(progress);
    
            if (progress < 100) trackProgress();// You can add a delay here if you want it is not risky then.
        });
    }
    

    Dans la mesure où les appels sont asynchrones, ils peuvent revenir dans un ordre indésirable dans le cas contraire.

  2. économiser dans $_SESSION['some_progress'] est intelligent et vous pouvez, pas besoin de stockage de base de données. Ce dont vous avez réellement besoin est de permettre aux deux scripts d'être appelés simultanément et de ne pas être mis en file d'attente par PHP. Donc, ce dont vous avez le plus besoin, c'est session_write_close() ! J'ai posté un exemple de démonstration très simple ici: https://stackoverflow.com/a/38334673/2012407

6
antoni

Avez-vous envisagé de générer du javascript et d'utiliser un vidage de flux? Cela ressemblerait à quelque chose comme ça

echo '<script type="text/javascript> update_progress('.($progress->get_progress()).');</script>';
flush();

Cette sortie est envoyée immédiatement au navigateur en raison du vidage. Faites-le périodiquement à partir du long script et cela devrait fonctionner à merveille.

4
Michael Petrov

Fréquemment dans les applications Web, nous pouvons avoir une demande au système principal qui peut déclencher un long processus tel que la recherche d'une énorme quantité de données ou un long processus de base de données. Ensuite, la page Web frontale peut se bloquer et attendre la fin du processus. Au cours de ce processus, si nous pouvons fournir à l'utilisateur des informations sur la progression du processus principal, cela peut améliorer l'expérience utilisateur. Malheureusement, dans les applications Web, cela ne semble pas être une tâche facile car les langages de script Web ne prennent pas en charge le multithreading et HTTP est sans état. Nous pouvons maintenant avoir AJAX pour simuler le processus en temps réel.

Fondamentalement, nous avons besoin de trois fichiers pour gérer la demande. Le premier est le script qui exécute le travail de longue durée et il doit avoir une variable de session pour stocker la progression. Le deuxième script est le script d'état qui fera écho à la variable de session dans le script de travail de longue durée. Le dernier est le côté client AJAX script qui peut interroger le script d'état fréquemment.

Pour les détails de l'implémentation, vous pouvez vous référer à PHP pour obtenir dynamiquement la progression du processus long

0
PixelsTech