Completed
Pull Request — master (#546)
by Greg
03:06
created

CollectionBuilder::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 11
Ratio 100 %

Importance

Changes 0
Metric Value
dl 11
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
namespace Robo\Collection;
3
4
use Robo\Config;
5
use Psr\Log\LogLevel;
6
use Robo\Contract\InflectionInterface;
7
use Robo\Contract\TaskInterface;
8
use Robo\Contract\CompletionInterface;
9
use Robo\Contract\WrappedTaskInterface;
10
use Robo\Task\Simulator;
11
use ReflectionClass;
12
use Robo\Task\BaseTask;
13
use Robo\Contract\BuilderAwareInterface;
14
use Robo\Contract\CommandInterface;
15
use Robo\Contract\VerbosityThresholdInterface;
16
17
/**
18
 * Creates a collection, and adds tasks to it.  The collection builder
19
 * offers a streamlined chained-initialization mechanism for easily
20
 * creating task groups.  Facilities for creating working and temporary
21
 * directories are also provided.
22
 *
23
 * ``` php
24
 * <?php
25
 * $result = $this->collectionBuilder()
26
 *   ->taskFilesystemStack()
27
 *     ->mkdir('g')
28
 *     ->touch('g/g.txt')
29
 *   ->rollback(
30
 *     $this->taskDeleteDir('g')
31
 *   )
32
 *   ->taskFilesystemStack()
33
 *     ->mkdir('g/h')
34
 *     ->touch('g/h/h.txt')
35
 *   ->taskFilesystemStack()
36
 *     ->mkdir('g/h/i/c')
37
 *     ->touch('g/h/i/i.txt')
38
 *   ->run()
39
 * ?>
40
 *
41
 * In the example above, the `taskDeleteDir` will be called if
42
 * ```
43
 */
44
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface
45
{
46
47
    /**
48
     * @var \Robo\Tasks
49
     */
50
    protected $commandFile;
51
52
    /**
53
     * @var CollectionInterface
54
     */
55
    protected $collection;
56
57
    /**
58
     * @var TaskInterface
59
     */
60
    protected $currentTask;
61
62
    /**
63
     * @var bool
64
     */
65
    protected $simulated;
66
67
    /**
68
     * @param \Robo\Tasks $commandFile
69
     */
70
    public function __construct($commandFile)
71
    {
72
        $this->commandFile = $commandFile;
73
    }
74
75 View Code Duplication
    public static function create($container, $commandFile)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
76
    {
77
        $builder = new self($commandFile);
78
79
        $builder->setLogger($container->get('logger'));
80
        $builder->setProgressIndicator($container->get('progressIndicator'));
81
        $builder->setConfig($container->get('config'));
82
        $builder->setOutputAdapter($container->get('outputAdapter'));
83
84
        return $builder;
85
    }
86
87
    /**
88
     * @param bool $simulated
89
     *
90
     * @return $this
91
     */
92
    public function simulated($simulated = true)
93
    {
94
        $this->simulated = $simulated;
95
        return $this;
96
    }
97
98
    /**
99
     * @return bool
100
     */
101
    public function isSimulated()
102
    {
103
        if (!isset($this->simulated)) {
104
            $this->simulated = $this->getConfig()->get(Config::SIMULATE);
105
        }
106
        return $this->simulated;
107
    }
108
109
    /**
110
     * Create a temporary directory to work in. When the collection
111
     * completes or rolls back, the temporary directory will be deleted.
112
     * Returns the path to the location where the directory will be
113
     * created.
114
     *
115
     * @param string $prefix
116
     * @param string $base
117
     * @param bool $includeRandomPart
118
     *
119
     * @return string
120
     */
121
    public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
122
    {
123
        // n.b. Any task that the builder is asked to create is
124
        // automatically added to the builder's collection, and
125
        // wrapped in the builder object. Therefore, the result
126
        // of any call to `taskFoo()` from within the builder will
127
        // always be `$this`.
128
        return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
0 ignored issues
show
Bug introduced by
The method taskTmpDir() does not exist on Robo\Collection\CollectionBuilder. Did you maybe mean tmpDir()?

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...
129
    }
130
131
    /**
132
     * Create a working directory to hold results. A temporary directory
133
     * is first created to hold the intermediate results.  After the
134
     * builder finishes, the work directory is moved into its final location;
135
     * any results already in place will be moved out of the way and
136
     * then deleted.
137
     *
138
     * @param string $finalDestination The path where the working directory
139
     *   will be moved once the task collection completes.
140
     *
141
     * @return string
142
     */
143
    public function workDir($finalDestination)
144
    {
145
        // Creating the work dir task in this context adds it to our task collection.
146
        return $this->taskWorkDir($finalDestination)->getPath();
0 ignored issues
show
Bug introduced by
The method taskWorkDir() does not exist on Robo\Collection\CollectionBuilder. Did you maybe mean workDir()?

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...
147
    }
148
149
    public function addTask(TaskInterface $task)
150
    {
151
        $this->getCollection()->add($task);
152
        return $this;
153
    }
154
155
  /**
156
   * Add arbitrary code to execute as a task.
157
   *
158
   * @see \Robo\Collection\CollectionInterface::addCode
159
   *
160
   * @param callable $code
161
   * @param int|string $name
162
   * @return $this
163
   */
164
    public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
165
    {
166
        $this->getCollection()->addCode($code, $name);
167
        return $this;
168
    }
169
170
    /**
171
     * Add a list of tasks to our task collection.
172
     *
173
     * @param TaskInterface[] $tasks
174
     *   An array of tasks to run with rollback protection
175
     *
176
     * @return $this
177
     */
178
    public function addTaskList(array $tasks)
179
    {
180
        $this->getCollection()->addTaskList($tasks);
181
        return $this;
182
    }
183
184
    public function rollback(TaskInterface $task)
185
    {
186
        // Ensure that we have a collection if we are going to add
187
        // a rollback function.
188
        $this->getCollection()->rollback($task);
189
        return $this;
190
    }
191
192
    public function rollbackCode(callable $rollbackCode)
193
    {
194
        $this->getCollection()->rollbackCode($rollbackCode);
195
        return $this;
196
    }
197
198
    public function completion(TaskInterface $task)
199
    {
200
        $this->getCollection()->completion($task);
201
        return $this;
202
    }
203
204
    public function completionCode(callable $completionCode)
205
    {
206
        $this->getCollection()->completionCode($completionCode);
207
        return $this;
208
    }
209
210
    /**
211
     * @param string $text
212
     * @param array $context
213
     * @param string $level
214
     *
215
     * @return $this
216
     */
217
    public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
218
    {
219
        $this->getCollection()->progressMessage($text, $context, $level);
220
        return $this;
221
    }
222
223
    /**
224
     * @param \Robo\Collection\NestedCollectionInterface $parentCollection
225
     *
226
     * @return $this
227
     */
228
    public function setParentCollection(NestedCollectionInterface $parentCollection)
229
    {
230
        $this->getCollection()->setParentCollection($parentCollection);
231
        return $this;
232
    }
233
234
    /**
235
     * Called by the factory method of each task; adds the current
236
     * task to the task builder.
237
     *
238
     * TODO: protected
239
     *
240
     * @param TaskInterface $task
241
     *
242
     * @return $this
243
     */
244
    public function addTaskToCollection($task)
245
    {
246
        // Postpone creation of the collection until the second time
247
        // we are called. At that time, $this->currentTask will already
248
        // be populated.  We call 'getCollection()' so that it will
249
        // create the collection and add the current task to it.
250
        // Note, however, that if our only tasks implements NestedCollectionInterface,
251
        // then we should force this builder to use a collection.
252
        if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
253
            $this->getCollection();
254
        }
255
        $this->currentTask = $task;
256
        if ($this->collection) {
257
            $this->collection->add($task);
258
        }
259
        return $this;
260
    }
261
262
    public function setVerbosityThreshold($verbosityThreshold)
263
    {
264
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
265
        if ($currentTask) {
266
            $currentTask->setVerbosityThreshold($verbosityThreshold);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Robo\Contract\TaskInterface as the method setVerbosityThreshold() does only exist in the following implementations of said interface: ExceptionTask, Robo\Collection\Collection, Robo\Collection\CollectionBuilder, Robo\Collection\CompletionWrapper, Robo\Collection\TaskForEach, Robo\Task\ApiGen\ApiGen, Robo\Task\Archive\Extract, Robo\Task\Archive\Pack, Robo\Task\Assets\CssPreprocessor, Robo\Task\Assets\ImageMinify, Robo\Task\Assets\Less, Robo\Task\Assets\Minify, Robo\Task\Assets\Scss, Robo\Task\BaseTask, Robo\Task\Base\Exec, Robo\Task\Base\ExecStack, Robo\Task\Base\ParallelExec, Robo\Task\Base\SymfonyCommand, Robo\Task\Base\Watch, Robo\Task\Bower\Base, Robo\Task\Bower\Install, Robo\Task\Bower\Update, Robo\Task\CommandStack, Robo\Task\Composer\Base, Robo\Task\Composer\DumpAutoload, Robo\Task\Composer\Install, Robo\Task\Composer\Remove, Robo\Task\Composer\Update, Robo\Task\Composer\Validate, Robo\Task\Development\Changelog, Robo\Task\Development\GenerateMarkdownDoc, Robo\Task\Development\GenerateTask, Robo\Task\Development\GitHub, Robo\Task\Development\GitHubRelease, Robo\Task\Development\OpenBrowser, Robo\Task\Development\PackPhar, Robo\Task\Development\PhpServer, Robo\Task\Docker\Base, Robo\Task\Docker\Build, Robo\Task\Docker\Commit, Robo\Task\Docker\Exec, Robo\Task\Docker\Pull, Robo\Task\Docker\Remove, Robo\Task\Docker\Run, Robo\Task\Docker\Start, Robo\Task\Docker\Stop, Robo\Task\File\Concat, Robo\Task\File\Replace, Robo\Task\File\TmpFile, Robo\Task\File\Write, Robo\Task\Filesystem\BaseDir, Robo\Task\Filesystem\CleanDir, Robo\Task\Filesystem\CopyDir, Robo\Task\Filesystem\DeleteDir, Robo\Task\Filesystem\FilesystemStack, Robo\Task\Filesystem\FlattenDir, Robo\Task\Filesystem\MirrorDir, Robo\Task\Filesystem\TmpDir, Robo\Task\Filesystem\WorkDir, Robo\Task\Gulp\Base, Robo\Task\Gulp\Run, Robo\Task\Npm\Base, Robo\Task\Npm\Install, Robo\Task\Npm\Update, Robo\Task\Remote\Rsync, Robo\Task\Remote\Ssh, Robo\Task\Simulator, Robo\Task\StackBasedTask, Robo\Task\Testing\Atoum, Robo\Task\Testing\Behat, Robo\Task\Testing\Codecept, Robo\Task\Testing\PHPUnit, Robo\Task\Testing\Phpspec, Robo\Task\Vcs\GitStack, Robo\Task\Vcs\HgStack, Robo\Task\Vcs\SvnStack, TestedRoboTask, unit\CollectionTestTask, unit\ConfigurationTestTaskA, unit\ConfigurationTestTaskB, unit\CountingTask.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
267
            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...
268
        }
269
        parent::setVerbosityThreshold($verbosityThreshold);
270
        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...
271
    }
272
273
274
    /**
275
     * Return the current task for this collection builder.
276
     * TODO: Not needed?
277
     *
278
     * @return \Robo\Contract\TaskInterface
279
     */
280
    public function getCollectionBuilderCurrentTask()
281
    {
282
        return $this->currentTask;
283
    }
284
285
    /**
286
     * Create a new builder with its own task collection
287
     *
288
     * @return CollectionBuilder
289
     */
290
    public function newBuilder()
291
    {
292
        $collectionBuilder = new self($this->commandFile);
293
        $collectionBuilder->inflect($this);
294
        $collectionBuilder->simulated($this->isSimulated());
295
        $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
296
297
        return $collectionBuilder;
298
    }
299
300
    /**
301
     * Calling the task builder with methods of the current
302
     * task calls through to that method of the task.
303
     *
304
     * There is extra complexity in this function that could be
305
     * simplified if we attached the 'LoadAllTasks' and custom tasks
306
     * to the collection builder instead of the RoboFile.  While that
307
     * change would be a better design overall, it would require that
308
     * the user do a lot more work to set up and use custom tasks.
309
     * We therefore take on some additional complexity here in order
310
     * to allow users to maintain their tasks in their RoboFile, which
311
     * is much more convenient.
312
     *
313
     * Calls to $this->collectionBuilder()->taskFoo() cannot be made
314
     * directly because all of the task methods are protected.  These
315
     * calls will therefore end up here.  If the method name begins
316
     * with 'task', then it is eligible to be used with the builder.
317
     *
318
     * When we call getBuiltTask, below, it will use the builder attached
319
     * to the commandfile to build the task. However, this is not what we
320
     * want:  the task needs to be built from THIS collection builder, so that
321
     * it will be affected by whatever state is active in this builder.
322
     * To do this, we have two choices: 1) save and restore the builder
323
     * in the commandfile, or 2) clone the commandfile and set this builder
324
     * on the copy. 1) is vulnerable to failure in multithreaded environments
325
     * (currently not supported), while 2) might cause confusion if there
326
     * is shared state maintained in the commandfile, which is in the
327
     * domain of the user.
328
     *
329
     * Note that even though we are setting up the commandFile to
330
     * use this builder, getBuiltTask always creates a new builder
331
     * (which is constructed using all of the settings from the
332
     * commandFile's builder), and the new task is added to that.
333
     * We therefore need to transfer the newly built task into this
334
     * builder. The temporary builder is discarded.
335
     *
336
     * @param string $fn
337
     * @param array $args
338
     *
339
     * @return $this|mixed
340
     */
341
    public function __call($fn, $args)
342
    {
343
        if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
344
            $saveBuilder = $this->commandFile->getBuilder();
345
            $this->commandFile->setBuilder($this);
346
            $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
347
            $this->commandFile->setBuilder($saveBuilder);
348
            if (!$temporaryBuilder) {
349
                throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
350
            }
351
            $temporaryBuilder->getCollection()->transferTasks($this);
352
            return $this;
353
        }
354
        if (!isset($this->currentTask)) {
355
            throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
356
        }
357
        // If the method called is a method of the current task,
358
        // then call through to the current task's setter method.
359
        $result = call_user_func_array([$this->currentTask, $fn], $args);
360
361
        // If something other than a setter method is called, then return its result.
362
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
363
        if (isset($result) && ($result !== $currentTask)) {
364
            return $result;
365
        }
366
367
        return $this;
368
    }
