How to display loading percentage and how to do it

2019-01-22 10:44发布

问题:

I want to make something similar to loaders in PHP so I used this code:

<?php 
$x=1;
while($x<=100) {
   echo "Loading: $x %<br>";
   $x++;
}   
?>

So that it would display from "Loading 1%" to "Loading 100%". But this will result in all appearing one at a time without disappearing after the new line appears. So I want to know how to make the new line appear and the old disappear and this starts after the page loads, so the user will be able to watch a loader actually loading from 1% to 100%.

UPDATE: I know I should use JS and/or Ajax to achieve it, I just wanted to know if there's a way to also do it in PHP :)

回答1:

To follow long-running tasks is common but not really easy to implement the first time. Here is a complete example.

(a sample of long-running task in a Sellermania's product)

Context

The task

Imagine you currently have the following task, and want to display a progress bar to your visitors.

PHP task.php

  <?php

  $total_stuffs = 200;
  $current_stuff = 0;
  while ($current_stuff < $total_stuffs) {
    $progress = round($current_stuff * 100 / $total_stuffs, 2);

    // ... some stuff
    sleep(1);

    $current_stuff++;
  }

The UI

Your beautiful UI looks like this:

HTML ui.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a id="run_task" href="#">Run task</a>

        <div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

        <script type="text/javascript">

            $('#run_task').click(function(e) {
                e.preventDefault();
                $.get('task-launch.php');
            });

        </script>

    </body>
</html>

The launcher

We need to launch the task in background, in order to make this demonstration relevant. So, this php script will be called asynchronously when clicking "run", and will execute the task above in background.

PHP task-launch.php

<?php

  // launches the task in background
  // see https://stackoverflow.com/a/12341511/731138 for details about the arguments
  exec("/usr/bin/php task.php > /dev/null 2>&1 &");

Problems

There are 3 problems here:

  1. you can run the task more than once by clicking several times the run button, how to avoid several tasks in the background at the same time?

  2. the task is ran at server side, so we need to do something to access the server and ask for progression information.

  3. and when we will be connected to the server side, the $progress variable is unavailable for reading, because it is stored within the context of the task.php running instance.


Solutions

Store progression information into something readable from outside

Usually, progression information are stored into a database, or a file, or whatever that can be writtable by a program (your task actaully), and readable by another (your ui where progression should be displayed).

I developped a class for sharing data within several php applications (available on github here), it works about the same way as stdClass but always securely synchronize its contents into a file.

Just download src/Sync.php and change the task.php above by:

PHP task.php

<?php

require("Sync.php");

$total_stuffs = 200;
$current_stuff = 0;

$shared = new Sync("some_file.txt");
$shared->progress = 0;

while ($current_stuff < $total_stuffs) {
  $shared->progress = round($current_stuff * 100 / $total_stuffs, 2);

  // ... some stuff
  sleep(1);

  $current_stuff++;
}

// to tell the follower that the task ended
$shared->progress = null;

Important note: here, some_file.txt is where are stored your task's shared data, so don't hesitate to use "task_[user_id].txt" for example if each user has its own task. And look at the readme on github to optimize file access.

Use the synchronized variable to protect the task launcher

  • The progression is set to 0 at the beginning of the task, so the first thing to do is to check, before running the task, that this progression is not set to 0.

PHP task-launch.php

<?php

require("Sync.php");
$shared = new Sync("some_file.txt");

if (is_null($shared->progress)) {
  exec("/usr/bin/php task.php > /dev/null 2>&1 &");
}
  • If the run button is clicked twice very quickly, we can still have 2 instances of the task. To handle this case, we need to simulate a mutex, in a word, make the variable only available to the current application to do some stuff - other applications will stay sleeping until the shared variable is unlocked.

PHP task.php

<?php

require("Sync.php");

$total_stuffs = 200;
$current_stuff = 0;

$shared = new Sync("some_file.txt");

// here is the magic: impossible to set the progression to 0 if an instance is running
// ------------------
$shared->lock();
if (!is_null($shared->progress))
{
    $shared->unlock();
    exit ;  
}
$shared->progress = 0;
$shared->unlock();
// ------------------

while ($current_stuff < $total_stuffs) {
  $shared->progress = round($current_stuff * 100 / $total_stuffs, 2);

  // ... some stuff
  sleep(1);

  $current_stuff++;
}

// the task ended, no more progression
$shared->progress = null;

Warning: if your task crashes and never reach the end, you'll never be able to launch it anymore. To avoid such cases, you can also store the child's getmypid() and some time() stuffs inside your shared variable, and add a timeout logic in your task.

Use polling to ask the server progression information

Polling stands for asking for progression information to the server every lapse of time (such as, 1 sec, 5 secs or whatever). In a word, client asks progression information to the server every N secs.

  • at server-side, we need to code the handler to answer the progression information.

PHP task-follow.php

<?php

require("Sync.php");

$shared = new Sync("some_file.txt");

if ($shared->progress !== null) {
    echo $shared->progress;
} else {
    echo "--"; // invalid value that will break polling
}
  • at client-side, we need to code the "asking progression information to the server" business

HTML ui-polling.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a id="run_task" href="#">Run task</a>

        <div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

        <script type="text/javascript">

            $('#run_task').click(function(e) {
                e.preventDefault();

                <!-- not a good practice to run a long-running task this way but that's a sample -->
                $.get('task-launch.php');

                <!-- launches the polling business -->
                setTimeout(function() {
                    getProgressionInformation();
                }, 1000);

            });

            function getProgressionInformation() {
                $.get('task-follow.php', function(progress) {
                    $('#task_progress_pct').html(progress);
                    if (progress !== '--') {
                        <!-- if the task has not finished, we restart the request after a 1000ms delay -->
                        setTimeout(function() {
                            getProgressionInformation();
                        }, 1000);
                    }
                });
            }

            /* the task might be already running when the page loads */
            $(document).ready(function() {
                getProgressionInformation();
            });

        </script>

    </body>
</html>


With a minimum of JavaScript ?

I also developed a jquery plugin, domajax, intended to do "ajax without javascript" (in fact, the plugin itself is in jQuery, but using it do not require JavaScript code), and by combining options you can do polling.

In our demonstration:

  • the follower becomes:

PHP task-follow.php

<?php

require("Sync.php");

$shared = new Sync("some_file.txt");

if ($shared->progress !== null) {
    echo $shared->progress;
}
  • the UI source becomes:

HTML ui-domajax.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a
            href="#"
            id="run-task"
            class="domajax click"
            data-endpoint="task-launch.php"
            data-domajax-complete="#polling"
        >Run task</a>

        <div
            id="polling"
            data-endpoint="task-follow.php"
            data-delay="1000"
            data-output-not-empty="#task-progress-pct"
            data-domajax-not-empty=""
        ></div>

        <div id="task-progress">Progression: <span id="task-progress-pct">--</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
        <script src="//domajax.com/js/domajax/jquery.domajax.js"></script>

    </body>
</html>

As you can see, there is no visible javascript at all in this code. Clean isn't it?

Other examples are available on domajax's website, look at the "Manage progress bars easily" tab in the demo pane. All options are heavily documented for details.



回答2:

PHP is a server side language and it give you response based on request .

you can not manipulate (like you want) on DOM .

for this you should use javascript and AJAX to change DOM elements based on PHP result



回答3:

You actually do not need Javascript to accomplish this. If the task you're waiting for is persistent, you can simply create a PHP script that:

  1. Finds the task (in a DB for example)
  2. Checks the percentage complete of the task
  3. Echo's the percentage to the user
  4. If percentage < 100%
    • Refresh page
    • Otherwise, redirect to complete page.

You can do a non-javascript refresh using an HTML meta refresh. It might look something like this (haven't done PHP in like 10 years, so treat this like pseudocode):

<?php
    $taskId = $_GET("taskId");
    $task = lookupTask($taskId);
    $refresh = $task->percentComplete < 100;
?>
<html>
    <head>
        <? if($refresh): ?>
        <meta http-equiv="refresh" content="5">
        <? else: ?>
        <meta http-equiv="refresh" content="0; url=taskComplete?taskId=<?=$taskId ?>" />
        <? endif; ?>
    </head>
    <body>
        Loading: <?=$task->percentComplete ?>
    </body>
</html>

Another approach, a really ugly hack, would be to use PHP's flush() method. Flush will force the current output buffer to whatever is running PHP (CGI for example). This output will sometimes be further buffered by the host application, but on some web servers it will send the output straight to the user. Read more here: http://php.net/manual/en/function.flush.php

You might need to combine it with ob_flush, but I'm not positive: http://php.net/manual/en/function.ob-flush.php

With CSS, you could hide all 'loading' text by default, but override that for the last-child loading text and display it instead. For example:

<style type="text/css">
.loadText {
    display: none;
}
.loadText:last-child {
    display: block;
}
</style>
<?php 
for ($x = 0; $x <= 100; $x++) {
   echo '<div class="loadText">Loading: ', $x, '%</div>';
   flush();
}
?>

Both of these solutions are terrible, I just wanted to share them for completeness :)



回答4:

To elaborate a little on the answers here;

PHP is a server side language, but this is not why it won't normally work.

This has to do with the way we communicate over the HTTP protocol.
When a client sends an HTTP request to the server, the first thing that happens is they establish a connection. The server then processes the request and produces a response which is then sent back to the client.
Once the response is received, the connection dies.
So, generally, you get one response per request.

A common misconception is that using the now defaulted keep-alive header for persistent connections, you can keep pushing content as the socket remains open. HTTP doesn't actually work that way and would require a little more manipulation before you can stream your data.
But more on that here and here.

What can you do?

There are a few options I can think of.

  1. Using a client side language (typically, JavaScript), we can keep sending XMLHttpRequest objects to query the server based on each response we receive effectively creating a stream of media.
  2. Comet is a pretty broad term for allowing web servers to push data back to the client over HTTP requests. APE is one implementation of that.
  3. Have a look at Web Sockets using Ratchet.
  4. You could, in theory and if you knew the address of your client, write APIs on both sides which would accept cURLs and XHRs. In this case, both sides would act as clients and servers and could communicate on the same level. This is possible with PHP but not the best solution for browsers.

Finally, you might want to read this popular answer that goes through some of these technologies.



回答5:

More simply:

<style> div:not(:last-child) { display:none } </style>
<?php 
$x=1;
while($x<=100) {
   echo "<div>Loading: $x %</div>";
   $x++;
}   
?>

That way, the old <div>s will be hidden as the new ones appear.



回答6:

This script works with PHP 5.4 and Nginx on server-side and it's tested with Firefox on client-side. It doesn't work with Chrome.

Demo over at YouTube

Hints

  • only one request
  • the connection must be kept open
  • in order to send content in parts, the response must be chunked
  • you need set a no-buffering header for your webserver
  • php must be configured to allow immediate flushes
  • you need a trick or clever solution to write to the same line in the browser
    • here a css trick is used

<?php
header('X-Accel-Buffering: no'); // nginx related - turn the output buffer off
header('Content-Encoding: chunked;');

header('Transfer-Encoding', 'chunked');
header('Content-Type', 'text/plain');
header('Connection', 'keep-alive');

function dump_chunk($chunk)
{
  printf("%s\r\n", $chunk);
  flush();
  ob_flush(); // if you comment this out, the chunk updates are slower
}

ob_start();
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>A Chunked Response</title>
        <style> div:not(:last-child) { display:none; } </style>
    </head>
    <body>
        <?php
        // inital clear all
        ob_end_flush(); flush(); ob_flush();

        // browser padding
        echo str_pad('', 4096) . "\n"; flush(); ob_flush();

        for ($i = 0; $i < 1000; $i++) {
            usleep(15000);
            dump_chunk('<div>Sending data chunk ' . ($i + 1) . ' of 1000 <br /></div>');
        }

        usleep(10000); // this is needed for the last update
        ?>
    </body>
</html>


回答7:

I don't know if I really understand your question, but I did something usually people don't know about it. In order to give the impression that the line is being overwritten, you can insert a CR (Carriage Return, "\r") in front of your output. I wrote it down on an example:

<?php
$x=1;
while($x<=100) {
   echo "\rLoading: $x %";
   usleep(10000);
   $x++;
}
?>

Running this on a terminal I get a cool loading status: $ php code.php

Hope that's what you've been looking for.



回答8:

No, You cannot do this without the help of javascript or jquery.

You can have a php page, if you need and that will return the progress like below,

//yourprogresspage.php

    <?php 
    $x="your progress";
    echo "Loading: $x %<br>";

    ?>

then you need to call it from your page using jquery

<script>
   var repeater;
   function updateprogress() {
      $.post( "yourprogresspage.php", function( data ) {
         $( ".progressdiv" ).html( data );
      });
      repeater = setTimeout(updateprogress, 1000);
   }   
  updateprogress();
</script>


回答9:

I used a bit of CSS and HTML, This is what I came up with:

<html>
    <?php 
        $x=1;
        while($x<=100)
        {
    ?>
    <div id="count"style="position:absolute;top:10px;left:10px;">
        <style>
        #count:after 
        {
          content:'<?php echo "Loading: $x % "; ?>';      
        }
        </style>
    </div>
    <?php 
        $x++;
        sleep(1); 
        } 
    ?>
</html>


回答10:

You need to update the page using javascript.

<?php

echo '<div id="percent"></div>';

$x = 0;
while($x<=100) {
   echo '<script type="text/javascript">document.getElementById("percent").innerHTML = '.$x.';'.'</script>';
   $x++;
   sleep(1);
}   

You can remove the sleep function and do your work there.



回答11:

Actually, PHP doesn't provide such a feature.

To show a progress-bar, you might try this:

While executing the long-running file generation process, save a percentage-done value in the current user session

On the page, which shows the download form, add an iFrame which calls a progress.php script

The progress.php script needs to generate webpage, which needs to reload auto-magically

The progress.php script needs to read the current progress-value from the session and draw a progress bar graphics, which visually represents the progress.

While this is a PHP-only solution, you might well prepare another one using JS / AJAX.

The HTML auto-reload should like like this:

http://linuxfreak.com/progress.php">



回答12:

After reading everyone's comments and answers saying that this isn't possible using PHP, I thought I'd take a minute to prove them wrong :P

I don't recommend actually using this (because there's certainly better ways to achieve a loading progress indicator) but this will "un-echo" the previous line as long as you don't have zlib compression enabled in PHP:

<?php 
$x = 0;
echo "<html><body>";
echo "<div style=\"position: fixed; color: white; background-color: rgba(0,0,0,0.5); top: 0; left: 0;\" id=\"loader0\">Loading: 0%</div>";
ob_implicit_flush(true); //Forces all output to immediately send
while($x < 100) {
    $x++;
    //Padding the string overcomes browser caching and TCP packet delays due to insufficiently sized packets
    echo str_pad("<div style=\"position: fixed; color: white; background-color: rgba(0,0,0,0.5); top: 0; left: 0;\" id=\"loader{$x}\">Loading: {$x}%<script>var prev = document.getElementById('loader".($x-1)."'); prev.parentNode.removeChild(prev)</script></div>", 4096, " ");
    sleep(1); //Just for demo
}
?>


回答13:

I can't see other way then sending multiple requests to the same script and saving the progress percentage in session. This could be a pure PHP and HTML solution.

<?php
session_start();
if(isset($_SESSION['progress']))
    $progress = $_SESSION['progress'] = $_SESSION['progress'] + 10;
else
    $progress = $_SESSION['progress'] = 10;
if($progress < 100){
    echo '<head><meta http-equiv="refresh" content="1; 
            url=http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].'" /></head>
        <body>'.$progress.'%</body>';
} else {
    echo 'Done';
}

