49

I have an AJAX script to upload a file to 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.

John
  • 1
  • 13
  • 98
  • 177
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308

9 Answers9

54

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

?>
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
l0ft13
  • 710
  • 1
  • 7
  • 11
  • Why we need `sleep(1)`? It will take more time to complete the script. – Sithu Apr 02 '13 at 04:32
  • This is a great method, but care needs to be taken when the array is huge because it outputs a new DOM element each time. I recommend checking the percentage against a mod operator (%). – Matthew Muro Jul 30 '13 at 16:52
  • dont know my % showing is 1500% ++ gradually, i have so many values in my array any suggestions? – Samia Ruponti Sep 09 '13 at 19:37
  • 1
    may i know what this part is for `echo(str_repeat(' ', 256));` – slier Mar 27 '14 at 19:29
  • 2
    years later this is almost SSE – ShrekOverflow Nov 22 '14 at 12:12
  • @Sithu sleep() is probably to simulate time taken to execute some code. It's just for demonstration. – Cave Johnson Jun 16 '18 at 01:19
  • 3
    @slier In case you are still wondering, I believe this is to force the some servers to flush the output buffer because it only flushes the buffer when there is at least a certain amount of data in the response. – Cave Johnson Jun 16 '18 at 01:26
  • This is what it is. In my case - for my back end 'super user' upload tool - it is quick, dirty, perfect and *exactly* what I was looking for. Thanks for posting it! – Jon Vote May 06 '19 at 19:45
  • 1
    Thanks for the idea. To make job works I echoing longer string `echo str_repeat(' ', 4*1024), "\n";` to avoid http server and/or browser to wait for enough data in their buffer. – Cyrille37 Jan 26 '20 at 13:36
46

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

enter image description here

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.

Community
  • 1
  • 1
ShrekOverflow
  • 6,795
  • 3
  • 37
  • 48
  • ob_flush(); flush(); works when gzip is enabled on the server? I don't think so – yeahman Nov 03 '18 at 05:58
  • Down-voted for answering for an upload, burning the question and forcing (because the OP accepted the wrong answer to their own question) an edit. – John Nov 20 '20 at 01:53
27

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.

Amal Murali
  • 75,622
  • 18
  • 128
  • 150
Jarod Law Ding Yong
  • 5,697
  • 2
  • 24
  • 16
  • 1
    some developers use some object cache pattern, the object write to some cache, so ajax request can obtain the same object properties and value. – Jarod Law Ding Yong Aug 13 '11 at 08:16
  • This sounds like a good solution. I'd like to hear other answers though, if possible I'd like to avoid unnecessary `$_SESSION`s. – Madara's Ghost Aug 13 '11 at 08:30
  • 1
    use APC cache with ajax. Get progressing same as show progressing upload with apc. You can use Session , too. It is simple, can solve your matter. – meotimdihia Aug 13 '11 at 08:51
  • 2
    I don't think the session method will work well. session_start in the ajax script will block execution as that session is open in another file. An exception to this is if you use session_write_close in the original file but then you can't update the progress anymore. – Michael Petrov Aug 13 '11 at 15:44
  • Does APC persist between executions? Also, session is out because the session is already running while the large script executes, so it's not possible to use `session_start()` again while it's executing. (@MichaelP point noted and tested.) – Madara's Ghost Aug 15 '11 at 18:57
  • I went with something similar. Write progress to a database, get with timed Ajax request to a handler page. Simple as that. – Madara's Ghost Apr 02 '12 at 21:33
  • yeap sorry for late reply here, we shall use non-native (file) session. For me , I use sqlite. – Jarod Law Ding Yong Apr 04 '12 at 03:38
14

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;
}
?>
Ramon La Pietra
  • 609
  • 7
  • 13
9

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.

Silver Moon
  • 1,086
  • 14
  • 19
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

Community
  • 1
  • 1
antoni
  • 5,001
  • 1
  • 35
  • 44
3

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.

Michael Petrov
  • 2,247
  • 15
  • 15
  • Yes, though if I remember correctly, browsers don't render with less then x bytes sent. So that might prove to be a problem. Nonetheless, noted. Will try that too. – Madara's Ghost Aug 13 '11 at 20:22
  • You're correct, IE might buffer if it's less than 256 bytes. Just add an echo str_repeat(" ",256); – Michael Petrov Aug 13 '11 at 20:43
  • It's more then that. Chrome does it as well. And I'm not sure how much it takes. Nonetheless, when I'll be next to a computer, I'll test it. – Madara's Ghost Aug 13 '11 at 20:45
2

Here is the most effective, easy and tested solution through $_SESSION['progress'] , session_start() and session_write_close().

  1. The idea is we will save the progress in $_SESSION['progress'] and then lock the session to update the progress to the user continuously through session_write_close().

  2. use session_start(); on start of iteration and session_write_close() at the end of iteration.

  3. Then get this session variable value $_SESSION['progress'] through ajax from a different script.

  4. AND then show results through ajax response in progress_bar etc.

Now let's do some code:

Create a form/Request page to trigger AJAX request (request.php):

<div id="demo">
<button type="button" class="button">Do Something</button>
</div>

 $("button").click(function(){
  $.ajax({url: "request-handler.php", success: function(result){
    alert('done');
  }});
});

request-handler.php

$total = 100;
$count = 1;
while(true)
{
    /*write your code
     ....
    here*/


    session_start();
    $percent = ($count *100)/$total;
    $_SESSION["progress"] = number_format((float)$percent, 2, '.', '');
    $_SESSION["progress"] = (int) ceil($percent);
    $count++;
    session_write_close();
}

Ajax to get progress(front-view.php):

 <div class="progress" style="display: none;">
      <div class="progress-bar progress-bar-striped active" role="progressbar"
      aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:0%">
        0%
      </div>
    </div>

    <div class="alert alert-success alert-dismissible" style="display: none;">
      <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
      <strong>Done!</strong> Successfully Done!.
    </div>

<script type="text/javascript">

  var pBar = $(".progress-bar");
  var progress = 0;
  var intervalId = window.setInterval(function(){
    $.get("get-progress.php", function(data, status){
      progress = data;
    });
    if(progress > 0)
    {
      pBar.parent().show();
    } 
    var percent = progress+'%';
    pBar.width(percent);
    pBar.html(percent);
    if(progress >= 100)
    {
      pBar.parent().hide();
     $ (".alert").show();
      clearInterval(intervalId);
    } 
     
  }, 2000);
    
 

       </script> 

get-progress.php:

<?php

session_start();

$progress = $_SESSION['progress'];
if ($progress >= 100) {
    session_start();
    unset($_SESSION['progress']);
}
//output the response
echo json_encode($progress);
?>
1

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

PixelsTech
  • 3,229
  • 1
  • 33
  • 33