369
370
    /**
371
     * Construct the desired task and add it to this builder.
372
     *
373
     * @param string|object $name
374
     * @param array $args
375
     *
376
     * @return \Robo\Collection\CollectionBuilder
377
     */
378
    public function build($name, $args)
379
    {
380
        $reflection = new ReflectionClass($name);
381
        $task = $reflection->newInstanceArgs($args);
382
        if (!$task) {
383
            throw new RuntimeException("Can not construct task $name");
384
        }
385
        $task = $this->fixTask($task, $args);
386
        return $this->addTaskToCollection($task);
387
    }
388
389
    /**
390
     * @param InflectionInterface $task
391
     * @param array $args
392
     *
393
     * @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
394
     */
395
    protected function fixTask($task, $args)
396
    {
397
        if ($task instanceof InflectionInterface) {
398
            $task->inflect($this);
399
        }
400
        if ($task instanceof BuilderAwareInterface) {
401
            $task->setBuilder($this);
402
        }
403
        if ($task instanceof VerbosityThresholdInterface) {
404
            $task->setVerbosityThreshold($this->verbosityThreshold());
405
        }
406
407
        // Do not wrap our wrappers.
408
        if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
409
            return $task;
410
        }
411
412
        // Remember whether or not this is a task before
413
        // it gets wrapped in any decorator.
414
        $isTask = $task instanceof TaskInterface;
415
        $isCollection = $task instanceof NestedCollectionInterface;
416
417
        // If the task implements CompletionInterface, ensure
418
        // that its 'complete' method is called when the application
419
        // terminates -- but only if its 'run' method is called
420
        // first.  If the task is added to a collection, then the
421
        // task will be unwrapped via its `original` method, and
422
        // it will be re-wrapped with a new completion wrapper for
423
        // its new collection.
424
        if ($task instanceof CompletionInterface) {
425
            $task = new CompletionWrapper(Temporary::getCollection(), $task);
426
        }
427
428
        // If we are in simulated mode, then wrap any task in
429
        // a TaskSimulator.
430
        if ($isTask && !$isCollection && ($this->isSimulated())) {
431
            $task = new \Robo\Task\Simulator($task, $args);
0 ignored issues
show
Documentation introduced by
$task is of type object<Robo\Contract\InflectionInterface>, but the function expects a object<Robo\Contract\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...
432
            $task->inflect($this);
433
        }
434
435
        return $task;
436
    }
437
438
    /**
439
     * When we run the collection builder, run everything in the collection.
440
     *
441
     * @return \Robo\Result
442
     */
443
    public function run()
444
    {
445
        $this->startTimer();
446
        $result = $this->runTasks();
447
        $this->stopTimer();
448
        $result['time'] = $this->getExecutionTime();
449
        return $result;
450
    }
