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