CollectionBuilder   F
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 524
Duplicated Lines 2.1 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 66
lcom 1
cbo 15
dl 11
loc 524
rs 3.12
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A create() 11 11 1
A simulated() 0 5 1
A isSimulated() 0 7 2
A tmpDir() 0 9 1
A workDir() 0 5 1
A addTask() 0 5 1
A addCode() 0 5 1
A addTaskList() 0 5 1
A rollback() 0 7 1
A rollbackCode() 0 5 1
A completion() 0 5 1
A completionCode() 0 5 1
A progressMessage() 0 5 1
A setParentCollection() 0 5 1
A addTaskToCollection() 0 17 5
A getState() 0 5 1
A storeState() 0 4 1
A deferTaskConfiguration() 0 4 1
A defer() 0 4 1
A setVerbosityThreshold() 0 10 3
A getCollectionBuilderCurrentTask() 0 4 1
A newBuilder() 0 10 1
B __call() 0 28 8
A build() 0 11 2
B fixTask() 0 42 10
A configureTask() 0 14 1
A run() 0 9 1
A runTasks() 0 8 3
A getCommand() 0 12 5
A original() 0 4 1
A getCollection() 0 14 3
A callCollectionStateFunction() 0 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CollectionBuilder 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 CollectionBuilder, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

Loading history...
Unused Code introduced by Greg Anderson
The parameter $source is not used and could be removed.

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

Loading history...
275
    {
276
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
277
    }
278
279
    public function deferTaskConfiguration($functionName, $stateKey)
0 ignored issues
show
Unused Code introduced by Greg Anderson
The parameter $functionName is not used and could be removed.

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

Loading history...
Unused Code introduced by Greg Anderson
The parameter $stateKey is not used and could be removed.

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

Loading history...
280
    {
281
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
282
    }
283
284
    public function defer($callback)
0 ignored issues
show
Unused Code introduced by Greg Anderson
The parameter $callback is not used and could be removed.

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

Loading history...
285
    {
286
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
287
    }
288
289
    protected function callCollectionStateFunction($functionName, $args)
290
    {
291
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
292
293
        array_unshift($args, $currentTask);
294
        $collection = $this->getCollection();
295
        $fn = [$collection, $functionName];
296
297
        call_user_func_array($fn, $args);
298
        return $this;
299
    }
300
301
    public function setVerbosityThreshold($verbosityThreshold)
302
    {
303
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
304
        if ($currentTask) {
305
            $currentTask->setVerbosityThreshold($verbosityThreshold);
0 ignored issues
show
Bug introduced by Greg Anderson
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: RoboExample\Robo\Plugin\Commands\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\CollectionTestTask, Robo\Task\CommandStack, Robo\Task\Composer\Base, Robo\Task\Composer\Config, Robo\Task\Composer\CreateProject, Robo\Task\Composer\DumpAutoload, Robo\Task\Composer\Init, Robo\Task\Composer\Install, Robo\Task\Composer\Remove, Robo\Task\Composer\RequireDependency, Robo\Task\Composer\Update, Robo\Task\Composer\Validate, Robo\Task\CountingTask, 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\ValueProviderTask, Robo\Task\Vcs\GitStack, Robo\Task\Vcs\HgStack, Robo\Task\Vcs\SvnStack, TestedRoboTask, unit\ConfigurationTestTaskA, unit\ConfigurationTestTaskB.

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...
306
            return $this;
0 ignored issues
show
Bug Best Practice introduced by Greg Anderson
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...
307
        }
308
        parent::setVerbosityThreshold($verbosityThreshold);
309
        return $this;
0 ignored issues
show
Bug Best Practice introduced by Greg Anderson
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...
310
    }
311
312
313
    /**
314
     * Return the current task for this collection builder.
315
     * TODO: Not needed?
316
     *
317
     * @return \Robo\Contract\TaskInterface
318
     */
319
    public function getCollectionBuilderCurrentTask()
320
    {
321
        return $this->currentTask;
322
    }
323
324
    /**
325
     * Create a new builder with its own task collection
326
     *
327
     * @return CollectionBuilder
328
     */
