Completed
Pull Request — master (#893)
by Greg
05:57
created

src/Collection/CollectionBuilder.php (6 issues)

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 \Robo\Collection\CollectionInterface
59
     */
60
    protected $collection;
61
62
    /**
63
     * @var \Robo\Contract\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
    /**
82
     * @param \League\Container\ContainerInterface $container
83
     * @param \Robo\Tasks $commandFile
84
     *
85
     * @return static
86
     */
87 View Code Duplication
    public static function create($container, $commandFile)
88
    {
89
        $builder = new self($commandFile);
90
91
        $builder->setLogger($container->get('logger'));
92
        $builder->setProgressIndicator($container->get('progressIndicator'));
93
        $builder->setConfig($container->get('config'));
94
        $builder->setOutputAdapter($container->get('outputAdapter'));
95
96
        return $builder;
97
    }
98
99
    /**
100
     * @param bool $simulated
101
     *
102
     * @return $this
103
     */
104
    public function simulated($simulated = true)
105
    {
106
        $this->simulated = $simulated;
107
        return $this;
108
    }
109
110
    /**
111
     * @return bool
112
     */
113
    public function isSimulated()
114
    {
115
        if (!isset($this->simulated)) {
116
            $this->simulated = $this->getConfig()->get(Config::SIMULATE);
117
        }
118
        return $this->simulated;
119
    }
120
121
    /**
122
     * Create a temporary directory to work in. When the collection
123
     * completes or rolls back, the temporary directory will be deleted.
124
     * Returns the path to the location where the directory will be
125
     * created.
126
     *
127
     * @param string $prefix
128
     * @param string $base
129
     * @param bool $includeRandomPart
130
     *
131
     * @return string
132
     */
133
    public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
134
    {
135
        // n.b. Any task that the builder is asked to create is
136
        // automatically added to the builder's collection, and
137
        // wrapped in the builder object. Therefore, the result
138
        // of any call to `taskFoo()` from within the builder will
139
        // always be `$this`.
140
        return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
141
    }
142
143
    /**
144
     * Create a working directory to hold results. A temporary directory
145
     * is first created to hold the intermediate results.  After the
146
     * builder finishes, the work directory is moved into its final location;
147
     * any results already in place will be moved out of the way and
148
     * then deleted.
149
     *
150
     * @param string $finalDestination
151
     *   The path where the working directory will be moved once the task
152
     *   collection completes.
153
     *
154
     * @return string
155
     */
156
    public function workDir($finalDestination)
157
    {
158
        // Creating the work dir task in this context adds it to our task collection.
159
        return $this->taskWorkDir($finalDestination)->getPath();
160
    }
161
162
    /**
163
     * @return $this
164
     */
165
    public function addTask(TaskInterface $task)
166
    {
167
        $this->getCollection()->add($task);
168
        return $this;
169
    }
170
171
  /**
172
   * Add arbitrary code to execute as a task.
173
   *
174
   * @see \Robo\Collection\CollectionInterface::addCode
175
   *
176
   * @param callable $code
177
   * @param int|string $name
178
   *
179
   * @return $this
180
   */
181
    public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
182
    {
183
        $this->getCollection()->addCode($code, $name);
184
        return $this;
185
    }
186
187
    /**
188
     * Add a list of tasks to our task collection.
189
     *
190
     * @param \Robo\Contract\TaskInterface[] $tasks
191
     *   An array of tasks to run with rollback protection
192
     *
193
     * @return $this
194
     */
195
    public function addTaskList(array $tasks)
196
    {
197
        $this->getCollection()->addTaskList($tasks);
198
        return $this;
199
    }
200
201
    /**
202
     * @return $this
203
     */
204
    public function rollback(TaskInterface $task)
205
    {
206
        // Ensure that we have a collection if we are going to add
207
        // a rollback function.
208
        $this->getCollection()->rollback($task);
209
        return $this;
210
    }
211
212
    /**
213
     * @return $this
214
     */
215
    public function rollbackCode(callable $rollbackCode)
216
    {
217
        $this->getCollection()->rollbackCode($rollbackCode);
218
        return $this;
219
    }
220
221
    /**
222
     * @return $this
223
     */
224
    public function completion(TaskInterface $task)
225
    {
226
        $this->getCollection()->completion($task);
227
        return $this;
228
    }
229
230
    /**
231
     * @return $this
232
     */
233
    public function completionCode(callable $completionCode)
234
    {
235
        $this->getCollection()->completionCode($completionCode);
236
        return $this;
237
    }
238
239
    /**
240
     * @param string $text
241
     * @param array $context
242
     * @param string $level
243
     *
244
     * @return $this
245
     */
246
    public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
247
    {
248
        $this->getCollection()->progressMessage($text, $context, $level);
249
        return $this;
250
    }
251
252
    /**
253
     * @return $this
254
     */
255
    public function setParentCollection(NestedCollectionInterface $parentCollection)
256
    {
257
        $this->getCollection()->setParentCollection($parentCollection);
258
        return $this;
259
    }
260
261
    /**
262
     * Called by the factory method of each task; adds the current
263
     * task to the task builder.
264
     *
265
     * TODO: protected
266
     *
267
     * @param \Robo\Contract\TaskInterface $task
268
     *
269
     * @return $this
270
     */
271
    public function addTaskToCollection($task)
272
    {
273
        // Postpone creation of the collection until the second time
274
        // we are called. At that time, $this->currentTask will already
275
        // be populated.  We call 'getCollection()' so that it will
276
        // create the collection and add the current task to it.
277
        // Note, however, that if our only tasks implements NestedCollectionInterface,
278
        // then we should force this builder to use a collection.
279
        if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
280
            $this->getCollection();
281
        }
282
        $this->currentTask = $task;
283
        if ($this->collection) {
284
            $this->collection->add($task);
285
        }
286
        return $this;
287
    }
