Search code examples
javascriptphploopsloadingpolling

How to display loading percentage and how to do it without javascript?


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


Solution

  • 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 (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:

    a beautiful ui

    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

    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

    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>
    

    it works!


    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.