-->

Show progress for long running PHP script

2019-01-02 20:13发布

问题:

I have a PHP script that is likely to take at least 10 seconds to run. I would like to display progress for it for the user.

In the executing class, I have a property $progress which is updated with the progress (in 1-100), and a method get_progress() (which's purpose should be obvious).

The question is, how to update the <progress> element on the front end for the user to see?

I think AJAX is the solution, but I just can not get my head around it. I can not get to the same object instance.

回答1:

If your task is to upload a huge data-set or process it on the server, while updating progress to the server you should consider going with some sort of jobs architecture, where you initiate the job and do it with some other script running on the server (for example scaling / processing images etc). In this you do one thing at a time, thus forming a pipeline of tasks where there is an input and a final processed output.

At each step of pipeline the status of task is updated inside the database which can then be sent to the user by any server-push mechanism which exists these days. Running a single script which handles uploads and updates puts load on your server and also restricts you (what if the browser closes, what if some other error occurs). When process is divided into steps you can resume a failed task from the point where it succeeded last.

There exists many ways to do it. But the overall process flow looks like this

The following method is what I did for a personal project and this script held good for uploading and processing thousands of high resolution image to my server which then were scaled down into multiple versions and uploaded to amazon s3 while recognizing objects inside them. (My original code was in python)

Step 1 :

Initiate the transport or task

First Upload your content and then return a transaction id or uuid for this transaction immediately via a simple POST request. If you are doing multiple files or multiple things in the task, you may also want to handle that logic in this step

Step 2:

Do the job & Return the progress.

Once you have figured out how transactions occur you can then use any server side push technology to send update packet. I would choose WebSocket or Server Sent Events whichever applicable falling back to Long Polling on un-supported browsers. A simple SSE method would look like this.

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);

On the server side, you will need to create something which keeps a track of the tasks, a simple Jobs architecture with job_id and progress sent to the db shall suffice. I would leave the job scheduling to you and the routing as well, but after that the conceptual code (for simplest SSE which will suffice the above code) is as follows.

<?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);
}

?>

A good tutorial on SSE can be found here http://html5doctor.com/server-sent-events/

and you can read more about pushing updates from the server on this question Pushing updates from server

The above was a conceptual explanation, there are other ways to achieve this but this was the solution that took care of a fairly huge task in my case.



回答2:

I'll put this here as a reference for anyone searching - this relies on no javascript at all..

<?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();
}

?>


回答3:

It's kinda difficult, (FYI) PHP process and your AJAX request are being handled by separate thread, hence you can't get the $progress value.

A quick solution: you can write the progress value to $_SESSION['some_progress'] every time it is being updated, then your AJAX request can get the progress value by accessing the $_SESSION['some_progress'].

You'll need JavaScript's setInterval() or setTimeout() to keep calling the AJAX handler, until you get the return as 100.

It is not the perfect solution, but its quick and easy.


Because you cannot use the same session twice at the same time, use a database instead. Write the status to a database and read from that with the interval'd AJAX call.



回答4:

It's an old question but I had a similar need. I wanted to run a script with the php system() command and show the output.

I've done it without polling.

For Second Rikudoit case should be something like this:

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;
}
?>


回答5:

Solutions are :

  1. Ajax polling - On the server side store the progress somewhere and then use a ajax call to fetch the progress at regular intervals.

  2. Server sent events - An html5 feature that allows to generate dom events from output send by server. This is the best solution for such a case, but IE 10 does not support it.

  3. Script/Iframe streaming - Use an iframe to stream output from the long running script which would output script tags as intervals that can generate some reaction in the browser.



回答6:

Have you considered outputting javascript and using a stream flush? It would look something like this

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

This output is sent immediately to the browser because of the flush. Do it periodically from the long running script and it should work beautifully.



回答7:

Here I just wanted to add 2 concerns on top of what @Jarod Law wrote above https://stackoverflow.com/a/7049321/2012407

Very simple and efficient indeed. I tweaked-and-used :) So my 2 concerns are:

  1. rather than using setInterval() or setTimeout() use a recursive call in callback like:

    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.
        });
    }
    

    As the calls are asynchronous in may come back in unwanted order otherwise.

  2. saving into $_SESSION['some_progress'] is smart and you can, no need a database storage. What you actually need is to allow both scripts to be called simultaneously and not being queued by PHP. So what you need the most is session_write_close()! I posted a very simple demo example here: https://stackoverflow.com/a/38334673/2012407



回答8:

Frequently in web applications, we may have a request to the back end system which may trigger a long running process such as searching huge amount of data or a long running database process. Then the front end webpage may hang and wait for the process to be finished. During this process, if we can provide the user some information about the progress of the back end process, it may improve user experience. Unfortunately, in web applications, this seems not an easy task because web scripting languages don't support multithreading and HTTP is stateless. We now can have AJAX to simulate real time process.

Basically we need three files to handle the request. The first one is the script which runs the actual long running job and it needs to have a session variable to store the progress. The second script is the status script which will echo the session variable in the long running job script. The last one is the client side AJAX script which can poll the status script frequently.

For the details of the implementation, you can refer to PHP to get long running process progress dynamically



标签: