3e037b01e98cf5473d1c7360631c19a4460e9bea
[yaffs-website] / vendor / consolidation / robo / src / Collection / CollectionBuilder.php
1 <?php
2 namespace Robo\Collection;
3
4 use Consolidation\Config\Inject\ConfigForSetters;
5 use Robo\Config\Config;
6 use Psr\Log\LogLevel;
7 use Robo\Contract\InflectionInterface;
8 use Robo\Contract\TaskInterface;
9 use Robo\Contract\CompletionInterface;
10 use Robo\Contract\WrappedTaskInterface;
11 use Robo\Task\Simulator;
12 use ReflectionClass;
13 use Robo\Task\BaseTask;
14 use Robo\Contract\BuilderAwareInterface;
15 use Robo\Contract\CommandInterface;
16 use Robo\Contract\VerbosityThresholdInterface;
17 use Robo\State\StateAwareInterface;
18 use Robo\State\StateAwareTrait;
19 use Robo\Result;
20
21 /**
22  * Creates a collection, and adds tasks to it.  The collection builder
23  * offers a streamlined chained-initialization mechanism for easily
24  * creating task groups.  Facilities for creating working and temporary
25  * directories are also provided.
26  *
27  * ``` php
28  * <?php
29  * $result = $this->collectionBuilder()
30  *   ->taskFilesystemStack()
31  *     ->mkdir('g')
32  *     ->touch('g/g.txt')
33  *   ->rollback(
34  *     $this->taskDeleteDir('g')
35  *   )
36  *   ->taskFilesystemStack()
37  *     ->mkdir('g/h')
38  *     ->touch('g/h/h.txt')
39  *   ->taskFilesystemStack()
40  *     ->mkdir('g/h/i/c')
41  *     ->touch('g/h/i/i.txt')
42  *   ->run()
43  * ?>
44  *
45  * In the example above, the `taskDeleteDir` will be called if
46  * ```
47  */
48 class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface, StateAwareInterface
49 {
50     use StateAwareTrait;
51
52     /**
53      * @var \Robo\Tasks
54      */
55     protected $commandFile;
56
57     /**
58      * @var CollectionInterface
59      */
60     protected $collection;
61
62     /**
63      * @var TaskInterface
64      */
65     protected $currentTask;
66
67     /**
68      * @var bool
69      */
70     protected $simulated;
71
72     /**
73      * @param \Robo\Tasks $commandFile
74      */
75     public function __construct($commandFile)
76     {
77         $this->commandFile = $commandFile;
78         $this->resetState();
79     }
80
81     public static function create($container, $commandFile)
82     {
83         $builder = new self($commandFile);
84
85         $builder->setLogger($container->get('logger'));
86         $builder->setProgressIndicator($container->get('progressIndicator'));
87         $builder->setConfig($container->get('config'));
88         $builder->setOutputAdapter($container->get('outputAdapter'));
89
90         return $builder;
91     }
92
93     /**
94      * @param bool $simulated
95      *
96      * @return $this
97      */
98     public function simulated($simulated = true)
99     {
100         $this->simulated = $simulated;
101         return $this;
102     }
103
104     /**
105      * @return bool
106      */
107     public function isSimulated()
108     {
109         if (!isset($this->simulated)) {
110             $this->simulated = $this->getConfig()->get(Config::SIMULATE);
111         }
112         return $this->simulated;
113     }
114
115     /**
116      * Create a temporary directory to work in. When the collection
117      * completes or rolls back, the temporary directory will be deleted.
118      * Returns the path to the location where the directory will be
119      * created.
120      *
121      * @param string $prefix
122      * @param string $base
123      * @param bool $includeRandomPart
124      *
125      * @return string
126      */
127     public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
128     {
129         // n.b. Any task that the builder is asked to create is
130         // automatically added to the builder's collection, and
131         // wrapped in the builder object. Therefore, the result
132         // of any call to `taskFoo()` from within the builder will
133         // always be `$this`.
134         return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
135     }
136
137     /**
138      * Create a working directory to hold results. A temporary directory
139      * is first created to hold the intermediate results.  After the
140      * builder finishes, the work directory is moved into its final location;
141      * any results already in place will be moved out of the way and
142      * then deleted.
143      *
144      * @param string $finalDestination The path where the working directory
145      *   will be moved once the task collection completes.
146      *
147      * @return string
148      */
149     public function workDir($finalDestination)
150     {
151         // Creating the work dir task in this context adds it to our task collection.
152         return $this->taskWorkDir($finalDestination)->getPath();
153     }
154
155     public function addTask(TaskInterface $task)
156     {
157         $this->getCollection()->add($task);
158         return $this;
159     }
160
161   /**
162    * Add arbitrary code to execute as a task.
163    *
164    * @see \Robo\Collection\CollectionInterface::addCode
165    *
166    * @param callable $code
167    * @param int|string $name
168    * @return $this
169    */
170     public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
171     {
172         $this->getCollection()->addCode($code, $name);
173         return $this;
174     }
175
176     /**
177      * Add a list of tasks to our task collection.
178      *
179      * @param TaskInterface[] $tasks
180      *   An array of tasks to run with rollback protection
181      *
182      * @return $this
183      */
184     public function addTaskList(array $tasks)
185     {
186         $this->getCollection()->addTaskList($tasks);
187         return $this;
188     }
189
190     public function rollback(TaskInterface $task)
191     {
192         // Ensure that we have a collection if we are going to add
193         // a rollback function.
194         $this->getCollection()->rollback($task);
195         return $this;
196     }
197
198     public function rollbackCode(callable $rollbackCode)
199     {
200         $this->getCollection()->rollbackCode($rollbackCode);
201         return $this;
202     }
203
204     public function completion(TaskInterface $task)
205     {
206         $this->getCollection()->completion($task);
207         return $this;
208     }
209
210     public function completionCode(callable $completionCode)
211     {
212         $this->getCollection()->completionCode($completionCode);
213         return $this;
214     }
215
216     /**
217      * @param string $text
218      * @param array $context
219      * @param string $level
220      *
221      * @return $this
222      */
223     public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
224     {
225         $this->getCollection()->progressMessage($text, $context, $level);
226         return $this;
227     }
228
229     /**
230      * @param \Robo\Collection\NestedCollectionInterface $parentCollection
231      *
232      * @return $this
233      */
234     public function setParentCollection(NestedCollectionInterface $parentCollection)
235     {
236         $this->getCollection()->setParentCollection($parentCollection);
237         return $this;
238     }
239
240     /**
241      * Called by the factory method of each task; adds the current
242      * task to the task builder.
243      *
244      * TODO: protected
245      *
246      * @param TaskInterface $task
247      *
248      * @return $this
249      */
250     public function addTaskToCollection($task)
251     {
252         // Postpone creation of the collection until the second time
253         // we are called. At that time, $this->currentTask will already
254         // be populated.  We call 'getCollection()' so that it will
255         // create the collection and add the current task to it.
256         // Note, however, that if our only tasks implements NestedCollectionInterface,
257         // then we should force this builder to use a collection.
258         if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
259             $this->getCollection();
260         }
261         $this->currentTask = $task;
262         if ($this->collection) {
263             $this->collection->add($task);
264         }
265         return $this;
266     }
267
268     public function getState()
269     {
270         $collection = $this->getCollection();
271         return $collection->getState();
272     }
273
274     public function storeState($key, $source = '')
275     {
276         return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
277     }
278
279     public function deferTaskConfiguration($functionName, $stateKey)
280     {
281         return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
282     }
283
284     public function defer($callback)
285     {
286         return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
287     }
288
289     protected function callCollectionStateFuntion($functionName, $args)
290     {
291         $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
292
293         array_unshift($args, $currentTask);
294         $collection = $this->getCollection();
295         $fn = [$collection, $functionName];
296
297         call_user_func_array($fn, $args);
298         return $this;
299     }
300
301     public function setVerbosityThreshold($verbosityThreshold)
302     {
303         $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
304         if ($currentTask) {
305             $currentTask->setVerbosityThreshold($verbosityThreshold);
306             return $this;
307         }
308         parent::setVerbosityThreshold($verbosityThreshold);
309         return $this;
310     }
311
312
313     /**
314      * Return the current task for this collection builder.
315      * TODO: Not needed?
316      *
317      * @return \Robo\Contract\TaskInterface
318      */
319     public function getCollectionBuilderCurrentTask()
320     {
321         return $this->currentTask;
322     }
323
324     /**
325      * Create a new builder with its own task collection
326      *
327      * @return CollectionBuilder
328      */
329     public function newBuilder()
330     {
331         $collectionBuilder = new self($this->commandFile);
332         $collectionBuilder->inflect($this);
333         $collectionBuilder->simulated($this->isSimulated());
334         $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
335         $collectionBuilder->setState($this->getState());
336
337         return $collectionBuilder;
338     }
339
340     /**
341      * Calling the task builder with methods of the current
342      * task calls through to that method of the task.
343      *
344      * There is extra complexity in this function that could be
345      * simplified if we attached the 'LoadAllTasks' and custom tasks
346      * to the collection builder instead of the RoboFile.  While that
347      * change would be a better design overall, it would require that
348      * the user do a lot more work to set up and use custom tasks.
349      * We therefore take on some additional complexity here in order
350      * to allow users to maintain their tasks in their RoboFile, which
351      * is much more convenient.
352      *
353      * Calls to $this->collectionBuilder()->taskFoo() cannot be made
354      * directly because all of the task methods are protected.  These
355      * calls will therefore end up here.  If the method name begins
356      * with 'task', then it is eligible to be used with the builder.
357      *
358      * When we call getBuiltTask, below, it will use the builder attached
359      * to the commandfile to build the task. However, this is not what we
360      * want:  the task needs to be built from THIS collection builder, so that
361      * it will be affected by whatever state is active in this builder.
362      * To do this, we have two choices: 1) save and restore the builder
363      * in the commandfile, or 2) clone the commandfile and set this builder
364      * on the copy. 1) is vulnerable to failure in multithreaded environments
365      * (currently not supported), while 2) might cause confusion if there
366      * is shared state maintained in the commandfile, which is in the
367      * domain of the user.
368      *
369      * Note that even though we are setting up the commandFile to
370      * use this builder, getBuiltTask always creates a new builder
371      * (which is constructed using all of the settings from the
372      * commandFile's builder), and the new task is added to that.
373      * We therefore need to transfer the newly built task into this
374      * builder. The temporary builder is discarded.
375      *
376      * @param string $fn
377      * @param array $args
378      *
379      * @return $this|mixed
380      */
381     public function __call($fn, $args)
382     {
383         if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
384             $saveBuilder = $this->commandFile->getBuilder();
385             $this->commandFile->setBuilder($this);
386             $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
387             $this->commandFile->setBuilder($saveBuilder);
388             if (!$temporaryBuilder) {
389                 throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
390             }
391             $temporaryBuilder->getCollection()->transferTasks($this);
392             return $this;
393         }
394         if (!isset($this->currentTask)) {
395             throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
396         }
397         // If the method called is a method of the current task,
398         // then call through to the current task's setter method.
399         $result = call_user_func_array([$this->currentTask, $fn], $args);
400
401         // If something other than a setter method is called, then return its result.
402         $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
403         if (isset($result) && ($result !== $currentTask)) {
404             return $result;
405         }
406
407         return $this;
408     }
409
410     /**
411      * Construct the desired task and add it to this builder.
412      *
413      * @param string|object $name
414      * @param array $args
415      *
416      * @return \Robo\Collection\CollectionBuilder
417      */
418     public function build($name, $args)
419     {
420         $reflection = new ReflectionClass($name);
421         $task = $reflection->newInstanceArgs($args);
422         if (!$task) {
423             throw new RuntimeException("Can not construct task $name");
424         }
425         $task = $this->fixTask($task, $args);
426         $this->configureTask($name, $task);
427         return $this->addTaskToCollection($task);
428     }
429
430     /**
431      * @param InflectionInterface $task
432      * @param array $args
433      *
434      * @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
435      */
436     protected function fixTask($task, $args)
437     {
438         if ($task instanceof InflectionInterface) {
439             $task->inflect($this);
440         }
441         if ($task instanceof BuilderAwareInterface) {
442             $task->setBuilder($this);
443         }
444         if ($task instanceof VerbosityThresholdInterface) {
445             $task->setVerbosityThreshold($this->verbosityThreshold());
446         }
447
448         // Do not wrap our wrappers.
449         if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
450             return $task;
451         }
452
453         // Remember whether or not this is a task before
454         // it gets wrapped in any decorator.
455         $isTask = $task instanceof TaskInterface;
456         $isCollection = $task instanceof NestedCollectionInterface;
457
458         // If the task implements CompletionInterface, ensure
459         // that its 'complete' method is called when the application
460         // terminates -- but only if its 'run' method is called
461         // first.  If the task is added to a collection, then the
462         // task will be unwrapped via its `original` method, and
463         // it will be re-wrapped with a new completion wrapper for
464         // its new collection.
465         if ($task instanceof CompletionInterface) {
466             $task = new CompletionWrapper(Temporary::getCollection(), $task);
467         }
468
469         // If we are in simulated mode, then wrap any task in
470         // a TaskSimulator.
471         if ($isTask && !$isCollection && ($this->isSimulated())) {
472             $task = new \Robo\Task\Simulator($task, $args);
473             $task->inflect($this);
474         }
475
476         return $task;
477     }
478
479     /**
480      * Check to see if there are any setter methods defined in configuration
481      * for this task.
482      */
483     protected function configureTask($taskClass, $task)
484     {
485         $taskClass = static::configClassIdentifier($taskClass);
486         $configurationApplier = new ConfigForSetters($this->getConfig(), $taskClass, 'task.');
487         $configurationApplier->apply($task, 'settings');
488
489         // TODO: If we counted each instance of $taskClass that was called from
490         // this builder, then we could also apply configuration from
491         // "task.{$taskClass}[$N].settings"
492
493         // TODO: If the builder knew what the current command name was,
494         // then we could also search for task configuration under
495         // command-specific keys such as "command.{$commandname}.task.{$taskClass}.settings".
496     }
497
498     /**
499      * When we run the collection builder, run everything in the collection.
500      *
501      * @return \Robo\Result
502      */
503     public function run()
504     {
505         $this->startTimer();
506         $result = $this->runTasks();
507         $this->stopTimer();
508         $result['time'] = $this->getExecutionTime();
509         $result->mergeData($this->getState()->getData());
510         return $result;
511     }
512
513     /**
514      * If there is a single task, run it; if there is a collection, run
515      * all of its tasks.
516      *
517      * @return \Robo\Result
518      */
519     protected function runTasks()
520     {
521         if (!$this->collection && $this->currentTask) {
522             $result = $this->currentTask->run();
523             return Result::ensureResult($this->currentTask, $result);
524         }
525         return $this->getCollection()->run();
526     }
527
528     /**
529      * @return string
530      */
531     public function getCommand()
532     {
533         if (!$this->collection && $this->currentTask) {
534             $task = $this->currentTask;
535             $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
536             if ($task instanceof CommandInterface) {
537                 return $task->getCommand();
538             }
539         }
540
541         return $this->getCollection()->getCommand();
542     }
543
544     /**
545      * @return \Robo\Collection\Collection
546      */
547     public function original()
548     {
549         return $this->getCollection();
550     }
551
552     /**
553      * Return the collection of tasks associated with this builder.
554      *
555      * @return CollectionInterface
556      */
557     public function getCollection()
558     {
559         if (!isset($this->collection)) {
560             $this->collection = new Collection();
561             $this->collection->inflect($this);
562             $this->collection->setState($this->getState());
563             $this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
564
565             if (isset($this->currentTask)) {
566                 $this->collection->add($this->currentTask);
567             }
568         }
569         return $this->collection;
570     }
571 }