Collection   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 748
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 18

Importance

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