451
452
    /**
453
     * If there is a single task, run it; if there is a collection, run
454
     * all of its tasks.
455
     *
456
     * @return \Robo\Result
457
     */
458
    protected function runTasks()
459
    {
460
        if (!$this->collection && $this->currentTask) {
461
            return $this->currentTask->run();
462
        }
463
        return $this->getCollection()->run();
464
    }
465
466
    /**
467
     * @return string
468
     */
469
    public function getCommand()
470
    {
471
        if (!$this->collection && $this->currentTask) {
472
            $task = $this->currentTask;
473
            $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
474
            if ($task instanceof CommandInterface) {
475
                return $task->getCommand();
476
            }
477
        }
478
479
        return $this->getCollection()->getCommand();
480
    }
481
482
    /**
483
     * @return \Robo\Collection\Collection
484
     */
485
    public function original()
486
    {
487
        return $this->getCollection();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getCollection(); (Robo\Collection\CollectionInterface) is incompatible with the return type declared by the interface Robo\Contract\WrappedTaskInterface::original of type Robo\Contract\TaskInterface.

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...
488
    }
489
490
    /**
491
     * Return the collection of tasks associated with this builder.
492
     *
493
     * @return CollectionInterface
494
     */
495
    public function getCollection()
496
    {
497
        if (!isset($this->collection)) {
498
            $this->collection = new Collection();
499
            $this->collection->inflect($this);
500
            $this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
501
502
            if (isset($this->currentTask)) {
503
                $this->collection->add($this->currentTask);
504
            }
505
        }
506
        return $this->collection;
507
    }
508
}
509