288
289
    /**
290
     * @return \Robo\State\Data
291
     */
292
    public function getState()
293
    {
294
        $collection = $this->getCollection();
295
        return $collection->getState();
296
    }
297
298
    /**
299
     * @param int|string $key
300
     * @param mixed $source
301
     *
302
     * @return $this
303
     */
304
    public function storeState($key, $source = '')
0 ignored issues
show
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $source is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
305
    {
306
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
307
    }
308
309
    /**
310
     * @param string $functionName
311
     * @param int|string $stateKey
312
     *
313
     * @return $this
314
     */
315
    public function deferTaskConfiguration($functionName, $stateKey)
316
    {
317
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
318
    }
319
320
    /**
321
     * @param callable$callback
322
     *
323
     * @return $this
324
     */
325
    public function defer($callback)
0 ignored issues
show
The parameter $callback is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
326
    {
327
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
328
    }
329
330
    /**
331
     * @param string $functionName
332
     * @param array $args
333
     *
334
     * @return $this
335
     */
336
    protected function callCollectionStateFunction($functionName, $args)
337
    {
338
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
339
340
        array_unshift($args, $currentTask);
341
        $collection = $this->getCollection();
342
        $fn = [$collection, $functionName];
343
344
        call_user_func_array($fn, $args);
345
        return $this;
346
    }
347
348
    /**
349
     * @param string $functionName
350
     * @param array $args
351
     *
352
     * @return $this
353
     *
354
     * @deprecated Use ::callCollectionStateFunction() instead.
355
     */
356
    protected function callCollectionStateFuntion($functionName, $args)
357
    {
358
        return $this->callCollectionStateFunction($functionName, $args);
359
    }
360
361
    /**
362
     * @param int $verbosityThreshold
363
     *
364
     * @return $this
365
     */
366
    public function setVerbosityThreshold($verbosityThreshold)
367
    {
368
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
369
        if ($currentTask) {
370
            $currentTask->setVerbosityThreshold($verbosityThreshold);
371
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Robo\Collection\CollectionBuilder) is incompatible with the return type of the parent method Robo\Task\BaseTask::setVerbosityThreshold of type Robo\Common\VerbosityThresholdTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
372
        }
373
        parent::setVerbosityThreshold($verbosityThreshold);
374
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Robo\Collection\CollectionBuilder) is incompatible with the return type of the parent method Robo\Task\BaseTask::setVerbosityThreshold of type Robo\Common\VerbosityThresholdTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
375
    }
376
377
378
    /**
379
     * Return the current task for this collection builder.
380
     * TODO: Not needed?
381
     *
382
     * @return \Robo\Contract\TaskInterface
383
     */
384
    public function getCollectionBuilderCurrentTask()