What do you think ?



回答14:

You could use ob_flush(); and flush();

This will work but be aware that you should specify document Charset

<meta charset="utf-8" />

and then in the beginning of php add ob_start(); in the loop copy both ob_flush(); flush();

but this will print all 1 to 100 you should set php to print

<script>
   document.getElementById('that div').innerHTML = "Loading [THE PERCENT]%";
</script>

This is my own way.



回答15:

So much for such a simple question. Really what you are asking is how to stream output from the server, If I got the question correct, call this function.

function streamOutput(){
    // Turn off output buffering
    ini_set('output_buffering', 'off');
    // Turn off PHP output compression
    ini_set('zlib.output_compression', false);

    //Flush (send) the output buffer and turn off output buffering
    //ob_end_flush();
     while (@ob_end_flush());

    // Implicitly flush the buffer(s)
    ini_set('implicit_flush', true);
    ob_implicit_flush(true);

    //prevent apache from buffering it for deflate/gzip.
    for($i = 0; $i < 1000; $i++){
        echo ' ';
    }

    flush();
}

Then in your loop do something like this.

$sleep = 100;
for( $i = 0; $i < $sleep; $i++){
    echo str_repeat('.', $i+1)." ".$i."%\n";
    flush();
    sleep(1);
}

The important part here is to call flush whenever you want to generate output. As for changing the DOM, ever hear of an IFRAME, just run it in an iframe and its all good. Iframes are the wormholes of the interwebs.



