607435a48fc0f2a1510f557e48d5960cf0f91bab
[yaffs-website] / vendor / consolidation / robo / src / Collection / Collection.php
1 <?php
2 namespace Robo\Collection;
3
4 use Robo\Result;
5 use Robo\State\Data;
6 use Psr\Log\LogLevel;
7 use Robo\Contract\TaskInterface;
8 use Robo\Task\StackBasedTask;
9 use Robo\Task\BaseTask;
10 use Robo\TaskInfo;
11 use Robo\Contract\WrappedTaskInterface;
12 use Robo\Exception\TaskException;
13 use Robo\Exception\TaskExitException;
14 use Robo\Contract\CommandInterface;
15
16 use Robo\Contract\InflectionInterface;
17 use Robo\State\StateAwareInterface;
18 use Robo\State\StateAwareTrait;
19
20 /**
21  * Group tasks into a collection that run together. Supports
22  * rollback operations for handling error conditions.
23  *
24  * This is an internal class. Clients should use a CollectionBuilder
25  * rather than direct use of the Collection class.  @see CollectionBuilder.
26  *
27  * Below, the example FilesystemStack task is added to a collection,
28  * and associated with a rollback task.  If any of the operations in
29  * the FilesystemStack, or if any of the other tasks also added to
30  * the task collection should fail, then the rollback function is
31  * called. Here, taskDeleteDir is used to remove partial results
32  * of an unfinished task.
33  */
34 class Collection extends BaseTask implements CollectionInterface, CommandInterface, StateAwareInterface
35 {
36     use StateAwareTrait;
37
38     /**
39      * @var \Robo\Collection\Element[]
40      */
41     protected $taskList = [];
42
43     /**
44      * @var TaskInterface[]
45      */
46     protected $rollbackStack = [];
47
48     /**
49      * @var TaskInterface[]
50      */
51     protected $completionStack = [];
52
53     /**
54      * @var CollectionInterface
55      */
56     protected $parentCollection;
57
58     /**
59      * @var callable[]
60      */
61     protected $deferredCallbacks = [];
62
63     /**
64      * @var string[]
65      */
66     protected $messageStoreKeys = [];
67
68     /**
69      * Constructor.
70      */
71     public function __construct()
72     {
73         $this->resetState();
74     }
75
76     public function setProgressBarAutoDisplayInterval($interval)
77     {
78         if (!$this->progressIndicator) {
79             return;
80         }
81         return $this->progressIndicator->setProgressBarAutoDisplayInterval($interval);
82     }
83
84     /**
85      * {@inheritdoc}
86      */
87     public function add(TaskInterface $task, $name = self::UNNAMEDTASK)
88     {
89         $task = new CompletionWrapper($this, $task);
90         $this->addToTaskList($name, $task);
91         return $this;
92     }
93
94     /**
95      * {@inheritdoc}
96      */
97     public function addCode(callable $code, $name = self::UNNAMEDTASK)
98     {
99         return $this->add(new CallableTask($code, $this), $name);
100     }
101
102     /**
103      * {@inheritdoc}
104      */
105     public function addIterable($iterable, callable $code)
106     {
107         $callbackTask = (new IterationTask($iterable, $code, $this))->inflect($this);
108         return $this->add($callbackTask);
109     }
110
111     /**
112      * {@inheritdoc}
113      */
114     public function rollback(TaskInterface $rollbackTask)
115     {
116         // Rollback tasks always try as hard as they can, and never report failures.
117         $rollbackTask = $this->ignoreErrorsTaskWrapper($rollbackTask);
118         return $this->wrapAndRegisterRollback($rollbackTask);
119     }
120
121     /**
122      * {@inheritdoc}
123      */
124     public function rollbackCode(callable $rollbackCode)
125     {
126         // Rollback tasks always try as hard as they can, and never report failures.
127         $rollbackTask = $this->ignoreErrorsCodeWrapper($rollbackCode);
128         return $this->wrapAndRegisterRollback($rollbackTask);
129     }
130
131     /**
132      * {@inheritdoc}
133      */
134     public function completion(TaskInterface $completionTask)
135     {
136         $collection = $this;
137         $completionRegistrationTask = new CallableTask(
138             function () use ($collection, $completionTask) {
139
140                 $collection->registerCompletion($completionTask);
141             },
142             $this
143         );
144         $this->addToTaskList(self::UNNAMEDTASK, $completionRegistrationTask);
145         return $this;
146     }
147
148     /**
149      * {@inheritdoc}
150      */
151     public function completionCode(callable $completionTask)
152     {
153         $completionTask = new CallableTask($completionTask, $this);
154         return $this->completion($completionTask);
155     }
156
157     /**
158      * {@inheritdoc}
159      */
160     public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
161     {
162         return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
163     }
164
165     /**
166      * {@inheritdoc}
167      */
168     public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
169     {
170         return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
171     }
172
173     /**
174      * {@inheritdoc}
175      */
176     public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
177     {
178         $context += ['name' => 'Progress'];
179         $context += TaskInfo::getTaskContext($this);
180         return $this->addCode(
181             function () use ($level, $text, $context) {
182                 $context += $this->getState()->getData();
183                 $this->printTaskOutput($level, $text, $context);
184             }
185         );
186     }
187
188     /**
189      * @param \Robo\Contract\TaskInterface $rollbackTask
190      *
191      * @return $this
192      */
193     protected function wrapAndRegisterRollback(TaskInterface $rollbackTask)
194     {
195         $collection = $this;
196         $rollbackRegistrationTask = new CallableTask(
197             function () use ($collection, $rollbackTask) {
198                 $collection->registerRollback($rollbackTask);
199             },
200             $this
201         );
202         $this->addToTaskList(self::UNNAMEDTASK, $rollbackRegistrationTask);
203         return $this;
204     }
205
206     /**
207      * Add either a 'before' or 'after' function or task.
208      *
209      * @param string $method
210      * @param string $name
211      * @param callable|TaskInterface $task
212      * @param string $nameOfTaskToAdd
213      *
214      * @return $this
215      */
216     protected function addBeforeOrAfter($method, $name, $task, $nameOfTaskToAdd)
217     {
218         if (is_callable($task)) {
219             $task = new CallableTask($task, $this);
220         }
221         $existingTask = $this->namedTask($name);
222         $fn = [$existingTask, $method];
223         call_user_func($fn, $task, $nameOfTaskToAdd);
224         return $this;
225     }
226
227     /**
228      * Wrap the provided task in a wrapper that will ignore
229      * any errors or exceptions that may be produced.  This
230      * is useful, for example, in adding optional cleanup tasks
231      * at the beginning of a task collection, to remove previous
232      * results which may or may not exist.
233      *
234      * TODO: Provide some way to specify which sort of errors
235      * are ignored, so that 'file not found' may be ignored,
236      * but 'permission denied' reported?
237      *
238      * @param \Robo\Contract\TaskInterface $task
239      *
240      * @return \Robo\Collection\CallableTask
241      */
242     public function ignoreErrorsTaskWrapper(TaskInterface $task)
243     {
244         // If the task is a stack-based task, then tell it
245         // to try to run all of its operations, even if some
246         // of them fail.
247         if ($task instanceof StackBasedTask) {
248             $task->stopOnFail(false);
249         }
250         $ignoreErrorsInTask = function () use ($task) {
251             $data = [];
252             try {
253                 $result = $this->runSubtask($task);
254                 $message = $result->getMessage();
255                 $data = $result->getData();
256                 $data['exitcode'] = $result->getExitCode();
257             } catch (\Exception $e) {
258                 $message = $e->getMessage();
259             }
260
261             return Result::success($task, $message, $data);
262         };
263         // Wrap our ignore errors callable in a task.
264         return new CallableTask($ignoreErrorsInTask, $this);
265     }
266
267     /**
268      * @param callable $task
269      *
270      * @return \Robo\Collection\CallableTask
271      */
272     public function ignoreErrorsCodeWrapper(callable $task)
273     {
274         return $this->ignoreErrorsTaskWrapper(new CallableTask($task, $this));
275     }
276
277     /**
278      * Return the list of task names added to this collection.
279      *
280      * @return array
281      */
282     public function taskNames()
283     {
284         return array_keys($this->taskList);
285     }
286
287     /**
288      * Test to see if a specified task name exists.
289      * n.b. before() and after() require that the named
290      * task exist; use this function to test first, if
291      * unsure.
292      *
293      * @param string $name
294      *
295      * @return bool
296      */
297     public function hasTask($name)
298     {
299         return array_key_exists($name, $this->taskList);
300     }
301
302     /**
303      * Find an existing named task.
304      *
305      * @param string $name
306      *   The name of the task to insert before.  The named task MUST exist.
307      *
308      * @return Element
309      *   The task group for the named task. Generally this is only
310      *   used to call 'before()' and 'after()'.
311      */
312     protected function namedTask($name)
313     {
314         if (!$this->hasTask($name)) {
315             throw new \RuntimeException("Could not find task named $name");
316         }
317         return $this->taskList[$name];
318     }
319
320     /**
321      * Add a list of tasks to our task collection.
322      *
323      * @param TaskInterface[] $tasks
324      *   An array of tasks to run with rollback protection
325      *
326      * @return $this
327      */
328     public function addTaskList(array $tasks)
329     {
330         foreach ($tasks as $name => $task) {
331             $this->add($task, $name);
332         }
333         return $this;
334     }
335
336     /**
337      * Add the provided task to our task list.
338      *
339      * @param string $name
340      * @param \Robo\Contract\TaskInterface $task
341      *
342      * @return \Robo\Collection\Collection
343      */
344     protected function addToTaskList($name, TaskInterface $task)
345     {
346         // All tasks are stored in a task group so that we have a place
347         // to hang 'before' and 'after' tasks.
348         $taskGroup = new Element($task);
349         return $this->addCollectionElementToTaskList($name, $taskGroup);
350     }
351
352     /**
353      * @param int|string $name
354      * @param \Robo\Collection\Element $taskGroup
355      *
356      * @return $this
357      */
358     protected function addCollectionElementToTaskList($name, Element $taskGroup)
359     {
360         // If a task name is not provided, then we'll let php pick
361         // the array index.
362         if (Result::isUnnamed($name)) {
363             $this->taskList[] = $taskGroup;
364             return $this;
365         }
366         // If we are replacing an existing task with the
367         // same name, ensure that our new task is added to
368         // the end.
369         $this->taskList[$name] = $taskGroup;
370         return $this;
371     }
372
373     /**
374      * Set the parent collection. This is necessary so that nested
375      * collections' rollback and completion tasks can be added to the
376      * top-level collection, ensuring that the rollbacks for a collection
377      * will run if any later task fails.
378      *
379      * @param \Robo\Collection\NestedCollectionInterface $parentCollection
380      *
381      * @return $this
382      */
383     public function setParentCollection(NestedCollectionInterface $parentCollection)
384     {
385         $this->parentCollection = $parentCollection;
386         return $this;
387     }
388
389     /**
390      * Get the appropriate parent collection to use
391      *
392      * @return CollectionInterface
393      */
394     public function getParentCollection()
395     {
396         return $this->parentCollection ? $this->parentCollection : $this;
397     }
398
399     /**
400      * Register a rollback task to run if there is any failure.
401      *
402      * Clients are free to add tasks to the rollback stack as
403      * desired; however, usually it is preferable to call
404      * Collection::rollback() instead.  With that function,
405      * the rollback function will only be called if all of the
406      * tasks added before it complete successfully, AND some later
407      * task fails.
408      *
409      * One example of a good use-case for registering a callback
410      * function directly is to add a task that sends notification
411      * when a task fails.
412      *
413      * @param TaskInterface $rollbackTask
414      *   The rollback task to run on failure.
415      */
416     public function registerRollback(TaskInterface $rollbackTask)
417     {
418         if ($this->parentCollection) {
419             return $this->parentCollection->registerRollback($rollbackTask);
420         }
421         if ($rollbackTask) {
422             $this->rollbackStack[] = $rollbackTask;
423         }
424     }
425
426     /**
427      * Register a completion task to run once all other tasks finish.
428      * Completion tasks run whether or not a rollback operation was
429      * triggered. They do not trigger rollbacks if they fail.
430      *
431      * The typical use-case for a completion function is to clean up
432      * temporary objects (e.g. temporary folders).  The preferred
433      * way to do that, though, is to use Temporary::wrap().
434      *
435      * On failures, completion tasks will run after all rollback tasks.
436      * If one task collection is nested inside another task collection,
437      * then the nested collection's completion tasks will run as soon as
438      * the nested task completes; they are not deferred to the end of
439      * the containing collection's execution.
440      *
441      * @param TaskInterface $completionTask
442      *   The completion task to run at the end of all other operations.
443      */
444     public function registerCompletion(TaskInterface $completionTask)
445     {
446         if ($this->parentCollection) {
447             return $this->parentCollection->registerCompletion($completionTask);
448         }
449         if ($completionTask) {
450             // Completion tasks always try as hard as they can, and never report failures.
451             $completionTask = $this->ignoreErrorsTaskWrapper($completionTask);
452             $this->completionStack[] = $completionTask;
453         }
454     }
455
456     /**
457      * Return the count of steps in this collection
458      *
459      * @return int
460      */
461     public function progressIndicatorSteps()
462     {
463         $steps = 0;
464         foreach ($this->taskList as $name => $taskGroup) {
465             $steps += $taskGroup->progressIndicatorSteps();
466         }
467         return $steps;
468     }
469
470     /**
471      * A Collection of tasks can provide a command via `getCommand()`
472      * if it contains a single task, and that task implements CommandInterface.
473      *
474      * @return string
475      *
476      * @throws \Robo\Exception\TaskException
477      */
478     public function getCommand()
479     {
480         if (empty($this->taskList)) {
481             return '';
482         }
483
484         if (count($this->taskList) > 1) {
485             // TODO: We could potentially iterate over the items in the collection
486             // and concatenate the result of getCommand() from each one, and fail
487             // only if we encounter a command that is not a CommandInterface.
488             throw new TaskException($this, "getCommand() does not work on arbitrary collections of tasks.");
489         }
490
491         $taskElement = reset($this->taskList);
492         $task = $taskElement->getTask();
493         $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
494         if ($task instanceof CommandInterface) {
495             return $task->getCommand();
496         }
497
498         throw new TaskException($task, get_class($task) . " does not implement CommandInterface, so can't be used to provide a command");
499     }
500
501     /**
502      * Run our tasks, and roll back if necessary.
503      *
504      * @return \Robo\Result
505      */
506     public function run()
507     {
508         $result = $this->runWithoutCompletion();
509         $this->complete();
510         return $result;
511     }
512
513     /**
514      * @return \Robo\Result
515      */
516     private function runWithoutCompletion()
517     {
518         $result = Result::success($this);
519
520         if (empty($this->taskList)) {
521             return $result;
522         }
523
524         $this->startProgressIndicator();
525         if ($result->wasSuccessful()) {
526             foreach ($this->taskList as $name => $taskGroup) {
527                 $taskList = $taskGroup->getTaskList();
528                 $result = $this->runTaskList($name, $taskList, $result);
529                 if (!$result->wasSuccessful()) {
530                     $this->fail();
531                     return $result;
532                 }
533             }
534             $this->taskList = [];
535         }
536         $this->stopProgressIndicator();
537         $result['time'] = $this->getExecutionTime();
538
539         return $result;
540     }
541
542     /**
543      * Run every task in a list, but only up to the first failure.
544      * Return the failing result, or success if all tasks run.
545      *
546      * @param string $name
547      * @param TaskInterface[] $taskList
548      * @param \Robo\Result $result
549      *
550      * @return \Robo\Result
551      *
552      * @throws \Robo\Exception\TaskExitException
553      */
554     private function runTaskList($name, array $taskList, Result $result)
555     {
556         try {
557             foreach ($taskList as $taskName => $task) {
558                 $taskResult = $this->runSubtask($task);
559                 $this->advanceProgressIndicator();
560                 // If the current task returns an error code, then stop
561                 // execution and signal a rollback.
562                 if (!$taskResult->wasSuccessful()) {
563                     return $taskResult;
564                 }
565                 // We accumulate our results into a field so that tasks that
566                 // have a reference to the collection may examine and modify
567                 // the incremental results, if they wish.
568                 $key = Result::isUnnamed($taskName) ? $name : $taskName;
569                 $result->accumulate($key, $taskResult);
570                 // The result message will be the message of the last task executed.
571                 $result->setMessage($taskResult->getMessage());
572             }
573         } catch (TaskExitException $exitException) {
574             $this->fail();
575             throw $exitException;
576         } catch (\Exception $e) {
577             // Tasks typically should not throw, but if one does, we will
578             // convert it into an error and roll back.
579             return Result::fromException($task, $e, $result->getData());
580         }
581         return $result;
582     }
583
584     /**
585      * Force the rollback functions to run
586      *
587      * @return $this
588      */
589     public function fail()
590     {
591         $this->disableProgressIndicator();
592         $this->runRollbackTasks();
593         $this->complete();
594         return $this;
595     }
596
597     /**
598      * Force the completion functions to run
599      *
600      * @return $this
601      */
602     public function complete()
603     {
604         $this->detatchProgressIndicator();
605         $this->runTaskListIgnoringFailures($this->completionStack);
606         $this->reset();
607         return $this;
608     }
609
610     /**
611      * Reset this collection, removing all tasks.
612      *
613      * @return $this
614      */
615     public function reset()
616     {
617         $this->taskList = [];
618         $this->completionStack = [];
619         $this->rollbackStack = [];
620         return $this;
621     }
622
623     /**
624      * Run all of our rollback tasks.
625      *
626      * Note that Collection does not implement RollbackInterface, but
627      * it may still be used as a task inside another task collection
628      * (i.e. you can nest task collections, if desired).
629      */
630     protected function runRollbackTasks()
631     {
632         $this->runTaskListIgnoringFailures($this->rollbackStack);
633         // Erase our rollback stack once we have finished rolling
634         // everything back.  This will allow us to potentially use
635         // a command collection more than once (e.g. to retry a
636         // failed operation after doing some error recovery).
637         $this->rollbackStack = [];
638     }
639
640     /**
641      * @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
642      *
643      * @return \Robo\Result
644      */
645     protected function runSubtask($task)
646     {
647         $original = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
648         $this->setParentCollectionForTask($original, $this->getParentCollection());
649         if ($original instanceof InflectionInterface) {
650             $original->inflect($this);
651         }
652         if ($original instanceof StateAwareInterface) {
653             $original->setState($this->getState());
654         }
655         $this->doDeferredInitialization($original);
656         $taskResult = $task->run();
657         $taskResult = Result::ensureResult($task, $taskResult);
658         $this->doStateUpdates($original, $taskResult);
659         return $taskResult;
660     }
661
662     protected function doStateUpdates($task, Data $taskResult)
663     {
664         $this->updateState($taskResult);
665         $key = spl_object_hash($task);
666         if (array_key_exists($key, $this->messageStoreKeys)) {
667             $state = $this->getState();
668             list($stateKey, $sourceKey) = $this->messageStoreKeys[$key];
669             $value = empty($sourceKey) ? $taskResult->getMessage() : $taskResult[$sourceKey];
670             $state[$stateKey] = $value;
671         }
672     }
673
674     public function storeState($task, $key, $source = '')
675     {
676         $this->messageStoreKeys[spl_object_hash($task)] = [$key, $source];
677
678         return $this;
679     }
680
681     public function deferTaskConfiguration($task, $functionName, $stateKey)
682     {
683         return $this->defer(
684             $task,
685             function ($task, $state) use ($functionName, $stateKey) {
686                 $fn = [$task, $functionName];
687                 $value = $state[$stateKey];
688                 $fn($value);
689             }
690         );
691     }
692
693     /**
694      * Defer execution of a callback function until just before a task
695      * runs. Use this time to provide more settings for the task, e.g. from
696      * the collection's shared state, which is populated with the results
697      * of previous test runs.
698      */
699     public function defer($task, $callback)
700     {
701         $this->deferredCallbacks[spl_object_hash($task)][] = $callback;
702
703         return $this;
704     }
705
706     protected function doDeferredInitialization($task)
707     {
708         // If the task is a state consumer, then call its receiveState method
709         if ($task instanceof \Robo\State\Consumer) {
710             $task->receiveState($this->getState());
711         }
712
713         // Check and see if there are any deferred callbacks for this task.
714         $key = spl_object_hash($task);
715         if (!array_key_exists($key, $this->deferredCallbacks)) {
716             return;
717         }
718
719         // Call all of the deferred callbacks
720         foreach ($this->deferredCallbacks[$key] as $fn) {
721             $fn($task, $this->getState());
722         }
723     }
724
725     /**
726      * @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
727      * @param $parentCollection
728      */
729     protected function setParentCollectionForTask($task, $parentCollection)
730     {
731         if ($task instanceof NestedCollectionInterface) {
732             $task->setParentCollection($parentCollection);
733         }
734     }
735
736     /**
737      * Run all of the tasks in a provided list, ignoring failures.
738      * This is used to roll back or complete.
739      *
740      * @param TaskInterface[] $taskList
741      */
742     protected function runTaskListIgnoringFailures(array $taskList)
743     {
744         foreach ($taskList as $task) {
745             try {
746                 $this->runSubtask($task);
747             } catch (\Exception $e) {
748                 // Ignore rollback failures.
749             }
750         }
751     }
752
753     /**
754      * Give all of our tasks to the provided collection builder.
755      *
756      * @param CollectionBuilder $builder
757      */
758     public function transferTasks($builder)
759     {
760         foreach ($this->taskList as $name => $taskGroup) {
761             // TODO: We are abandoning all of our before and after tasks here.
762             // At the moment, transferTasks is only called under conditions where
763             // there will be none of these, but care should be taken if that changes.
764             $task = $taskGroup->getTask();
765             $builder->addTaskToCollection($task);
766         }
767         $this->reset();
768     }
769 }