329
    public function newBuilder()
330
    {
331
        $collectionBuilder = new self($this->commandFile);
332
        $collectionBuilder->inflect($this);
333
        $collectionBuilder->simulated($this->isSimulated());
334
        $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
335
        $collectionBuilder->setState($this->getState());
336
337
        return $collectionBuilder;
338
    }
339
340
    /**
341
     * Calling the task builder with methods of the current
342
     * task calls through to that method of the task.
343
     *
344
     * There is extra complexity in this function that could be
345
     * simplified if we attached the 'LoadAllTasks' and custom tasks
346
     * to the collection builder instead of the RoboFile.  While that
347
     * change would be a better design overall, it would require that
348
     * the user do a lot more work to set up and use custom tasks.
349
     * We therefore take on some additional complexity here in order
350
     * to allow users to maintain their tasks in their RoboFile, which
351
     * is much more convenient.
352
     *
353
     * Calls to $this->collectionBuilder()->taskFoo() cannot be made
354
     * directly because all of the task methods are protected.  These
355
     * calls will therefore end up here.  If the method name begins
356
     * with 'task', then it is eligible to be used with the builder.
357
     *
358
     * When we call getBuiltTask, below, it will use the builder attached
359
     * to the commandfile to build the task. However, this is not what we
360
     * want:  the task needs to be built from THIS collection builder, so that
361
     * it will be affected by whatever state is active in this builder.
362
     * To do this, we have two choices: 1) save and restore the builder
363
     * in the commandfile, or 2) clone the commandfile and set this builder
364
     * on the copy. 1) is vulnerable to failure in multithreaded environments
365
     * (currently not supported), while 2) might cause confusion if there
366
     * is shared state maintained in the commandfile, which is in the
367
     * domain of the user.
368
     *
369
     * Note that even though we are setting up the commandFile to
370
     * use this builder, getBuiltTask always creates a new builder
371
     * (which is constructed using all of the settings from the
372
     * commandFile's builder), and the new task is added to that.
373
     * We therefore need to transfer the newly built task into this
374
     * builder. The temporary builder is discarded.
375
     *
376
     * @param string $fn
377
     * @param array $args
378
     *
379
     * @return $this|mixed
380
     */
381
    public function __call($fn, $args)
382
    {
383
        if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
384
            $saveBuilder = $this->commandFile->getBuilder();
385
            $this->commandFile->setBuilder($this);
386
            $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
387
            $this->commandFile->setBuilder($saveBuilder);
388
            if (!$temporaryBuilder) {
389
                throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
390
            }
391
            $temporaryBuilder->getCollection()->transferTasks($this);
392
            return $this;
393
        }
394
        if (!isset($this->currentTask)) {
395
            throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
396
        }
397
        // If the method called is a method of the current task,
398
        // then call through to the current task's setter method.
399
        $result = call_user_func_array([$this->currentTask, $fn], $args);
400
401
        // If something other than a setter method is called, then return its result.
402
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
403
        if (isset($result) && ($result !== $currentTask)) {
404
            return $result;
405
        }
406
407
        return $this;
408
    }
409
410
    /**
411
     * Construct the desired task and add it to this builder.
412
     *
413
     * @param string|object $name
414
     * @param array $args
415
     *
416
     * @return \Robo\Collection\CollectionBuilder
417
     */
418
    public function build($name, $args)
419
    {
420
        $reflection = new ReflectionClass($name);
421
        $task = $reflection->newInstanceArgs($args);
422
        if (!$task) {
423
            throw new RuntimeException("Can not construct task $name");
424
        }
425
        $task = $this->fixTask($task, $args);
426
        $this->configureTask($name, $task);
427
        return $this->addTaskToCollection($task);
428
    }
429
430
    /**
431
     * @param InflectionInterface $task
432
     * @param array $args
433
     *
434
     * @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
435
     */
436
    protected function fixTask($task, $args)
