Introduction
Web applications normally execute immediately the user's requests. But some requests, like statistical reports, take long time to execute.
This article discusses how one can run a long task in background in yiiframework 1.1 using Ajax technique.
Approaches
There are different ways to run background jobs in yiiframework. Let's see some of them.
Console application
The console application is proper to run periodically tasks using operating system schedulers such as cron. One can, for example, send emails from email queue every 15 minutes or collect statistical information every night.
The console applications can access the same models of the web application reusing most of code. The console applications are appropriate for generic system jobs, but are not suitable for tasks for single users. In fact, there is no web user in console application and hence no role authorizations and row level access limitations normally applied to web users.
BackJob extension
The BackJob extension of Siquo can start actions in background and monitor their execution. It can initiate a controller's action of the Yii web application as current or as an anonymous user. Running an action as current user gives the possibility to execute the code applying usual role authorizations and row level access limitations.
Unfortunately this approach cannot pass correctly the user's session cookies if the web application uses the cookie validation. In this case there is no way to start the web application with the same user identity.
Ajax
The Ajax technique discussed in this article gives the possibility to start a controller's action as current user without leaving the web page. This can be used to run long tasks with authorizations given to the user reusing the same Yii controllers and models. One should only intercept the result of the finished task.
Example with ajaxSubmitButton
This is a fictional example of a statistical reporting. The user specifies a parameter in a web form, then invokes the long task. The task produces a file to be downloaded.
The model Example contains a method doTask() executing the long task. This method takes a parameter and yields a blob result. The action runTask in the controller ExampleController renders the web form task and invokes the Example.doTask() method when requested. The form runTask contains one text field, a submit button and an ajaxSubmitButton.
The workflow is as follows. The user calls the runTask form, specifies the parameter's value and presses one of the submit buttons. Each button is designed to call the ExampleController.actionRunTask() action passing the text parameter. The controller in turn calls the Example.doTask() method with the specified parameter. The result of the Example.doTask() method is rendered to the user.
When the user presses the usual submit button, he/she should wait for the result. The ajaxSubmitButton runs the action without leaving the web form; the user is informed that the task is started and that he/she can download the result later returning in the same form.
The result of the long task should be kept in a database table longtask in a blob column. This table keeps one record for each user/task. This example uses also the relative Longtask ActiveRecord model and the LongtaskController controller on order to save and retrieve the task's results.
To run this example, navigate to the URL example/runTask.
Table longtask
The table longtask keeps the results of the tasks. Follows the sql script for this table in MySql database:
CREATE TABLE longtask ( id int(11) NOT NULL AUTO_INCREMENT, end_time datetime DEFAULT NULL, task text, username text, filename text, mime text, cont longblob, PRIMARY KEY (id) );
Longtask model
The Longtask model corresponds to the longtask table. Follow essential parts of the model:
class Longtask extends CActiveRecord { // // Initialize attributes public function init() { if ($this->scenario <> 'search') { $this->end_time = date('Y-m-d H:i:s'); $this->task = Yii::app()->request->url; $this->username = Yii::app()->user->id; } } // Delete this task of this user public function cleanMy() { $this->deleteAll('task = :task AND username = :username', array(':task' => Yii::app()->request->url, ':username' => Yii::app()->user->id)); } // Permission to view the tasks public function defaultScope() { $u = Yii::app()->user; // user identity if ($u->id == 'admin') // admin sees every record return array(); else // others can see only their own tasks return array( 'condition' => 'username = :username', 'params' => array(':username' => $u->id) ); } }
LongtaskController controller
Follow essential parts of the controller:
class LongtaskController extends Controller { public function accessRules() { return array( array('allow', // allow all users to perform 'download' action 'actions'=>array('download'), 'users'=>array('*'), ), // other rules ); } // // Download the document result of a task $id public function actionDownload($id) { $model = $this->loadModel($id); Yii::app()->getRequest()->sendFile($model->filename, $model->cont, $model->mime); } } Example model The most important part of this model is the fictional report generation method: class Example extends CFormModel { // // Long task. Takes parameter $par, yields blob result public function doTask($par) { sleep (10); // does some long work $cont = "This is the result for $par."; return $cont; // returns the result } }
ExampleController controller
The most important part of this controller is the action to produce the statistical report:
class ExampleController extends Controller { // The action runTask public function actionRunTask() { $model = new Example; // the model to generate report $par = ''; // form parameter // Returned from the form if (isset($_POST['par'])) { $par = $_POST['par']; // parameter specified $fn = "rep_$par.txt"; // file name $mime = 'text/plain'; // file type $cont = $model->doTask($par); // generate report // Ajax button if (Yii::app()->request->isAjaxRequest) { $lt = new Longtask; $lt->cleanMy(); // delete the previous task $lt->filename = $fn; $lt->cont = $cont; $lt->mime = $mime; $lt->save(); // create new task record Yii::app()->end(); // end of Ajax initiated process } // Normal submit button: download file else Yii::app()->getRequest()->sendFile ($fn, $cont, $mime); } // Calls the form $lt = Longtask::model()->findByAttributes(array( 'task' => Yii::app()->request->url, 'username' => Yii::app()->user->id)); $this->render('runTask', array('par' => $par, 'lt' => $lt)); } }
runTask form
Follows the form to run the report. The same form contains also the link to download the result of the task ran in background:
/* @var $this ExampleController */ /* @var $par string Parameter to be specified in the form */ /* @var $lt Longtask The last task */ <h1>Example of a background task</h1> <div class="form"> <?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'runTask-form', 'enableAjaxValidation'=>false, )); <div class="row"> <?php echo CHtml::label('Report parameter', 'par'); <?php echo CHtml::textField('par', $par); </div> <div class="row buttons"> <?php echo CHtml::submitButton('Submit'); <br> <?php echo CHtml::ajaxSubmitButton( 'Submit in background', $this->createUrl("runTask"), array('type'=>'POST', 'dataType' => 'json') ); </div> <?php $this->endWidget(); </div><!-- form --> <?php // Link to download the result if ($lt) { echo "Result of " . $lt->end_time . ": "; echo CHtml::link(CHtml::encode($lt->filename), array('longtask/download', 'id' => $lt->id)); }
Variations
One can use this example to run in background different tasks for different users. The application administrator should have access to the longtask table in order to monitor the background jobs.
One can use also ajaxLink in case there are no input parameters requested from the user.
One can keep more records per task/user. In this case it is better for the user to have access to the longtask table via CGridView.
More information on the tasks can be specified in the longtask table. For example, it is possible to save the start time and the percentage of the execution. See for example the BackJob extension.
If the task yields more then one file, they can be saved in a child table of longtask.
The form can give more information on the Ajax execution. See the example.
When the task's result is downloaded, it is reasonable to automatically delete the task's record from the longtask table. If the user has downloaded/deleted from the runTask form, a javascript should immediately hide the download link.