385
    {
386
        return $this->currentTask;
387
    }
388
389
    /**
390
     * Create a new builder with its own task collection
391
     *
392
     * @return \Robo\Collection\CollectionBuilder
393
     */
394
    public function newBuilder()
395
    {
396
        $collectionBuilder = new self($this->commandFile);
397
        $collectionBuilder->inflect($this);
398
        $collectionBuilder->simulated($this->isSimulated());
399
        $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
400
        $collectionBuilder->setState($this->getState());
401
402
        return $collectionBuilder;
403
    }
404
405
    /**
406
     * Calling the task builder with methods of the current
407
     * task calls through to that method of the task.
408
     *
409
     * There is extra complexity in this function that could be
410
     * simplified if we attached the 'LoadAllTasks' and custom tasks
411
     * to the collection builder instead of the RoboFile.  While that
412
     * change would be a better design overall, it would require that
413
     * the user do a lot more work to set up and use custom tasks.
414
     * We therefore take on some additional complexity here in order
415
     * to allow users to maintain their tasks in their RoboFile, which
416
     * is much more convenient.
417
     *
418
     * Calls to $this->collectionBuilder()->taskFoo() cannot be made
419
     * directly because all of the task methods are protected.  These
420
     * calls will therefore end up here.  If the method name begins
421
     * with 'task', then it is eligible to be used with the builder.
422
     *
423
     * When we call getBuiltTask, below, it will use the builder attached
424
     * to the commandfile to build the task. However, this is not what we
425
     * want:  the task needs to be built from THIS collection builder, so that
426
     * it will be affected by whatever state is active in this builder.
427
     * To do this, we have two choices: 1) save and restore the builder
428
     * in the commandfile, or 2) clone the commandfile and set this builder
429
     * on the copy. 1) is vulnerable to failure in multithreaded environments
430
     * (currently not supported), while 2) might cause confusion if there
431
     * is shared state maintained in the commandfile, which is in the
432
     * domain of the user.
433
     *
434
     * Note that even though we are setting up the commandFile to
435
     * use this builder, getBuiltTask always creates a new builder
436
     * (which is constructed using all of the settings from the
437
     * commandFile's builder), and the new task is added to that.
438
     * We therefore need to transfer the newly built task into this
439
     * builder. The temporary builder is discarded.
440
     *
441
     * @param string $fn
442
     * @param array $args
443
     *
444
     * @return $this|mixed
445
     */
446
    public function __call($fn, $args)
447
    {
448
        if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
449
            $saveBuilder = $this->commandFile->getBuilder();
450
            $this->commandFile->setBuilder($this);
451
            $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
452
            $this->commandFile->setBuilder($saveBuilder);
453
            if (!$temporaryBuilder) {
454
                throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
455
            }
456
            $temporaryBuilder->getCollection()->transferTasks($this);
457
            return $this;
458
        }
459
        if (!isset($this->currentTask)) {
460
            throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
461
        }
462
        // If the method called is a method of the current task,
463
        // then call through to the current task's setter method.
464
        $result = call_user_func_array([$this->currentTask, $fn], $args);
465
466
        // If something other than a setter method is called, then return its result.
467
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
468
        if (isset($result) && ($result !== $currentTask)) {
469
            return $result;
470
        }
471
472
        return $this;
473
    }
474
475
    /**
476
     * Construct the desired task and add it to this builder.
477
     *
478
     * @param string|object $name
479
     * @param array $args
480
     *
481
     * @return $this
482
     */
483
    public function build($name, $args)
484
    {
485
        $reflection = new ReflectionClass($name);
486
        $task = $reflection->newInstanceArgs($args);
487
        if (!$task) {
488
            throw new RuntimeException("Can not construct task $name");
489
        }
490
        $task = $this->fixTask($task, $args);
491
        $this->configureTask($name, $task);
0 ignored issues
show
It seems like $name defined by parameter $name on line 483 can also be of type object; however, Robo\Collection\CollectionBuilder::configureTask() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
492
        return $this->addTaskToCollection($task);
493
    }
494
495
    /**
496
     * @param \Robo\Contract\TaskInterface $task
497
     * @param array $args
498
     *
499
     * @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
500
     */
501
    protected function fixTask($task, $args)
