1

I have the following Angular site which shows that Angular can update the page while PHP executes some process via AJAX in the background.

However, how can I now get my PHP process, e.g. the for loop to pass back the value of $x so that I can display the follow of $x as it increments?

index.php

<!doctype html>
<html lang="en">
    <head>
      <meta charset="utf-8">
      <title>ajaxtest</title>
      <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js"></script>
    </head>
    <body ng-app="mainApp">
        <div ng-controller="mainController">
            <div>Angular is counting: {{counter}}</div>
            <button ng-click="processFiles()">processFiles</button>
            <div>{{message}}</div>
        </div>

        <script>
        var mainApp = angular.module('mainApp',[]);
        function mainController($scope, $interval, $http) {

            var theTimer = $interval(function () {
                $scope.counter++;
            }, 1000);           

            $scope.counter = 0;
            $scope.message = 'click the button to have PHP do something in the background while Angular should continue to count';

            $scope.processFiles = function() {
                $scope.message = 'processing...';
                $http.get('http://localhost/webs/ajaxtest/data.php', {
                    params: {
                        taskIdCode: 'getLoadingStatus'
                    }
                }).success(function (data) {
                    $scope.message = data['status'];
                }); 
            }
        }
        </script>
    </body>
</html>

data.php

        $taskIdCode = filter_input(INPUT_GET, 'taskIdCode', FILTER_UNSAFE_RAW);
        $methodName = 'task_' . $taskIdCode;
        $this->$methodName();
    }
    public function task_getLoadingStatus() {
        for($x = 1; $x <= 4; $x++) {
            sleep(1);
        }
        $data['status'] = 'finished';
        echo json_encode($data);
    }
}
$taskRunner = new TaskRunner();
symcbean
  • 47,736
  • 6
  • 59
  • 94
Edward Tanguay
  • 189,012
  • 314
  • 712
  • 1,047

2 Answers2

3

As Ricardo says, one option would be to feed back a status message via the same channel that the processing is invoked - this is technically possible over the AJAX request, but entails a lot of tinkering with the webserver and any intermediate components to flush the content back to Javascript, and complexity in the javascript to handle a partial response. This is the problem websockets are intended to address. Its not good practice to handle a long running process via an HTTP request.

A further consideration to this approach is that HTTP and its infrastructure is designed around the concept of a server responding promptly to a request.

If you don't go down that route, then you need to make the data available via a different channel - the obvious solution being an AJAX webservice which will respond promptly with the current status.

But since this is dissociated from the invocation, it needs a mechanism for referencing the specific process which is being monitored. If you already have a session, then you could use the session id or the session itself as a key - however this isn't going to work if the session is associated with more than one such process. Further, exposing the session id via javascript has big implications for security.

Alternatively you could create a (hopefully unique) key in javascript and inject the key into the process invocation.

            var taskKeyValue=generate_unique_id();
            var keepChecking;

            // first invoke processing.....

            $scope.processFiles = function() {
            $scope.message = 'processing...';
            $http.get('http://localhost/webs/ajaxtest/data.php', {
                params: {
                    taskKey: taskKeyValue
                }
            }).success(function (data) {
                $scope.message = data['status'];
                clearInterval(keepChecking);
            }); 

            // then while this is outstanding
            var checkFiles = function() {
            $http.get('http://localhost/webs/ajaxtest/pollStatus.php', {
                params: {
                    taskKey: taskKeyValue
                }
            }).success(function (data) {
                $scope.message = data['status'];
            }); 
            keepChecking=set_interval(checkFiles, 2000);

Then within your existing php code....

 function doing_slow_thing() {
    for($x = 1; $x <= 40; $x++) {
        file_put_contents('/tmp/' . basename($_GET['taskKey']), $x);
        sleep(1);
    }
    $data['status'] = 'finished';
    echo json_encode($data);
 }

and in pollStatus.php:

<?php
    print json_encode(
          array('status'
             =>file_get_contents('/tmp/' . basename($_GET['taskKey']))
          ));
Community
  • 1
  • 1
symcbean
  • 47,736
  • 6
  • 59
  • 94
  • Actually my answer had the same goal as yours. The polling function would do an `$http.get` to another script that would provide the current status. The intention was not to push partial responses or do something out of the ordinary. I will review my answer later to see where I can improve :) Also, as an improvement to your own answer, you should make use of AngularJS's `$interval` service. Although it's mostly a wrapper for `setInterval` it's better suited for use with AngularJS. – Ricardo Velhote Oct 29 '15 at 14:43
  • 1
    @Piskvor: Even with the default (blocking) session handler you can close (and therefore unlock) the session at any time in your code. See session_write_close() – symcbean Oct 29 '15 at 16:41
0

If you're not using Websockets (or some type of server push thingy) you will have to resort to long-polling.

My suggestion is the following in your controller:

var promise = null;
var poller = function() {
    console.log("Query the server for the amount of progress done using another $http.get");
};

In the function before you perform the $http.get to initiate the long operation:

$scope.processFiles = function() {
  $scope.message = 'processing...';

  //
  // Initialize the interval pooling operation
  //
  var promise = $interval(poller, 1000);

  $http.get('http://localhost/webs/ajaxtest/data.php', {
    params: { 
      taskIdCode: 'getLoadingStatus'
    }
  }).success(function (data) {
    //
    // Terminate the interval so it does not run forever!
    //
    $interval.cancel(promise);
    $scope.message = data['status'];
  }).error(function() {
    $interval.cancel(promise);
  }); 
}

Then, server-side, you will have to store the progress in a database (or whatever equivalent thing you may have) to be checked by the polling operation. Each progress step, after the sleep, you will have to update this status.

Hope this information can get you get started :)

Community
  • 1
  • 1
Ricardo Velhote
  • 4,630
  • 1
  • 24
  • 30