Collection   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 781
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 17

Importance

Changes 0
Metric Value
wmc 85
lcom 2
cbo 17
dl 0
loc 781
rs 1.819
c 0
b 0
f 0

44 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setProgressBarAutoDisplayInterval() 0 7 2
A add() 0 6 1
A addCode() 0 4 1
A addIterable() 0 5 1
A rollback() 0 6 1
A rollbackCode() 0 6 1
A completion() 0 13 1
A completionCode() 0 5 1
A before() 0 4 1
A after() 0 4 1
A progressMessage() 0 11 1
A wrapAndRegisterRollback() 0 12 1
A addBeforeOrAfter() 0 10 2
A ignoreErrorsTaskWrapper() 0 26 4
A ignoreErrorsCodeWrapper() 0 4 1
A taskNames() 0 4 1
A hasTask() 0 4 1
A namedTask() 0 7 2
A addTaskList() 0 7 2
A addToTaskList() 0 7 1
A addCollectionElementToTaskList() 0 14 2
A setParentCollection() 0 5 1
A getParentCollection() 0 4 2
A registerRollback() 0 9 3
A registerCompletion() 0 11 3
A progressIndicatorSteps() 0 8 2
A getCommand() 0 22 5
A run() 0 6 1
A runWithoutCompletion() 0 25 5
B runTaskList() 0 29 6
A fail() 0 7 1
A complete() 0 7 1
A reset() 0 7 1
A runRollbackTasks() 0 9 1
A runSubtask() 0 16 4
A doStateUpdates() 0 11 3
A storeState() 0 6 1
A deferTaskConfiguration() 0 11 1
A defer() 0 6 1
A doDeferredInitialization() 0 18 4
A setParentCollectionForTask() 0 6 2
A runTaskListIgnoringFailures() 0 16 5
A transferTasks() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Collection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Collection, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Robo\Collection;
3
4
use Robo\Exception\AbortTasksException;
5
use Robo\Result;
6
use Robo\State\Data;
7
use Psr\Log\LogLevel;
8
use Robo\Contract\TaskInterface;
9
use Robo\Task\StackBasedTask;
10
use Robo\Task\BaseTask;
11
use Robo\TaskInfo;
12
use Robo\Contract\WrappedTaskInterface;
13
use Robo\Exception\TaskException;
14
use Robo\Exception\TaskExitException;
15
use Robo\Contract\CommandInterface;
16
17
use Robo\Contract\InflectionInterface;
18
use Robo\State\StateAwareInterface;
19
use Robo\State\StateAwareTrait;
20
21
/**
22
 * Group tasks into a collection that run together. Supports
23
 * rollback operations for handling error conditions.
24
 *
25
 * This is an internal class. Clients should use a CollectionBuilder
26
 * rather than direct use of the Collection class.  @see CollectionBuilder.
27
 *
28
 * Below, the example FilesystemStack task is added to a collection,
29
 * and associated with a rollback task.  If any of the operations in
30
 * the FilesystemStack, or if any of the other tasks also added to
31
 * the task collection should fail, then the rollback function is
32
 * called. Here, taskDeleteDir is used to remove partial results
33
 * of an unfinished task.
34
 */
