BatchController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 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 
20 
21 /**
22  * BatchController is used to process complex, longer running actions, that need
23  * to be divided into several requests to overcome resource limits and provide
24  * progress information to the user.
25  *
26  * Conceptually the process is divided into sub actions (_work packages_),
27  * that are called sequentially. Depending on the progress, the controller sets
28  * different actions on the response as result of one execution.
29  *
30  * BatchController only sets up the infrastructure, the concrete process is defined
31  * by creating a subclass and implementing the abstract methods (mainly
32  * BatchController::getWorkPackage()).
33  *
34  * The controller supports the following actions:
35  *
36  * <div class="controller-action">
37  * <div> __Action__ _default_ </div>
38  * <div>
39  * Initialize the work packages and process the first action.
40  * | Parameter | Description
41  * |-----------------------|-------------------------
42  * | _in_ `oneCall` | Boolean whether to accomplish the task in one call (optional, default: _false_)
43  * | _out_ `stepNumber` | The current step starting with 1, ending with _numberOfSteps_+1
44  * | _out_ `numberOfSteps` | Total number of steps
45  * | _out_ `displayText` | The display text for the current step
46  * | _out_ `status` | The value of the response action
47  * | __Response Actions__ | |
48  * | `progress` | The process is not finished and `continue` should be called as next action
49  * | `download` | The process is finished and the next call to `continue` will trigger the file download
50  * | `done` | The process is finished
51  * </div>
52  * </div>
53  *
54  * <div class="controller-action">
55  * <div> __Action__ continue </div>
56  * <div>
57  * Continue to process the next action.
58  * | Parameter | Description
59  * |-----------------------|-------------------------
60  * | _out_ `stepNumber` | The current step starting with 1, ending with _numberOfSteps_+1
61  * | _out_ `numberOfSteps` | Total number of steps
62  * | _out_ `displayText` | The display text for the next step (since the first request)
63  * | _out_ `status` | The value of the response action
64  * | __Response Actions__ | |
65  * | `progress` | The process is not finished and `continue` should be called as next action
66  * | `download` | The process is finished and the next call to `continue` will trigger the file download
67  * | `done` | The process is finished
68  * </div>
69  * </div>
70  *
71  * @author ingo herwig <ingo@wemove.com>
72  */
73 abstract class BatchController extends Controller {
74 
75  // session name constants
76  const REQUEST_VAR = 'request';
77  const ONE_CALL_VAR = 'oneCall';
78  const STEP_VAR = 'step';
79  const NUM_STEPS_VAR = 'numSteps';
80  const DOWNLOAD_STEP_VAR = 'downloadStep'; // signals that the next continue action triggers the download
81  const PACKAGES_VAR = 'packages';
82 
83  private $curStep = null;
84  private $workPackages = [];
85 
86  /**
87  * @see Controller::initialize()
88  */
89  public function initialize(Request $request, Response $response) {
90  parent::initialize($request, $response);
91 
92  if ($request->getAction() == 'continue') {
93  // get step for current call from session
94  $this->curStep = $this->getLocalSessionValue(self::STEP_VAR);
95  if ($this->curStep === null) {
96  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Current step undefined."));
97  }
98  // get workpackage definition for current call from session
99  $this->workPackages = $this->getLocalSessionValue(self::PACKAGES_VAR);
100  if ($this->workPackages === null) {
101  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Work packages undefined."));
102  }
103  }
104  else {
105  // initialize session variables
106  $this->setLocalSessionValue(self::ONE_CALL_VAR, $request->getBooleanValue('oneCall', false));
107  $this->setLocalSessionValue(self::REQUEST_VAR, $request->getValues());
108  $this->setLocalSessionValue(self::PACKAGES_VAR, []);
109  $this->setLocalSessionValue(self::STEP_VAR, 0);
110  $this->setLocalSessionValue(self::NUM_STEPS_VAR, 0);
111  $this->setLocalSessionValue(self::DOWNLOAD_STEP_VAR, 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 
131  // next step
132  $this->curStep = $this->getLocalSessionValue(self::STEP_VAR);
133  $this->setLocalSessionValue(self::STEP_VAR, ($this->curStep === null ? 0 : $this->curStep+1));
134  }
135 
136  /**
137  * @see Controller::doExecute()
138  */
139  protected function doExecute($method=null) {
140  $response = $this->getResponse();
141 
142  // check if a download was triggered in the last step
143  if ($this->getLocalSessionValue(self::DOWNLOAD_STEP_VAR) == true) {
144  $file = $this->getDownloadFile();
145  $response->setDocument(new FileDocument($file, true));
146  $response->setAction('done');
147  $this->cleanup();
148  }
149  else {
150  // continue processing
151  $oneStep = $this->getLocalSessionValue(self::ONE_CALL_VAR);
152 
153  $curStep = $oneStep ? 1 : $this->getStepNumber();
154  $numberOfSteps = $this->getNumberOfSteps();
155  if ($curStep <= $numberOfSteps) {
156  // step 0 only returns the process information
157  if ($curStep > 0 || $oneStep) {
158  $this->processPart($curStep);
159  }
160 
161  // update local variables after processing
162  $numberOfSteps = $this->getNumberOfSteps();
163 
164  // set response data
165  $response->setValue('stepNumber', $curStep);
166  $response->setValue('numberOfSteps', $numberOfSteps);
167  $response->setValue('displayText', $this->getDisplayText($curStep));
168  }
169 
170  // check if we are finished or should continue
171  if ($curStep >= $numberOfSteps || $oneStep) {
172  // finished -> check for download
173  $file = $this->getDownloadFile();
174  if ($file) {
175  $response->setAction('download');
176  $this->setLocalSessionValue(self::DOWNLOAD_STEP_VAR, true);
177  }
178  else {
179  $response->setAction('done');
180  $this->cleanup();
181  }
182  }
183  else {
184  // proceed
185  $response->setAction('progress');
186  }
187  }
188  $response->setValue('status', $response->getAction());
189  }
190 
191  /**
192  * Get the number of the current step (1..number of steps).
193  * @return The number of the current step
194  */
195  protected function getStepNumber() {
196  return $this->curStep;
197  }
198 
199  /**
200  * Add a work package to session. This package will be divided into sub packages of given size.
201  * @param $name Display name of the package (will be supplemented by startNumber-endNumber, e.g. '1-7', '8-14', ...)
202  * @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)
203  * @param $oids An array of object ids (or other application specific package identifiers) that will be distributed into sub packages of given size
204  * @param $callback The name of method to call for this package type.
205  * The callback method must accept the following parameters:
206  * 1. array parameter (the object ids to process in the current call)
207  * 2. optionally array parameter (the additional arguments)
208  * @param $args Associative array of additional callback arguments (application specific) (default: _null_)
209  */
210  protected function addWorkPackage($name, $size, array $oids, $callback, $args=null) {
211  $request = $this->getRequest();
212  $response = $this->getResponse();
213  if (strlen($callback) == 0) {
214  throw new ApplicationException($request, $response,
215  ApplicationError::getGeneral("Wrong work package description '".$name."': No callback given."));
216  }
217 
218  $workPackages = $this->getLocalSessionValue(self::PACKAGES_VAR);
219  $counter = 1;
220  $total = sizeof($oids);
221  while(sizeof($oids) > 0) {
222  $items = [];
223  for($i=0; $i<$size && sizeof($oids)>0; $i++) {
224  $nextItem = array_shift($oids);
225  $items[] = sprintf('%s', $nextItem);
226  }
227 
228  // define status text
229  $start = $counter;
230  $end = ($counter+sizeof($items)-1);
231  $stepsText = $counter;
232  if ($start != $end) {
233  $stepsText .= '-'.($counter+sizeof($items)-1);
234  }
235  $statusText = "";
236  if ($total > 1) {
237  $statusText = $stepsText.'/'.$total;
238  }
239 
240  $curWorkPackage = [
241  'name' => $name.' '.$statusText,
242  'oids' => $items,
243  'callback' => $callback,
244  'args' => $args
245  ];
246  $workPackages[] = $curWorkPackage;
247  $counter += $size;
248  }
249  $this->workPackages = $workPackages;
250 
251  // update session
252  $this->setLocalSessionValue(self::PACKAGES_VAR, $workPackages);
253  $this->setLocalSessionValue(self::NUM_STEPS_VAR, sizeof($workPackages));
254  }
255 
256  /**
257  * Process the given step (1-base).
258  * @param $step The step to process
259  */
260  protected function processPart($step) {
261  $curWorkPackageDef = $this->workPackages[$step-1];
262  $request = $this->getRequest();
263  $response = $this->getResponse();
264  if (strlen($curWorkPackageDef['callback']) == 0) {
265  throw new ApplicationException($request, $response, ApplicationError::getGeneral("Empty callback name."));
266  }
267  if (!method_exists($this, $curWorkPackageDef['callback'])) {
268  throw new ApplicationException($request, $response,
269  ApplicationError::getGeneral("Method '".$curWorkPackageDef['callback']."' must be implemented by ".get_class($this)));
270  }
271 
272  // unserialize oids
273  $oids = array_map(function($oidStr) {
274  $oid = ObjectId::parse($oidStr);
275  return $oid != null ? $oid : $oidStr;
276  }, $curWorkPackageDef['oids']);
277  call_user_func([$this, $curWorkPackageDef['callback']], $oids, $curWorkPackageDef['args']);
278  }
279 
280  /**
281  * Get a value from the initial request.
282  * @param $name The name of the value
283  * @return Mixed
284  */
285  protected function getRequestValue($name) {
286  $requestValues = $this->getLocalSessionValue(self::REQUEST_VAR);
287  return isset($requestValues[$name]) ? $requestValues[$name] : null;
288  }
289 
290  /**
291  * Get the number of steps to process.
292  * @return Integer
293  */
294  protected function getNumberOfSteps() {
295  return $this->getLocalSessionValue(self::NUM_STEPS_VAR);
296  }
297 
298  /**
299  * Get the text to display for the current step.
300  * @param $step The step number
301  */
302  protected function getDisplayText($step) {
303  $numPackages = sizeof($this->workPackages);
304  return ($step>=0 && $step<$numPackages) ? $this->workPackages[$step]['name']." ..." :
305  ($step>=$numPackages ? "Done" : "");
306  }
307 
308  /**
309  * Get the filename of the file to download at the end of processing.
310  * @return String of null, if no download is created.
311  */
312  protected function getDownloadFile() {
313  return null;
314  }
315 
316  /**
317  * Get definitions of work packages.
318  * @param $number The number of the work package (first number is 0, number is incremented on every call)
319  * @note This function gets called on first initialization run as often until it returns null.
320  * This allows to define different static work packages. If you would like to add work packages dynamically on
321  * subsequent runs this may be done by directly calling the BatchController::addWorkPackage() method.
322  * @return A work packages description as associative array with keys 'name', 'size', 'oids', 'callback'
323  * as required for BatchController::addWorkPackage() method or null to terminate.
324  */
325  protected abstract function getWorkPackage($number);
326 
327  /**
328  * Clean up after all tasks are finished.
329  * @note Subclasses may override this to do custom clean up, but should call the parent method.
330  */
331  protected function cleanup() {
332  $this->clearLocalSessionValues();
333  }
334 }
335 ?>
FileDocument represents a local file.
getDisplayText($step)
Get the text to display for the current step.
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:18
getValues()
Get all key value pairs.
clearLocalSessionValues()
Remove all local session values.
Definition: Controller.php:469
getLocalSessionValue($key, $default=null)
Set the value of a local session variable.
Definition: Controller.php:443
getStepNumber()
Get the number of the current step (1..number of steps).
getWorkPackage($number)
Get definitions of work packages.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
getAction()
Get the name of the action.
static getGeneral($message, $statusCode=self::DEFAULT_ERROR_STATUS)
Factory method for creating a general error instance.
setLocalSessionValue($key, $value)
Get the value of a local session variable.
Definition: Controller.php:454
ApplicationError is used to signal errors that occur while processing a request.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:135
processPart($step)
Process the given step (1-base).
getRequestValue($name)
Get a value from the initial request.
ApplicationException signals a general application exception.
getDownloadFile()
Get the filename of the file to download at the end of processing.
cleanup()
Clean up after all tasks are finished.
getRequest()
Get the Request instance.
Definition: Controller.php:251
Application controllers.
Definition: namespaces.php:3
BatchController is used to process complex, longer running actions, that need to be divided into seve...
addWorkPackage($name, $size, array $oids, $callback, $args=null)
Add a work package to session.
Controller is the base class of all controllers.
Definition: Controller.php:49
initialize(Request $request, Response $response)
getResponse()
Get the Response instance.
Definition: Controller.php:259
getBooleanValue($name, $default=false)
Get a value as boolean.
getNumberOfSteps()
Get the number of steps to process.