waLongActionController

Controller for execution of long actions which may potentially violate PHP time limits

Contents...

Parent class: waController.

This controller enables you to implement long operations when there is no way to avoid violation of the PHP time limit (max_execution_time). The controller requires a browser script which must periodically send AJAX requests to the controller to register the operation completion status.

Simple example of a JavaScript snippet used for sending requests to a controller:

var url = '...'; //URL processed by a waLongActionController-based controller
var processId = undefined;

var step = function (delay) {
    //interval between requests sent to server controller
    delay = delay || 2000;
    
    var timer_id = setTimeout(function () {
        $.post(
            url,
            {processId: processId},
            function (r) {
                if (!r) {
                    step(3000);
                } else if (r && r.ready) {
                    //controller operation completed
                    //setting progress indicator value to 100%
                    $('.progressbar .progressbar-inner').css({
                        width: '100%'
                    });
                    $('.progressbar-description').text('100%');
                    
                    //obtaining report on completed operation
                    $.post(url, {processId: processId, cleanup: 1}, function (r) {
                        if (r.report) {
                            setTimeout(function () {
                                //showing report to user
                                $('.progressbar').hide();
                                $('.report').show();
                                $('.report').html(r.report);
                            }, 1000);
                        }
                    }, 'json');
                } else if (r && r.error) {
                    //in case of an error, show its message to user
                    //and terminate the operation
                    $('.errormsg').text(r.error);
                } else {
                    //if everything's fine, update progress indicator value
                    if (r && r.progress) {
                        var progress = parseFloat(r.progress.replace(/,/, '.'));
                        $('.progressbar .progressbar-inner').animate({
                            'width': progress + '%'
                        });
                        $('.progressbar-description').text(r.progress);
                        $('.progressbar-hint').text(r.hint);
                    }
                    
                    //if controller returned a warning
                    //show it to user and continue as normal
                    if (r && r.warning) {
                        $('.progressbar-description').append('<i class="icon16 exclamation"></i><p>' + r.warning + '</p>');
                    }

                    //continue to next request-sending step
                    step();
                }
            },
            'json'
        ).error(function () {
            //in case of an error when sending a POST request
            //repeat in a few seconds
            step(3000);
        });
    }, delay);
};

//initial script start
$.post(url, {}, function (r) {
    if (r && r.processId) {
        processId = r.processId;
        step();
    } else if (r && r.error) {
        $('.errormsg').text(r.error);
    } else {
        $('.errormsg').text('Server error');
    }
}, 'json').error(function () {
    $('.errormsg').text('Server error');
});

Every request from the browser script starts a new process on the server. From all such generated processes, only one (self::TYPE_RUNNER) executes actual work, either until it's completed or until the process is terminated by the server side configuration set in max_execution_time parameter. Other processes (self::TYPE_MESSENGER) only send operation completion information to browser script in return to its repeating AJAX requests.

The main operating logic of the controller must be split into small steps, each being guaranteed unable to violate the max_execution_time parameter limitation. Each such relatively short iteration of the entire execution cycle must be described in method step.

Below are described methods which must be implemented by a developer in his own controller.

Methods

  • finish

    Determines whether temporary data (files) related to operation execution can be cleared.

  • info

    Returns operation completion status to browser script.

  • init

    Initialization of values, which can be used for controller operation.

  • isDone

    Determines whether operation has been completed.

  • step

    Executopn of a single operation step.

protected function finish ($filename)

Determines whether temporary data (files) related to operation execution can be cleared.

Parameters

  • $filename

    Name of file which is guaranteed to contain correct information on the last successful operation step.

Example

protected function finish($filename)
{
    //for greater confidence, you may analyze the contents of $filename here
    
    $this->info();
    if ($this->getRequest()::post('cleanup')) {
        return true;
    }
    return false;
}

protected function info ()

Returns operation completion status to browser script.

Example

protected function info()
{
    $interval = 0;
    if (!empty($this->data['timestamp'])) {
        $interval = time() - $this->data['timestamp'];
    }
    $response = array(
        'time'      => sprintf('%d:%02d:%02d', floor($interval / 3600), floor($interval / 60) % 60, $interval % 60),
        'processId' => $this->processId,
        'progress'  => 0.0,
        'ready'     => $this->isDone(),
        'offset'    => $this->data['offset'],
        'hint'      => $this->data['hint'],
    );
    $response['progress'] = empty($this->data['products_total_count']) ? 100 : ($this->data['offset'] / $this->data['products_total_count']) * 100;
    $response['progress'] = sprintf('%0.3f%%', $response['progress']);

    if ($this->getRequest()->post('cleanup')) {
        $response['report'] = $this->report();
    }

    echo json_encode($response);
}

protected function init ()

Initialization of values, which can be used for controller operation.

Example

protected function init()
{
    $product_model = new shopProductModel();

    $this->data['products_total_count'] = $product_model->countAll();
    $this->data['offset'] = 0;
    $this->data['product_id'] = null;
    $this->data['product_count'] = 0;
    $this->data['timestamp'] = time();
    $this->data['hint'] = _wp('Starting data update...');
}

protected function isDone ()

Determines whether operation has been completed. As soon as this method returns true, execution of controller's main logic described in step method is terminated.

Example

protected function isDone()
{
    return $this->data['offset'] >= $this->data['products_total_count'];
}

protected function step ()

Executopn of a single operation step.

Example

protected function step()
{
    static $products;
    if (empty($products)) {
        $products = ...; //get new data to be applied to existing records
        $this->data['hint'] = _wp('Applying new values...');
    }

    static $product_model;
    if (empty($product_model)) {
        $product_model = new shopProductModel();
    }

    // limiting the number of records to be updated by a relatively small value
    // so that their update does not take too long and does not violate the
    // max_execution_time limitation
    $chunk_size = 10;
    $chunk = array_slice($products, $this->data['offset'], $chunk_size);
    
    foreach ($chunk as $product) {
        $product_model->updateById($product['id'], array(
            'field_name' => $product['field_name'],
        ));
        if ($this->data['product_id'] != $product['id']) {
            sleep(0.2);
            $this->data['product_id'] = $product['id'];
            $this->data['product_count'] += 1;
        }
        $this->data['offset'] += 1;
    }

    return true;
}