回答16:

this works fine for me

if(!session_id())
    session_start();        

if($_SESSION['loding']<=100){
   echo "Loading:".$_SESSION['loding']++ ."%<br>";
    ?>
     <div style="width:<?echo $_SESSION['loding'];?>px; height:10px; background-color:green;"></div>
   <? //the above div content displays a green loader type effect.
    header( "refresh:.5; url=loaderinphp.php" );
   }
else{
     //redirect to another page or code here
}//unset the session $_SESSION['loding'] while calling again for another page load

Screenshot:

the thing about php is that it gets fully loaded and displayed as html content.

(i.e)

echo "1";
usleep(5000000);
echo "2";

here both the echo statements are displayed at the same time but the loading time of the page gets extended as you increase the time in usleep function.

so we have to make a server request every time we increment the value in the loading content.

as u already know that this could be done with javascript,jquery,ajax i have tried this using session.

hope this comes handy...



回答17:

Not possible only with PHP, but it can be achieved using PHP + JS, try following example,

<?php
ob_start();
ob_implicit_flush();
ob_end_flush();
?>
<html>
    <head>
        <title>Loader Test</title>
        <script type="text/javascript">
            function updateLoader(val) {
                document.getElementById('loader').innerHTML = "Loading:" + val + "%";
            }
        </script>
    </head>
    <body>
        <div id="loader"></div>
        <?php flush();ob_flush(); ?>
        <?php
            for( $i = 1 ; $i <= 100 ; $i++ )
            {
                echo '<script type="text/javascript">updateLoader('.($i).')</script>';
                flush();
                ob_flush();
                usleep(50000);
            }
        ?>
        <?php ob_end_flush(); ?>
        <p>Completed</p>
    </body>