35
class Collection extends BaseTask implements CollectionInterface, CommandInterface, StateAwareInterface
36
{
37
    use StateAwareTrait;
38
39
    /**
40
     * @var \Robo\Collection\Element[]
41
     */
42
    protected $taskList = [];
43
44
    /**
45
     * @var \Robo\Contract\TaskInterface[]
46
     */
47
    protected $rollbackStack = [];
48
49
    /**
50
     * @var \Robo\Contract\TaskInterface[]
51
     */
52
    protected $completionStack = [];
53
54
    /**
55
     * @var \Robo\Collection\CollectionInterface
56
     */
57
    protected $parentCollection;
58
59
    /**
60
     * @var callable[]
61
     */
62
    protected $deferredCallbacks = [];
63
64
    /**
65
     * @var string[]
66
     */
67
    protected $messageStoreKeys = [];
68
69
    /**
70
     * Constructor.
71
     */
72
    public function __construct()
73
    {
74
        $this->resetState();
75
    }
76
77
    /**
78
     * @param int $interval
79
     */
80
    public function setProgressBarAutoDisplayInterval($interval)
81
    {
82
        if (!$this->progressIndicator) {
83
            return;
84
        }
85
        return $this->progressIndicator->setProgressBarAutoDisplayInterval($interval);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function add(TaskInterface $task, $name = self::UNNAMEDTASK)
92
    {
93
        $task = new CompletionWrapper($this, $task);
94
        $this->addToTaskList($name, $task);
95
        return $this;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function addCode(callable $code, $name = self::UNNAMEDTASK)
102
    {
103
        return $this->add(new CallableTask($code, $this), $name);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function addIterable($iterable, callable $code)
110
    {
111
        $callbackTask = (new IterationTask($iterable, $code, $this))->inflect($this);
112
        return $this->add($callbackTask);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function rollback(TaskInterface $rollbackTask)
119
    {
120
        // Rollback tasks always try as hard as they can, and never report failures.
121
        $rollbackTask = $this->ignoreErrorsTaskWrapper($rollbackTask);
122
        return $this->wrapAndRegisterRollback($rollbackTask);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function rollbackCode(callable $rollbackCode)
129
    {
130
        // Rollback tasks always try as hard as they can, and never report failures.
131
        $rollbackTask = $this->ignoreErrorsCodeWrapper($rollbackCode);
132
        return $this->wrapAndRegisterRollback($rollbackTask);
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function completion(TaskInterface $completionTask)
139
    {
140
        $collection = $this;
141
        $completionRegistrationTask = new CallableTask(
142
            function () use ($collection, $completionTask) {
143
144
                $collection->registerCompletion($completionTask);
145
            },
146
            $this
147
        );
148
        $this->addToTaskList(self::UNNAMEDTASK, $completionRegistrationTask);
149
        return $this;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function completionCode(callable $completionTask)
156
    {
157
        $completionTask = new CallableTask($completionTask, $this);
158
        return $this->completion($completionTask);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
165
    {
166
        return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
173
    {
174
        return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
181
    {
182
        $context += ['name' => 'Progress'];
183
        $context += TaskInfo::getTaskContext($this);
184
        return $this->addCode(
185
            function () use ($level, $text, $context) {
186
                $context += $this->getState()->getData();
187
                $this->printTaskOutput($level, $text, $context);
0 ignored issues
show
Bug introduced by Greg Anderson
It seems like $level defined by parameter $level on line 180 can also be of type object<Psr\Log\LogLevel>; however, Robo\Common\TaskIO::printTaskOutput() 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...
188
            }
189
        );
190
    }
191
192
    /**
193
     * @param \Robo\Contract\TaskInterface $rollbackTask
194
     *
195
     * @return $this
196
     */
197
    protected function wrapAndRegisterRollback(TaskInterface $rollbackTask)
198
    {
199
        $collection = $this;
200
        $rollbackRegistrationTask = new CallableTask(
201
            function () use ($collection, $rollbackTask) {
202
                $collection->registerRollback($rollbackTask);
203
            },
204
            $this
205
        );
206
        $this->addToTaskList(self::UNNAMEDTASK, $rollbackRegistrationTask);
207
        return $this;
208
    }
209
210
    /**
211
     * Add either a 'before' or 'after' function or task.
212
     *
213
     * @param string $method
214
     * @param string $name
215
     * @param callable|\Robo\Contract\TaskInterface $task
216
     * @param string $nameOfTaskToAdd
217
     *
218
     * @return $this
219
     */
220
    protected function addBeforeOrAfter($method, $name, $task, $nameOfTaskToAdd)
221
    {
222
        if (is_callable($task)) {
223
            $task = new CallableTask($task, $this);
224
        }
225
        $existingTask = $this->namedTask($name);
226
        $fn = [$existingTask, $method];
227
        call_user_func($fn, $task, $nameOfTaskToAdd);
228
        return $this;
229
    }
230
231
    /**
232
     * Wrap the provided task in a wrapper that will ignore
233
     * any errors or exceptions that may be produced.  This
234
     * is useful, for example, in adding optional cleanup tasks
235
     * at the beginning of a task collection, to remove previous
236
     * results which may or may not exist.
237
     *
238
     * TODO: Provide some way to specify which sort of errors
239
     * are ignored, so that 'file not found' may be ignored,
240
     * but 'permission denied' reported?
241
     *
242
     * @param \Robo\Contract\TaskInterface $task
243
     *
244
     * @return \Robo\Collection\CallableTask
245
     */
246
    public function ignoreErrorsTaskWrapper(TaskInterface $task)
247
    {
248
        // If the task is a stack-based task, then tell it
249
        // to try to run all of its operations, even if some
250
        // of them fail.
251
        if ($task instanceof StackBasedTask) {
252
            $task->stopOnFail(false);
253
        }
254
        $ignoreErrorsInTask = function () use ($task) {
255
            $data = [];
256
            try {
257
                $result = $this->runSubtask($task);
258
                $message = $result->getMessage();
259
                $data = $result->getData();
260
                $data['exitcode'] = $result->getExitCode();
261
            } catch (AbortTasksException $abortTasksException) {
262
                throw $abortTasksException;
263
            } catch (\Exception $e) {
264
                $message = $e->getMessage();
265
            }
266
267
            return Result::success($task, $message, $data);
268
        };
269
        // Wrap our ignore errors callable in a task.
270
        return new CallableTask($ignoreErrorsInTask, $this);
271
    }
272
273
    /**
274
     * @param callable $task
275
     *
276
     * @return \Robo\Collection\CallableTask
277
     */
278
    public function ignoreErrorsCodeWrapper(callable $task)
279
    {
280
        return $this->ignoreErrorsTaskWrapper(new CallableTask($task, $this));
281
    }
282
283
    /**
284
     * Return the list of task names added to this collection.
285
     *
286
     * @return string[]
287
     */
288
    public function taskNames()
289
    {
290
        return array_keys($this->taskList);
291
    }
292
293
    /**
294
     * Test to see if a specified task name exists.
295
     * n.b. before() and after() require that the named
296
     * task exist; use this function to test first, if
297
     * unsure.
298
     *
299
     * @param string $name
300
     *
301
     * @return bool
302
     */
303
    public function hasTask($name)
304
    {
305
        return array_key_exists($name, $this->taskList);
306
    }
307
308
    /**
309
     * Find an existing named task.
310
     *
311
     * @param string $name
312
     *   The name of the task to insert before.  The named task MUST exist.
313
     *
314
     * @return \Robo\Collection\Element
315
     *   The task group for the named task. Generally this is only
316
     *   used to call 'before()' and 'after()'.
317
     */
318
    protected function namedTask($name)
319
    {
320
        if (!$this->hasTask($name)) {
321
            throw new \RuntimeException("Could not find task named $name");
322
        }
323
        return $this->taskList[$name];
324
    }
325
326
    /**
327
     * Add a list of tasks to our task collection.
328
     *
329
     * @param \Robo\Contract\TaskInterface[] $tasks
330
     *   An array of tasks to run with rollback protection
331
     *
332
     * @return $this
333
     */
334
    public function addTaskList(array $tasks)
335
    {
336
        foreach ($tasks as $name => $task) {
337
            $this->add($task, $name);
338
        }
339
        return $this;
340
    }
341
342
    /**
343
     * Add the provided task to our task list.
344
     *
345
     * @param string $name
346
     * @param \Robo\Contract\TaskInterface $task
347
     *
348
     * @return $this
349
     */
350
    protected function addToTaskList($name, TaskInterface $task)
351
    {
352
        // All tasks are stored in a task group so that we have a place
353
        // to hang 'before' and 'after' tasks.
354
        $taskGroup = new Element($task);
355
        return $this->addCollectionElementToTaskList($name, $taskGroup);
356
    }
357
358
    /**
359
     * @param int|string $name
360
     * @param \Robo\Collection\Element $taskGroup
361
     *
362
     * @return $this
363
     */
364
    protected function addCollectionElementToTaskList($name, Element $taskGroup)
365
    {
366
        // If a task name is not provided, then we'll let php pick
367
        // the array index.
368
        if (Result::isUnnamed($name)) {
369
            $this->taskList[] = $taskGroup;
370
            return $this;
371
        }
372
        // If we are replacing an existing task with the
373
        // same name, ensure that our new task is added to
374
        // the end.
375
        $this->taskList[$name] = $taskGroup;
376
        return $this;
377
    }
378
379
    /**
380
     * Set the parent collection. This is necessary so that nested
381
     * collections' rollback and completion tasks can be added to the
382
     * top-level collection, ensuring that the rollbacks for a collection
383
     * will run if any later task fails.
384
     *
385
     * @param \Robo\Collection\NestedCollectionInterface $parentCollection
386
     *
387
     * @return $this
388
     */
389
    public function setParentCollection(NestedCollectionInterface $parentCollection)
390
    {
391
        $this->parentCollection = $parentCollection;
0 ignored issues
show
Documentation Bug introduced by Greg Anderson
$parentCollection is of type object<Robo\Collection\NestedCollectionInterface>, but the property $parentCollection was declared to be of type object<Robo\Collection\CollectionInterface>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
392
        return $this;
393
    }
394
395
    /**
396
     * Get the appropriate parent collection to use
397
     *
398
     * @return \Robo\Collection\CollectionInterface|$this
399
     */
400
    public function getParentCollection()
401
    {
402
        return $this->parentCollection ? $this->parentCollection : $this;
403
    }
404
405
    /**
406
     * Register a rollback task to run if there is any failure.
407
     *
408
     * Clients are free to add tasks to the rollback stack as
409
     * desired; however, usually it is preferable to call
410
     * Collection::rollback() instead.  With that function,
411
     * the rollback function will only be called if all of the
412
     * tasks added before it complete successfully, AND some later
413
     * task fails.
414
     *
415
     * One example of a good use-case for registering a callback
416
     * function directly is to add a task that sends notification
417
     * when a task fails.
418
     *
419
     * @param \Robo\Contract\TaskInterface $rollbackTask
420
     *   The rollback task to run on failure.
421
     *
422
     * @return null
423
     */
424
    public function registerRollback(TaskInterface $rollbackTask)
425
    {
426
        if ($this->parentCollection) {
427
            return $this->parentCollection->registerRollback($rollbackTask);
0 ignored issues
show
Bug introduced by Greg Anderson
The method registerRollback() does not exist on Robo\Collection\CollectionInterface. Did you maybe mean rollback()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
428
        }
429
        if ($rollbackTask) {
430
            array_unshift($this->rollbackStack, $rollbackTask);
431
        }
432
    }
433
434
    /**
435
     * Register a completion task to run once all other tasks finish.
436
     * Completion tasks run whether or not a rollback operation was
437
     * triggered. They do not trigger rollbacks if they fail.
438
     *
439
     * The typical use-case for a completion function is to clean up
440
     * temporary objects (e.g. temporary folders).  The preferred
441
     * way to do that, though, is to use Temporary::wrap().
442
     *
443
     * On failures, completion tasks will run after all rollback tasks.
444
     * If one task collection is nested inside another task collection,
445
     * then the nested collection's completion tasks will run as soon as
446
     * the nested task completes; they are not deferred to the end of
447
     * the containing collection's execution.
448
     *
449
     * @param \Robo\Contract\TaskInterface $completionTask
450
     *   The completion task to run at the end of all other operations.
451
     *
452
     * @return null
453
     */
454
    public function registerCompletion(TaskInterface $completionTask)
455
    {
456
        if ($this->parentCollection) {
457
            return $this->parentCollection->registerCompletion($completionTask);
0 ignored issues
show
Bug introduced by Greg Anderson
The method registerCompletion() does not exist on Robo\Collection\CollectionInterface. Did you maybe mean completion()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
458
        }
459
        if ($completionTask) {
460
            // Completion tasks always try as hard as they can, and never report failures.
461
            $completionTask = $this->ignoreErrorsTaskWrapper($completionTask);
462
            $this->completionStack[] = $completionTask;
463
        }
464
    }
465
466
    /**
467
     * Return the count of steps in this collection
468
     *
469
     * @return int
470
     */
471
    public function progressIndicatorSteps()
472
    {
473
        $steps = 0;
474
        foreach ($this->taskList as $name => $taskGroup) {
475
            $steps += $taskGroup->progressIndicatorSteps();
476
        }
477
        return $steps;
478
    }
479
480
    /**
481
     * A Collection of tasks can provide a command via `getCommand()`
482
     * if it contains a single task, and that task implements CommandInterface.
483
     *
484
     * @return string
485
     *
486
     * @throws \Robo\Exception\TaskException
487
     */
488
    public function getCommand()
489
    {
490
        if (empty($this->taskList)) {
491
            return '';
492
        }
493
494
        if (count($this->taskList) > 1) {
495
            // TODO: We could potentially iterate over the items in the collection
496
            // and concatenate the result of getCommand() from each one, and fail
497
            // only if we encounter a command that is not a CommandInterface.
498
            throw new TaskException($this, "getCommand() does not work on arbitrary collections of tasks.");
499
        }
500
501
        $taskElement = reset($this->taskList);
502
        $task = $taskElement->getTask();
503
        $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
504
        if ($task instanceof CommandInterface) {
505
            return $task->getCommand();
506
        }
507
508
        throw new TaskException($task, get_class($task) . " does not implement CommandInterface, so can't be used to provide a command");
509
    }
510
511
    /**
512
     * Run our tasks, and roll back if necessary.
513
     *
514
     * @return \Robo\Result
515
     */
516
    public function run()
517
    {
518
        $result = $this->runWithoutCompletion();
519
        $this->complete();
520
        return $result;
521
    }
522
523
    /**
524
     * @return \Robo\Result
525
     */
526
    private function runWithoutCompletion()
527
    {
528
        $result = Result::success($this);
529
530
        if (empty($this->taskList)) {
531
            return $result;
532
        }
533
534
        $this->startProgressIndicator();
535
        if ($result->wasSuccessful()) {
536
            foreach ($this->taskList as $name => $taskGroup) {
537
                $taskList = $taskGroup->getTaskList();
538
                $result = $this->runTaskList($name, $taskList, $result);
0 ignored issues
show
Documentation introduced by Greg Anderson
$taskList is of type array<integer,callable|o...ract\\TaskInterface>"}>, but the function expects a array<integer,object<Rob...ontract\TaskInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
539
                if (!$result->wasSuccessful()) {
540
                    $this->fail();
541
                    return $result;
542
                }
543
            }
544
            $this->taskList = [];
545
        }
546
        $this->stopProgressIndicator();
547
        $result['time'] = $this->getExecutionTime();
548
549
        return $result;
550
    }
551
552
    /**
553
     * Run every task in a list, but only up to the first failure.
554
     * Return the failing result, or success if all tasks run.
555
     *
556
     * @param string $name
557
     * @param \Robo\Contract\TaskInterface[] $taskList
558
     * @param \Robo\Result $result
559
     *
560
     * @return \Robo\Result
561
     *
562
     * @throws \Robo\Exception\TaskExitException
563
     */
564
    private function runTaskList($name, array $taskList, Result $result)
565
    {
566
        try {
567
            foreach ($taskList as $taskName => $task) {
568
                $taskResult = $this->runSubtask($task);
569
                $this->advanceProgressIndicator();
570
                // If the current task returns an error code, then stop
571
                // execution and signal a rollback.
572
                if (!$taskResult->wasSuccessful()) {
573
                    return $taskResult;
574
                }
575
                // We accumulate our results into a field so that tasks that
576
                // have a reference to the collection may examine and modify
577
                // the incremental results, if they wish.
578
                $key = Result::isUnnamed($taskName) ? $name : $taskName;
579
                $result->accumulate($key, $taskResult);
580
                // The result message will be the message of the last task executed.
581
                $result->setMessage($taskResult->getMessage());
582
            }
583
        } catch (TaskExitException $exitException) {
584
            $this->fail();
585
            throw $exitException;
586
        } catch (\Exception $e) {
587
            // Tasks typically should not throw, but if one does, we will
588
            // convert it into an error and roll back.
589
            return Result::fromException($task, $e, $result->getData());
590
        }
591
        return $result;
592
    }
593
594
    /**
595
     * Force the rollback functions to run
596
     *
597
     * @return $this
598
     */
599
    public function fail()
600
    {
601
        $this->disableProgressIndicator();
602
        $this->runRollbackTasks();
603
        $this->complete();
604
        return $this;
605
    }
606
607
    /**
608
     * Force the completion functions to run
609
     *
610
     * @return $this
611
     */
612
    public function complete()
613
    {
614
        $this->detatchProgressIndicator();
615
        $this->runTaskListIgnoringFailures($this->completionStack);
616
        $this->reset();
617
        return $this;
618
    }
619
620
    /**
621
     * Reset this collection, removing all tasks.
622
     *
623
     * @return $this
624
     */
625
    public function reset()
626
    {
627
        $this->taskList = [];
628
        $this->completionStack = [];
629
        $this->rollbackStack = [];
630
        return $this;
631
    }
632
633
    /**
634
     * Run all of our rollback tasks.
635
     *
636
     * Note that Collection does not implement RollbackInterface, but
637
     * it may still be used as a task inside another task collection
638
     * (i.e. you can nest task collections, if desired).
639
     */
640
    protected function runRollbackTasks()
641
    {
642
        $this->runTaskListIgnoringFailures($this->rollbackStack);
643
        // Erase our rollback stack once we have finished rolling
644
        // everything back.  This will allow us to potentially use
645
        // a command collection more than once (e.g. to retry a
646
        // failed operation after doing some error recovery).
647
        $this->rollbackStack = [];
648
    }
649
650
    /**
651
     * @param \Robo\Contract\TaskInterface|\Robo\Collection\NestedCollectionInterface|\Robo\Contract\WrappedTaskInterface $task
652
     *
653
     * @return \Robo\Result
654
     */
655
    protected function runSubtask($task)
656
    {
657
        $original = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
658
        $this->setParentCollectionForTask($original, $this->getParentCollection());
659
        if ($original instanceof InflectionInterface) {
660
            $original->inflect($this);
661
        }
662
        if ($original instanceof StateAwareInterface) {
663
            $original->setState($this->getState());
664
        }
665
        $this->doDeferredInitialization($original);
0 ignored issues
show
Bug introduced by Greg Anderson
It seems like $original defined by $task instanceof \Robo\C...ask->original() : $task on line 657 can also be of type object<Robo\Collection\NestedCollectionInterface>; however, Robo\Collection\Collecti...eferredInitialization() does only seem to accept object<Robo\Contract\TaskInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
666
        $taskResult = $task->run();
0 ignored issues
show
Bug introduced by Greg Anderson
The method run does only exist in Robo\Contract\TaskInterface, but not in Robo\Collection\NestedCollectionInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
667
        $taskResult = Result::ensureResult($task, $taskResult);
0 ignored issues
show
Bug introduced by Greg Anderson
It seems like $task defined by parameter $task on line 655 can also be of type object<Robo\Collection\NestedCollectionInterface>; however, Robo\Result::ensureResult() does only seem to accept object<Robo\Contract\TaskInterface>, 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...
668
        $this->doStateUpdates($original, $taskResult);
0 ignored issues
show
Bug introduced by Greg Anderson
It seems like $original defined by $task instanceof \Robo\C...ask->original() : $task on line 657 can also be of type object<Robo\Collection\NestedCollectionInterface>; however, Robo\Collection\Collection::doStateUpdates() does only seem to accept object<Robo\Contract\TaskInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
669
        return $taskResult;
670
    }
671
672
    /**
673
     * @param \Robo\Contract\TaskInterface $task
674
     * @param \Robo\State\Data $taskResult
675
     */
676
    protected function doStateUpdates($task, Data $taskResult)
677
    {
678
        $this->updateState($taskResult);
679
        $key = spl_object_hash($task);
680
        if (array_key_exists($key, $this->messageStoreKeys)) {
681
            $state = $this->getState();
682
            list($stateKey, $sourceKey) = $this->messageStoreKeys[$key];
683
            $value = empty($sourceKey) ? $taskResult->getMessage() : $taskResult[$sourceKey];
684
            $state[$stateKey] = $value;
685
        }
686
    }
687
688
    /**
689
     * @param \Robo\Contract\TaskInterface $task
690
     * @param string $key
691
     * @param string $source
692
     *
693
     * @return $this
694
     */
695
    public function storeState($task, $key, $source = '')
696
    {
697
        $this->messageStoreKeys[spl_object_hash($task)] = [$key, $source];
698
699
        return $this;
700
    }
701
702
    /**
703
     * @param \Robo\Contract\TaskInterface $task
704
     * @param string $functionName
705
     * @param string $stateKey
706
     *
707
     * @return $this
708
     */
709
    public function deferTaskConfiguration($task, $functionName, $stateKey)
710
    {
711
        return $this->defer(
712
            $task,
713
            function ($task, $state) use ($functionName, $stateKey) {
714
                $fn = [$task, $functionName];
715
                $value = $state[$stateKey];
716
                $fn($value);
717
            }
718
        );
719
    }
720
721
    /**
722
     * Defer execution of a callback function until just before a task
723
     * runs. Use this time to provide more settings for the task, e.g. from
724
     * the collection's shared state, which is populated with the results
725
     * of previous test runs.
726
     *
727
     * @param \Robo\Contract\TaskInterface $task
728
     * @param callable $callback
729
     *
730
     * @return $this
731
     */
732
    public function defer($task, $callback)
733
    {
734
        $this->deferredCallbacks[spl_object_hash($task)][] = $callback;
735
736
        return $this;
737
    }
738
739
    /**
740
     * @param \Robo\Contract\TaskInterface $task
741
     */
742
    protected function doDeferredInitialization($task)
743
    {
744
        // If the task is a state consumer, then call its receiveState method
745
        if ($task instanceof \Robo\State\Consumer) {
746
            $task->receiveState($this->getState());
747
        }
748
749
        // Check and see if there are any deferred callbacks for this task.
750
        $key = spl_object_hash($task);
751
        if (!array_key_exists($key, $this->deferredCallbacks)) {
752
            return;
753
        }
754
755
        // Call all of the deferred callbacks
756
        foreach ($this->deferredCallbacks[$key] as $fn) {
0 ignored issues
show
Bug introduced by Greg Anderson
The expression $this->deferredCallbacks[$key] of type callable is not traversable.
Loading history...
757
            $fn($task, $this->getState());
758
        }
759
    }
760
761
    /**
762
     * @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
763
     * @param \Robo\Collection\CollectionInterface $parentCollection
764
     */
765
    protected function setParentCollectionForTask($task, $parentCollection)
766
    {
767
        if ($task instanceof NestedCollectionInterface) {
768
            $task->setParentCollection($parentCollection);
769
        }
770
    }
771
772
    /**
773
     * Run all of the tasks in a provided list, ignoring failures.
774
     *
775
     * You may force a failure by throwing a ForcedException in your rollback or
776
     * completion task or callback.
777
     *
778
     * This is used to roll back or complete.
779
     *
780
     * @param \Robo\Contract\TaskInterface[] $taskList
781
     */
782
    protected function runTaskListIgnoringFailures(array $taskList)
783
    {
784
        foreach ($taskList as $task) {
785
            try {
786
                $this->runSubtask($task);
787
            } catch (AbortTasksException $abortTasksException) {
788
                // If there's a forced exception, end the loop of tasks.
789
                if ($message = $abortTasksException->getMessage()) {
790
                    $this->logger()->notice($message);
791
                }
792
                break;
793
            } catch (\Exception $e) {
794
                // Ignore rollback failures.
795
            }
796
        }
797
    }
798
799
    /**
800
     * Give all of our tasks to the provided collection builder.
801
     *
802
     * @param \Robo\Collection\CollectionBuilder $builder
803
     */
804
    public function transferTasks($builder)
805
    {
806
        foreach ($this->taskList as $name => $taskGroup) {
807
            // TODO: We are abandoning all of our before and after tasks here.
808
            // At the moment, transferTasks is only called under conditions where
809
            // there will be none of these, but care should be taken if that changes.
810
            $task = $taskGroup->getTask();
811
            $builder->addTaskToCollection($task);
812
        }
813
        $this->reset();
814
    }
815
}
816