By Rob Gravelle

Show Progress Report for Long-running PHP Scripts

If you've ever downloaded a large video or installation file, you could always follow your progress on the download dialog:

Unfortunately, browsers aren't able to update you on the progress of long-running processes that take place on the server - that is until they're done. Meanwhile, you have no way of knowing if anything is in fact happening at all. A lot of people have tried to implement their own progress bar, but had varying degrees of success. Recently, the introduction of Server-Side Events (SSEs) have been a great step forward in this regard. In today's article we'll be taking a look at how to use SSEs to provide feedback and a progress bar for server-side processes.

Emulating a Long-running Process

The easiest way to make a script go on for a long time without actually doing anything is to run a loop. It also helps to free up processor time by calling a sleep function within the loop. PHP is a great language to use for the purposes of emulating a long-running process and it does indeed have a sleep() function. It accepts an integer specifying the number of seconds to sleep for. Hence, the following for loop will take about 60 seconds to complete and will send exactly 60 "processing..." messages to the client:

<?php //long_process.php for($i=1;$i<=60;$i++){ //do something echo 'processing...'; sleep(1); } ?>

Generating Server-sent Events Within a Loop

In my Receive Updates from the Server Using the EventSource article, I introduced Server-sent Events and their role in producing server pushes to the client. Under normal circumstances, the browser will maintain a connection to the script so long as it is running and does not issue a message to terminate. Combine that with a flexible ID-driven messaging system and you've got yourself an ideal way to send progress updates to the client. The best part is that with a little JSON encoding, we can send additional information as well as percentage numbers for the progress bar.

<?php header('Content-Type: text/event-stream'); // recommended to prevent caching of event data. header('Cache-Control: no-cache'); function send_message($id, $message, $progress) { $d = array('message' => $message , 'progress' => $progress); echo "id: $id" . PHP_EOL; echo "data: " . json_encode($d) . PHP_EOL; echo PHP_EOL; ob_flush(); flush(); } //LONG RUNNING TASK for($i = 1; $i <= 10; $i++) { send_message($i, 'on iteration ' . $i . ' of 10' , $i*10); sleep(1); } send_message('CLOSE', 'Process complete');

Monitoring Task Progress from the Browser

The following page is very much the same as the one from the EventSource article, with the addition of an HTML5 progress bar. Afterall, if a browser supports Server-sent Events, it probably also supports the new progress bar.

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <br /> <input type="button" onclick="startTask();" value="Start Long Task" /> <input type="button" onclick="stopTask();" value="Stop Task" /> <br /> <br /> <p>Results</p> <br /> <div id="results" style="border:1px solid #000; padding:10px; width:300px; height:250px; overflow:auto; background:#eee;"></div> <br /> <progress id='progressor' value="0" max='100' style=""></progress> <span id="percentage" style="text-align:right; display:block; margin-top:5px;">0</span> </body> </html>

Listening for Server Updates

The JavaScript EventSource object provides an onmessage() event that we can bind to in order to receive communications from the server. These are held in the event parameter's data attribute. Recall that it is a JSON-formatted string so we have to run the data through the JSON.parse() method before we can work with it. Once decoded, the message is logged to the results DIV and the progress is used to set the progress bar. I went a step further and display the actual percent value in a <span> element and continually update its width so as to move the number along with the progress bar meter.

The "CLOSE" termination signal is sent via the EventSource ID. On the client-side, it is stored in the event's lastEventId property. Upon receiving the "CLOSE" EventSource ID, the communication channel is closed by the client and the progress bar's value is set to it's maximum to represent completion of the task.

var es; function startTask() { es = new EventSource('sse_progress.php'); //a message is received es.addEventListener('message', function(e) { var result = JSON.parse( e.data ); addLog(result.message); if(e.lastEventId == 'CLOSE') { addLog('Received CLOSE closing'); es.close(); var pBar = document.getElementById('progressor'); pBar.value = pBar.max; //max out the progress bar } else { var pBar = document.getElementById('progressor'); pBar.value = result.progress; var perc = document.getElementById('percentage'); perc.innerHTML = result.progress + "%"; perc.style.width = (Math.floor(pBar.clientWidth * (result.progress/100)) + 15) + 'px'; } }); es.addEventListener('error', function(e) { addLog('Error occurred); es.close(); }); } function stopTask() { es.close(); addLog('Interrupted'); } function addLog(message) { var r = document.getElementById('results'); r.innerHTML += message + '<br>'; r.scrollTop = r.scrollHeight; }

Here is a snapshot of the HTML code in Chrome produced by the SSE's onmessage() event:







Results

on iteration 1 of 10

on iteration 2 of 10

on iteration 3 of 10

on iteration 4 of 10

on iteration 5 of 10

on iteration 6 of 10



60%

Conclusion

Though few and far between, there are some browsers that don't support Server-sent Events or the Progress Element. We'll consider some fallbacks for such older and non-compliant browsers in the next article.

Rob Gravelle resides in Ottawa, Canada, and is the founder of GravelleWebDesign.com. Rob has built systems for Intelligence-related organizations such as Canada Border Services, CSIS as well as for numerous commercial businesses.

In his spare time, Rob has become an accomplished guitar player, and has released several CDs. His band, Ivory Knight, was rated as one Canada's top hard rock and metal groups by Brave Words magazine (issue #92).