437
    {
438
        if ($task instanceof InflectionInterface) {
439
            $task->inflect($this);
440
        }
441
        if ($task instanceof BuilderAwareInterface) {
442
            $task->setBuilder($this);
443
        }
444
        if ($task instanceof VerbosityThresholdInterface) {
445
            $task->setVerbosityThreshold($this->verbosityThreshold());
446
        }
447
448
        // Do not wrap our wrappers.
449
        if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
450
            return $task;
451
        }
452
453
        // Remember whether or not this is a task before
454
        // it gets wrapped in any decorator.
455
        $isTask = $task instanceof TaskInterface;
456
        $isCollection = $task instanceof NestedCollectionInterface;
457
458
        // If the task implements CompletionInterface, ensure
459
        // that its 'complete' method is called when the application
460
        // terminates -- but only if its 'run' method is called
461
        // first.  If the task is added to a collection, then the
462
        // task will be unwrapped via its `original` method, and
463
        // it will be re-wrapped with a new completion wrapper for
464
        // its new collection.
465
        if ($task instanceof CompletionInterface) {
466
            $task = new CompletionWrapper(Temporary::getCollection(), $task);
467
        }
468
469
        // If we are in simulated mode, then wrap any task in
470
        // a TaskSimulator.
471
        if ($isTask && !$isCollection && ($this->isSimulated())) {
472
            $task = new \Robo\Task\Simulator($task, $args);
0 ignored issues
show
Documentation introduced by Greg Anderson
$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...
473
            $task->inflect($this);
474
        }
475
476
        return $task;
477
    }
478
479
    /**
480
     * Check to see if there are any setter methods defined in configuration
481
     * for this task.
482
     */
483
    protected function configureTask($taskClass, $task)
484
    {
485
        $taskClass = static::configClassIdentifier($taskClass);
486
        $configurationApplier = new ConfigForSetters($this->getConfig(), $taskClass, 'task.');
487
        $configurationApplier->apply($task, 'settings');
488
489
        // TODO: If we counted each instance of $taskClass that was called from
490
        // this builder, then we could also apply configuration from
491
        // "task.{$taskClass}[$N].settings"
492
493
        // TODO: If the builder knew what the current command name was,
494
        // then we could also search for task configuration under
495
        // command-specific keys such as "command.{$commandname}.task.{$taskClass}.settings".
496
    }
497
498
    /**
499
     * When we run the collection builder, run everything in the collection.
500
     *
501
     * @return \Robo\Result
502
     */
503
    public function run()
504
    {
505
        $this->startTimer();
506
        $result = $this->runTasks();
507
        $this->stopTimer();
508
        $result['time'] = $this->getExecutionTime();
509
        $result->mergeData($this->getState()->getData());
510
        return $result;
511
    }
512
513
    /**
514
     * If there is a single task, run it; if there is a collection, run
515
     * all of its tasks.
516
     *
517
     * @return \Robo\Result
518
     */
519
    protected function runTasks()
520
    {
521
        if (!$this->collection && $this->currentTask) {
522
            $result = $this->currentTask->run();
523
            return Result::ensureResult($this->currentTask, $result);
524
        }
525
        return $this->getCollection()->run();
526
    }
527
528
    /**
529
     * @return string
530
     */
531
    public function getCommand()
532
    {
533
        if (!$this->collection && $this->currentTask) {
534
            $task = $this->currentTask;
535
            $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
536
            if ($task instanceof CommandInterface) {
537
                return $task->getCommand();
538
            }
539
        }
540
541
        return $this->getCollection()->getCommand();
542
    }
543
544
    /**
545
     * @return \Robo\Collection\Collection
546
     */
547
    public function original()
548
    {
549
        return $this->getCollection();
0 ignored issues
show
Bug Best Practice introduced by Greg Anderson
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...
550
    }
551
552
    /**
553
     * Return the collection of tasks associated with this builder.
554
     *
555
     * @return CollectionInterface
556
     */
557
    public function getCollection()
558
    {
559
        if (!isset($this->collection)) {
560
            $this->collection = new Collection();
561
            $this->collection->inflect($this);
562
            $this->collection->setState($this->getState());
563
            $this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
564
565
            if (isset($this->currentTask)) {
566
                $this->collection->add($this->currentTask);
567
            }
568
        }
569
        return $this->collection;
570
    }
571
}
572