502
    {
503
        if ($task instanceof InflectionInterface) {
504
            $task->inflect($this);
505
        }
506
        if ($task instanceof BuilderAwareInterface) {
507
            $task->setBuilder($this);
508
        }
509
        if ($task instanceof VerbosityThresholdInterface) {
510
            $task->setVerbosityThreshold($this->verbosityThreshold());
511
        }
512
513
        // Do not wrap our wrappers.
514
        if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
515
            return $task;
516
        }
517
518
        // Remember whether or not this is a task before
519
        // it gets wrapped in any decorator.
520
        $isTask = $task instanceof TaskInterface;
521
        $isCollection = $task instanceof NestedCollectionInterface;
522
523
        // If the task implements CompletionInterface, ensure
524
        // that its 'complete' method is called when the application
525
        // terminates -- but only if its 'run' method is called
526
        // first.  If the task is added to a collection, then the
527
        // task will be unwrapped via its `original` method, and
528
        // it will be re-wrapped with a new completion wrapper for
529
        // its new collection.
530
        if ($task instanceof CompletionInterface) {
531
            $task = new CompletionWrapper(Temporary::getCollection(), $task);
532
        }
533
534
        // If we are in simulated mode, then wrap any task in
535
        // a TaskSimulator.
536
        if ($isTask && !$isCollection && ($this->isSimulated())) {
537
            $task = new \Robo\Task\Simulator($task, $args);
538
            $task->inflect($this);
539
        }
540
541
        return $task;
542
    }
543
544
    /**
545
     * Check to see if there are any setter methods defined in configuration
546
     * for this task.
547
     *
548
     * @param string $taskClass
549
     * @param \Robo\Contract\TaskInterface $task
550
     */
551
    protected function configureTask($taskClass, $task)
552
    {
553
        $taskClass = static::configClassIdentifier($taskClass);
554
        $configurationApplier = new ConfigForSetters($this->getConfig(), $taskClass, 'task.');
555
        $configurationApplier->apply($task, 'settings');
556
557
        // TODO: If we counted each instance of $taskClass that was called from
558
        // this builder, then we could also apply configuration from
559
        // "task.{$taskClass}[$N].settings"
560
561
        // TODO: If the builder knew what the current command name was,
562
        // then we could also search for task configuration under
563
        // command-specific keys such as "command.{$commandname}.task.{$taskClass}.settings".
564
    }
565
566
    /**
567
     * When we run the collection builder, run everything in the collection.
568
     *
569
     * @return \Robo\Result
570
     */
571
    public function run()
572
    {
573
        $this->startTimer();
574
        $result = $this->runTasks();
575
        $this->stopTimer();
576
        $result['time'] = $this->getExecutionTime();
577
        $result->mergeData($this->getState()->getData());
578
        return $result;
579
    }
580
581
    /**
582
     * If there is a single task, run it; if there is a collection, run
583
     * all of its tasks.
584
     *
585
     * @return \Robo\Result
586
     */
587
    protected function runTasks()
588
    {
589
        if (!$this->collection && $this->currentTask) {
590
            $result = $this->currentTask->run();
591
            return Result::ensureResult($this->currentTask, $result);
592
        }
593
        return $this->getCollection()->run();
594
    }
595
596
    /**
597
     * {@inheritdoc}
598
     */
599
    public function getCommand()
600
    {
601
        if (!$this->collection && $this->currentTask) {
602
            $task = $this->currentTask;
603
            $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
604
            if ($task instanceof CommandInterface) {
605
                return $task->getCommand();
606
            }
607
        }
608
609
        return $this->getCollection()->getCommand();
610
    }
611
612
    /**
613
     * @return \Robo\Collection\CollectionInterface
614
     */
615
    public function original()
616
    {
617
        return $this->getCollection();
618
    }
619
620
    /**
621
     * Return the collection of tasks associated with this builder.
622
     *
623
     * @return \Robo\Collection\CollectionInterface
624
     */
625
    public function getCollection()
626
    {
627
        if (!isset($this->collection)) {
628
            $this->collection = new Collection();
629
            $this->collection->inflect($this);
630
            $this->collection->setState($this->getState());
631
            $this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
632
633
            if (isset($this->currentTask)) {
634
                $this->collection->add($this->currentTask);
635
            }
636
        }
637
        return $this->collection;
638
    }
639
}
640