BatchController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
12 
18 
19 /**
20  * BatchController is used to process complex, longer running actions, that need
21  * to be divided into several requests to overcome resource limits and provide
22  * progress information to the user.
23  *
24  * Conceptionally the process is divided into subactions (_work packages_),
25  * that are called sequentially. Depending on the progress, the controller sets
26  * different actions on the response as result of one execution.
27  *
28  * BatchController only sets up the infrastructure, the concrete process is defined
29  * by creating a subclass and implementing the abstract methods (mainly
30  * BatchController::getWorkPackage()).
31  *
32  * The controller supports the following actions:
33  *
34  * <div class="controller-action">
35  * <div> __Action__ _default_ </div>
36  * <div>
37  * Initialize the work packages and process the first action.
38  * | Parameter | Description
39  * |-----------------------|-------------------------
40  * | _in_ `oneCall` | Boolean whether to accomplish the task in one call (optional, default: _false_)
41  * | _out_ `stepNumber` | The current step starting with 1, ending with _numberOfSteps_+1
42  * | _out_ `numberOfSteps` | Total number of steps
43  * | _out_ `displayText` | The display text for the current step
44  * | __Response Actions__ | |
45  * | `next` | The process is not finished and `continue` should be called as next action
46  * | `download` | The process is finished and the next call to `continue` will trigger the file download
47  * | `done` | The process is finished
48  * </div>
49  * </div>
50  *
51  * <div class="controller-action">
52  * <div> __Action__ continue </div>
53  * <div>
54  * Continue to process the next action.
55  * | Parameter | Description
56  * |-----------------------|-------------------------
57  * | _out_ `stepNumber` | The current step starting with 1, ending with _numberOfSteps_+1
58  * | _out_ `numberOfSteps` | Total number of steps
59  * | _out_ `displayText` | The display text for the current step
60  * | __Response Actions__ | |
61  * | `next` | The process is not finished and `continue` should be called as next action
62  * | `download` | The process is finished and the next call to `continue` will trigger the file download
63  * | `done` | The process is finished
64  * </div>
65  * </div>
66  *
67  * @author ingo herwig <ingo@wemove.com>
68  */
69 abstract class BatchController extends Controller {
70 
71  // session name constants
72  const ONE_CALL_SESSION_VARNAME = 'BatchController.oneCall';
73  const STEP_SESSION_VARNAME = 'BatchController.curStep';
74  const NUM_STEPS_VARNAME = 'BatchController.numSteps';
75  const DOWNLOAD_STEP = 'BatchController.downloadStep'; // signals that the next continue action triggers the download
76  const WORK_PACKAGES_VARNAME = 'BatchController.workPackages';
77 
78  private $_curStep = 1;
79  private $_workPackages = array();
80 
81  /**
82  * @see Controller::initialize()
83  */
84  public function initialize(Request $request, Response $response) {
85  parent::initialize($request, $response);
86 
87  $session = $this->getSession();
88  if ($request->getAction() == 'continue') {
89  // get step for current call from session
90  if ($session->exist(self::STEP_SESSION_VARNAME)) {
91  $this->_curStep = $session->get(self::STEP_SESSION_VARNAME);
92  }
93  else {
94  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Current step undefined."));
95  }
96  // get workpackage definition for current call from session
97  if ($session->exist(self::WORK_PACKAGES_VARNAME)) {
98  $this->_workPackages = $session->get(self::WORK_PACKAGES_VARNAME);
99  }
100  else {
101  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Work packages undefined."));
102  }
103  }
104  else {
105  // first call, initialize step session variable
106  $this->_curStep = 1;
107  $session->set(self::ONE_CALL_SESSION_VARNAME, $request->getBooleanValue('oneCall', false));
108 
109  $tmpArray = array();
110  $session->set(self::WORK_PACKAGES_VARNAME, $tmpArray);
111  $session->set(self::DOWNLOAD_STEP, false);
112 
113  // define work packages
114  $number = 0;
115  while (($workPackage = $this->getWorkPackage($number)) !== null) {
116  if (!isset($workPackage['name']) || !isset($workPackage['size']) ||
117  !isset($workPackage['oids']) || !isset($workPackage['callback'])) {
118  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Incomplete work package description."));
119  }
120  else {
121  $args = isset($workPackage['args']) ? $workPackage['args'] : null;
122  $this->addWorkPackage($workPackage['name'], $workPackage['size'], $workPackage['oids'], $workPackage['callback'], $args);
123  $number++;
124  }
125  }
126  if ($number == 0) {
127  throw new ApplicationException($request, $response, ApplicationError::getGeneral("No work packages."));
128  }
129  }
130  $nextStep = $this->_curStep+1;
131  $session->set(self::STEP_SESSION_VARNAME, $nextStep);
132  }
133 
134  /**
135  * @see Controller::doExecute()
136  */
137  protected function doExecute() {
138  $session = $this->getSession();
139  $response = $this->getResponse();
140 
141  // check if a download was triggered in the last step
142  if ($session->get(self::DOWNLOAD_STEP) == true) {
143  $file = $this->getDownloadFile();
144  $response->setFile($file);
145  return;
146  }
147 
148  // continue processing
149  $curStep = $this->getStepNumber();
150  $numberOfSteps = $this->getNumberOfSteps();
151  if ($curStep <= $numberOfSteps) {
152  $this->processPart();
153 
154  $response->setValue('stepNumber', $curStep);
155  $response->setValue('numberOfSteps', $numberOfSteps);
156  $response->setValue('displayText', $this->getDisplayText($curStep));
157  }
158 
159  // check if we are finished or should continue
160  // (number of packages may have changed while processing)
161  $numberOfSteps = $this->getNumberOfSteps();
162  if ($curStep >= $numberOfSteps || $session->get(self::ONE_CALL_SESSION_VARNAME) == true) {
163  // finished -> check for download
164  $file = $this->getDownloadFile();
165  if ($file) {
166  $response->setAction('download');
167  $session->set(self::DOWNLOAD_STEP, true);
168  }
169  else {
170  $response->setAction('done');
171  }
172  }
173  else {
174  // proceed
175  $response->setAction('next');
176  }
177  }
178 
179  /**
180  * Get the number of the current step (1..number of steps).
181  * @return The number of the current step
182  */
183  protected function getStepNumber() {
184  // since we actally call processPart() in the second step,
185  // return the real step number reduced by one
186  return $this->_curStep;
187  }
188 
189  /**
190  * Add a work package to session. This package will be devided into sub packages of given size.
191  * @param $name Display name of the package (will be supplemented by startNumber-endNumber, e.g. '1-7', '8-14', ...)
192  * @param $size Size of one sub package. This defines how many of the oids will be passed to the callback in one call (e.g. '7' means pass 7 oids per call)
193  * @param $oids An array of object ids (or other application specific package identifiers) with _at least one value_ that will be distributed into sub packages of given size
194  * @param $callback The name of method to call for this package type.
195  * The callback method must accept the following parameters:
196  * 1. array parameter (the object ids to process in the current call)
197  * 2. optionally array parameter (the additional arguments)
198  * @param $args Assoziative array of additional callback arguments (application specific) (default: _null_)
199  */
200  protected function addWorkPackage($name, $size, $oids, $callback, $args=null) {
201  $request = $this->getRequest();
202  $response = $this->getResponse();
203  if ($size < 1) {
204  throw new ApplicationException($request, $response,
205  ApplicationError::getGeneral("Wrong work package description '".$name."': Size must be at least 1."));
206  }
207  if (sizeOf($oids) == 0) {
208  throw new ApplicationException($request, $response,
209  ApplicationError::getGeneral("Wrong work package description '".$name."': No oids given."));
210  }
211  if (strlen($callback) == 0) {
212  throw new ApplicationException($request, $response,
213  ApplicationError::getGeneral("Wrong work package description '".$name."': No callback given."));
214  }
215 
216  $session = $this->getSession();
217  $workPackages = $session->get(self::WORK_PACKAGES_VARNAME);
218 
219  $counter = 1;
220  $total = sizeOf($oids);
221  while(sizeOf($oids) > 0) {
222  $items = array();
223  for($i=0; $i<$size; $i++) {
224  $nextItem = array_shift($oids);
225  if($nextItem !== null) {
226  $items[] = $nextItem;
227  }
228  }
229 
230  // define status text
231  $start = $counter;
232  $end = ($counter+sizeOf($items)-1);
233  $stepsText = $counter;
234  if ($start != $end) {
235  $stepsText .= '-'.($counter+sizeOf($items)-1);
236  }
237  $statusText = "";
238  if ($total > 1) {
239  $statusText = $stepsText.'/'.$total;
240  }
241 
242  $curWorkPackage = array('name' => $name.' '.$statusText,
243  'oids' => $items,
244  'callback' => $callback,
245  'args' => $args);
246  $workPackages[] = $curWorkPackage;
247  $counter += $size;
248  }
249  $session->set(self::WORK_PACKAGES_VARNAME, $workPackages);
250  $session->set(self::NUM_STEPS_VARNAME, sizeOf($workPackages));
251 
252  $this->_workPackages = $workPackages;
253  }
254 
255  /**
256  * Process the next step.
257  */
258  protected function processPart() {
259  $curWorkPackageDef = $this->_workPackages[$this->getStepNumber()-1];
260  if (strlen($curWorkPackageDef['callback']) == 0) {
261  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Empty callback name."));
262  }
263  else {
264  if (!method_exists($this, $curWorkPackageDef['callback'])) {
265  throw new ApplicationException($request, $response,
266  ApplicationError::getGeneral("Method '".$curWorkPackageDef['callback']."' must be implemented by ".get_class($this)));
267  }
268  else {
269  call_user_func(array($this, $curWorkPackageDef['callback']), $curWorkPackageDef['oids'], $curWorkPackageDef['args']);
270  }
271  }
272  }
273 
274  /**
275  * Get the number of steps to process.
276  * @return Integer
277  */
278  protected function getNumberOfSteps() {
279  return $this->getSession()->get(self::NUM_STEPS_VARNAME);
280  }
281 
282  /**
283  * Get the text to display for the current step.
284  * @param $step The step number
285  */
286  protected function getDisplayText($step) {
287  return $this->getMessage()->getText("Processing")." ".$this->_workPackages[$step-1]['name']." ...";
288  }
289 
290  /**
291  * Get the filename of the file to download at the end of processing.
292  * @return String of null, if no download is created.
293  */
294  protected function getDownloadFile() {
295  return null;
296  }
297 
298  /**
299  * Get definitions of work packages.
300  * @param $number The number of the work package (first number is 0, number is incremented on every call)
301  * @note This function gets called on first initialization run as often until it returns null.
302  * This allows to define different static work packages. If you would like to add work packages dynamically on
303  * subsequent runs this may be done by directly calling the BatchController::addWorkPackage() method.
304  * @return A work packages description as assoziative array with keys 'name', 'size', 'oids', 'callback'
305  * as required for BatchController::addWorkPackage() method or null to terminate.
306  */
307  protected abstract function getWorkPackage($number);
308 }
309 ?>
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
getRequest()
Get the Request instance.
Definition: Controller.php:190
getMessage()
Get the Message instance.
Definition: Controller.php:254
static getGeneral($message)
Factory method for creating a general error instance.
Controller is the base class of all controllers.
Definition: Controller.php:48
getDisplayText($step)
Get the text to display for the current step.
addWorkPackage($name, $size, $oids, $callback, $args=null)
Add a work package to session.
getStepNumber()
Get the number of the current step (1..number of steps).
getBooleanValue($name, $default=false)
Get a value as boolean.
getAction()
Get the name of the action.
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:20
BatchController is used to process complex, longer running actions, that need to be divided into seve...
getWorkPackage($number)
Get definitions of work packages.
initialize(Request $request, Response $response)
getDownloadFile()
Get the filename of the file to download at the end of processing.
Application controllers.
Definition: namespaces.php:3
ApplicationException signals a general application exception.
getNumberOfSteps()
Get the number of steps to process.
getResponse()
Get the Response instance.
Definition: Controller.php:198
getSession()
Get the Session instance.
Definition: Controller.php:214