</html>

Note: this will not work if zlib.output_compression is enabled or some antivirus software holds the buffer until the page has finished loaded before sending it to the browser.



回答18:

Lets say the page has some big data to load so you want a loader.

You can make the user think there is a loader.

<?php
    // Loader last id
    $loader_id = 0;

    // Initiate loader
    function initLoader(){
        echo '<style>.php_loader{width:100px;height:50px;line-height:50px;margin-top:-25px;margin-left:-50px;position:fixed;top:50%;left:50%;}</style>';
        // Load first loader
        echo '<div id="php_loader_'.$GLOBALS['loader_id'].'" class="php_loader">0%</div>';
    }

    // Upadate loader
    function updateLoader($percent){
        // Remove last loader
        clearLoader();

        // Insert new loader
        $GLOBALS['loader_id']++;
        echo '<div id="php_loader_'.$GLOBALS['loader_id'].'" class="php_loader">'.$percent.'%</div>';

        ob_flush();
        flush();
    }

    function clearLoader(){
        // Remove last loader
        echo '<style>#php_loader_'.$GLOBALS['loader_id'].'{display:none;}</style>';
    }

?>
<html>
    <head>
        <title>Server Side Loader</title>
    </head>
    <body style="padding:30px; font-size:30px;font-family: Courier;">
        <?php
            // On body start we init loader
            initLoader();
            sleep(1);
        ?>

        25% off the page just loaded</br>

        <?php
            updateLoader(25);
            sleep(1);
        ?>

        30% off the page just loaded</br>

        <?php
            updateLoader(30);
            sleep(1);
        ?>

        50% off the page just loaded</br>

        <?php
            updateLoader(50);
            sleep(1);
        ?>

        80% off the page just loaded</br>

        <?php
            updateLoader(80);
            sleep(1);
        ?>

        100% off the page just loaded</br>

        <?php
            updateLoader(100);
            sleep(1);

            // Clear loader before body
            clearLoader();
        ?>
    </body>
</html>

Nice and smooth.



回答19:

It really depends on how you do the loading but what you could do if you really want to use AJAX js to keep an opened connection and on each update from the processing on the back-end to update the front end using JS that updated the correct display widget/field in the AJAX callback.

If you just want to do it with PHP you could easily do the following:

<?php 
$x=1;
while($x<=100) {
   echo "Loading: $x %<br>";
   $x++;
   <CALL THE FUNCTION THAT DOES THE ACTUAL PROCESSING HERE>
   ob_end_clean();
}  


回答20:

Just to add to this question, you could use output buffering if you really wanted to achieve this (this is not advised though). The only limitation is that you cannot overwrite previous output. This is more like a progress bar:

<?php

while(@ob_get_clean());
echo "Loading: ";
for($i=0; $i<10; $i++) {
    echo str_pad("+", 4096);          
    usleep(200000); 
}
echo "Done.";

?>

See Display output in parts in PHP.



回答21:

Solution, not the best for developer, but it'll work for what you want. You're script is ok. But if you see all the result at all, and you don't see progression, it's because there is a buffer mechanism. Maybe php, maybe web-server (apache...) or even maybe browser buffer.

Take a look to the flush() php function. In order to test, you need to user a "command line browser" (maybe telnet / GET manual request). If command line show you the content's coming line after line, then the problem is with your web browser.

But first, you need to identify WHERE is the current buffer mechanism: php output? (see flush php function), web server compression ? (see gzip compression) maybe browser buffering ? (chrome's waiting the end of file before showing the results)