Completed
Push — master ( d3a073...5737c8 )
by Greg
02:21
created

src/Collection/CollectionBuilder.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 View Code Duplication
    public static function create($container, $commandFile)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
}
572