CollectionBuilder   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 592
Duplicated Lines 1.86 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 15
dl 11
loc 592
rs 3.04
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 11 11 1
A getState() 0 5 1
A storeState() 0 4 1
A deferTaskConfiguration() 0 4 1
A defer() 0 4 1
A callCollectionStateFunction() 0 11 2
A callCollectionStateFuntion() 0 4 1
A setVerbosityThreshold() 0 10 3
A configureTask() 0 14 1
A __construct() 0 5 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 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 run() 0 9 1
A runTasks() 0 8 3
A getCommand() 0 12 5
A original() 0 4 1
A getCollection() 0 14 3

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
3
namespace Robo\Collection;
4
5
use Consolidation\Config\Inject\ConfigForSetters;
6
use Robo\Config\Config;
7
use Psr\Log\LogLevel;
8
use Robo\Contract\InflectionInterface;
9
use Robo\Contract\TaskInterface;
10
use Robo\Contract\CompletionInterface;
11
use Robo\Contract\WrappedTaskInterface;
12
use Robo\Task\Simulator;
13
use ReflectionClass;
14
use Robo\Task\BaseTask;
15
use Robo\Contract\BuilderAwareInterface;
16
use Robo\Contract\CommandInterface;
17
use Robo\Contract\VerbosityThresholdInterface;
18
use Robo\State\StateAwareInterface;
19
use Robo\State\StateAwareTrait;
20
use Robo\Result;
21
22
/**
23
 * Creates a collection, and adds tasks to it.  The collection builder
24
 * offers a streamlined chained-initialization mechanism for easily
25
 * creating task groups.  Facilities for creating working and temporary
26
 * directories are also provided.
27
 *
28
 * ``` php
29
 * <?php
30
 * $result = $this->collectionBuilder()
31
 *   ->taskFilesystemStack()
32
 *     ->mkdir('g')
33
 *     ->touch('g/g.txt')
34
 *   ->rollback(
35
 *     $this->taskDeleteDir('g')
36
 *   )
37
 *   ->taskFilesystemStack()
38
 *     ->mkdir('g/h')
39
 *     ->touch('g/h/h.txt')
40
 *   ->taskFilesystemStack()
41
 *     ->mkdir('g/h/i/c')
42
 *     ->touch('g/h/i/i.txt')
43
 *   ->run()
44
 * ?>
45
 *
46
 * In the example above, the `taskDeleteDir` will be called if
47
 * ```
48
 */
49
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface, StateAwareInterface
50
{
51
    use StateAwareTrait;
52
53
    /**
54
     * @var \Robo\Tasks
55
     */
56
    protected $commandFile;
57
58
    /**
59
     * @var \Robo\Collection\CollectionInterface
60
     */
61
    protected $collection;
62
63
    /**
64
     * @var \Robo\Contract\TaskInterface
65
     */
66
    protected $currentTask;
67
68
    /**
69
     * @var bool
70
     */
71
    protected $simulated;
72
73
    /**
74
     * @param \Robo\Tasks $commandFile
75
     */
76
    public function __construct($commandFile)
77
    {
78
        $this->commandFile = $commandFile;
79
        $this->resetState();
80
    }
81
82
    /**
83
     * @param \League\Container\ContainerInterface $container
84
     * @param \Robo\Tasks $commandFile
85
     *
86
     * @return static
87
     */
88 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...
89
    {
90
        $builder = new self($commandFile);
91
92
        $builder->setLogger($container->get('logger'));
93
        $builder->setProgressIndicator($container->get('progressIndicator'));
94
        $builder->setConfig($container->get('config'));
95
        $builder->setOutputAdapter($container->get('outputAdapter'));
96
97
        return $builder;
98
    }
99
100
    /**
101
     * @param bool $simulated
102
     *
103
     * @return $this
104
     */
105
    public function simulated($simulated = true)
106
    {
107
        $this->simulated = $simulated;
108
        return $this;
109
    }
110
111
    /**
112
     * @return bool
113
     */
114
    public function isSimulated()
115
    {
116
        if (!isset($this->simulated)) {
117
            $this->simulated = $this->getConfig()->get(Config::SIMULATE);
118
        }
119
        return $this->simulated;
120
    }
121
122
    /**
123
     * Create a temporary directory to work in. When the collection
124
     * completes or rolls back, the temporary directory will be deleted.
125
     * Returns the path to the location where the directory will be
126
     * created.
127
     *
128
     * @param string $prefix
129
     * @param string $base
130
     * @param bool $includeRandomPart
131
     *
132
     * @return string
133
     */
134
    public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
135
    {
136
        // n.b. Any task that the builder is asked to create is
137
        // automatically added to the builder's collection, and
138
        // wrapped in the builder object. Therefore, the result
139
        // of any call to `taskFoo()` from within the builder will
140
        // always be `$this`.
141
        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...
142
    }
143
144
    /**
145
     * Create a working directory to hold results. A temporary directory
146
     * is first created to hold the intermediate results.  After the
147
     * builder finishes, the work directory is moved into its final location;
148
     * any results already in place will be moved out of the way and
149
     * then deleted.
150
     *
151
     * @param string $finalDestination
152
     *   The path where the working directory will be moved once the task
153
     *   collection completes.
154
     *
155
     * @return string
156
     */
157
    public function workDir($finalDestination)
158
    {
159
        // Creating the work dir task in this context adds it to our task collection.
160
        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...
161
    }
162
163
    /**
164
     * @return $this
165
     */
166
    public function addTask(TaskInterface $task)
167
    {
168
        $this->getCollection()->add($task);
169
        return $this;
170
    }
171
172
  /**
173
   * Add arbitrary code to execute as a task.
174
   *
175
   * @see \Robo\Collection\CollectionInterface::addCode
176
   *
177
   * @param callable $code
178
   * @param int|string $name
179
   *
180
   * @return $this
181
   */
182
    public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
183
    {
184
        $this->getCollection()->addCode($code, $name);
185
        return $this;
186
    }
187
188
    /**
189
     * Add a list of tasks to our task collection.
190
     *
191
     * @param \Robo\Contract\TaskInterface[] $tasks
192
     *   An array of tasks to run with rollback protection
193
     *
194
     * @return $this
195
     */
196
    public function addTaskList(array $tasks)
197
    {
198
        $this->getCollection()->addTaskList($tasks);
199
        return $this;
200
    }
201
202
    /**
203
     * @return $this
204
     */
205
    public function rollback(TaskInterface $task)
206
    {
207
        // Ensure that we have a collection if we are going to add
208
        // a rollback function.
209
        $this->getCollection()->rollback($task);
210
        return $this;
211
    }
212
213
    /**
214
     * @return $this
215
     */
216
    public function rollbackCode(callable $rollbackCode)
217
    {
218
        $this->getCollection()->rollbackCode($rollbackCode);
219
        return $this;
220
    }
221
222
    /**
223
     * @return $this
224
     */
225
    public function completion(TaskInterface $task)
226
    {
227
        $this->getCollection()->completion($task);
228
        return $this;
229
    }
230
231
    /**
232
     * @return $this
233
     */
234
    public function completionCode(callable $completionCode)
235
    {
236
        $this->getCollection()->completionCode($completionCode);
237
        return $this;
238
    }
239
240
    /**
241
     * @param string $text
242
     * @param array $context
243
     * @param string $level
244
     *
245
     * @return $this
246
     */
247
    public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
248
    {
249
        $this->getCollection()->progressMessage($text, $context, $level);
250
        return $this;
251
    }
252
253
    /**
254
     * @return $this
255
     */
256
    public function setParentCollection(NestedCollectionInterface $parentCollection)
257
    {
258
        $this->getCollection()->setParentCollection($parentCollection);
259
        return $this;
260
    }
261
262
    /**
263
     * Called by the factory method of each task; adds the current
264
     * task to the task builder.
265
     *
266
     * TODO: protected
267
     *
268
     * @param \Robo\Contract\TaskInterface $task
269
     *
270
     * @return $this
271
     */
272
    public function addTaskToCollection($task)
273
    {
274
        // Postpone creation of the collection until the second time
275
        // we are called. At that time, $this->currentTask will already
276
        // be populated.  We call 'getCollection()' so that it will
277
        // create the collection and add the current task to it.
278
        // Note, however, that if our only tasks implements NestedCollectionInterface,
279
        // then we should force this builder to use a collection.
280
        if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
281
            $this->getCollection();
282
        }
283
        $this->currentTask = $task;
284
        if ($this->collection) {
285
            $this->collection->add($task);
286
        }
287
        return $this;
288
    }
289
290
    /**
291
     * @return \Robo\State\Data
292
     */
293
    public function getState()
294
    {
295
        $collection = $this->getCollection();
296
        return $collection->getState();
297
    }
298
299
    /**
300
     * @param int|string $key
301
     * @param mixed $source
302
     *
303
     * @return $this
304
     */
305
    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...
306
    {
307
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
308
    }
309
310
    /**
311
     * @param string $functionName
312
     * @param int|string $stateKey
313
     *
314
     * @return $this
315
     */
316
    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...
317
    {
318
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
319
    }
320
321
    /**
322
     * @param callable$callback
323
     *
324
     * @return $this
325
     */
326
    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...
327
    {
328
        return $this->callCollectionStateFunction(__FUNCTION__, func_get_args());
329
    }
330
331
    /**
332
     * @param string $functionName
333
     * @param array $args
334
     *
335
     * @return $this
336
     */
337
    protected function callCollectionStateFunction($functionName, $args)
338
    {
339
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
340
341
        array_unshift($args, $currentTask);
342
        $collection = $this->getCollection();
343
        $fn = [$collection, $functionName];
344
345
        call_user_func_array($fn, $args);
346
        return $this;
347
    }
348
349
    /**
350
     * @param string $functionName
351
     * @param array $args
352
     *
353
     * @return $this
354
     *
355
     * @deprecated Use ::callCollectionStateFunction() instead.
356
     */
357
    protected function callCollectionStateFuntion($functionName, $args)
358
    {
359
        return $this->callCollectionStateFunction($functionName, $args);
360
    }
361
362
    /**
363
     * @param int $verbosityThreshold
364
     *
365
     * @return $this
366
     */
367
    public function setVerbosityThreshold($verbosityThreshold)
368
    {
369
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
370
        if ($currentTask) {
371
            $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...
372
            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...
373
        }
374
        parent::setVerbosityThreshold($verbosityThreshold);
375
        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...
376
    }
377
378
379
    /**
380
     * Return the current task for this collection builder.
381
     * TODO: Not needed?
382
     *
383
     * @return \Robo\Contract\TaskInterface
384
     */
385
    public function getCollectionBuilderCurrentTask()
386
    {
387
        return $this->currentTask;
388
    }
389
390
    /**
391
     * Create a new builder with its own task collection
392
     *
393
     * @return \Robo\Collection\CollectionBuilder
394
     */
395
    public function newBuilder()
396
    {
397
        $collectionBuilder = new self($this->commandFile);
398
        $collectionBuilder->inflect($this);
399
        $collectionBuilder->simulated($this->isSimulated());
400
        $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
401
        $collectionBuilder->setState($this->getState());
402
403
        return $collectionBuilder;
404
    }
405
406
    /**
407
     * Calling the task builder with methods of the current
408
     * task calls through to that method of the task.
409
     *
410
     * There is extra complexity in this function that could be
411
     * simplified if we attached the 'LoadAllTasks' and custom tasks
412
     * to the collection builder instead of the RoboFile.  While that
413
     * change would be a better design overall, it would require that
414
     * the user do a lot more work to set up and use custom tasks.
415
     * We therefore take on some additional complexity here in order
416
     * to allow users to maintain their tasks in their RoboFile, which
417
     * is much more convenient.
418
     *
419
     * Calls to $this->collectionBuilder()->taskFoo() cannot be made
420
     * directly because all of the task methods are protected.  These
421
     * calls will therefore end up here.  If the method name begins
422
     * with 'task', then it is eligible to be used with the builder.
423
     *
424
     * When we call getBuiltTask, below, it will use the builder attached
425
     * to the commandfile to build the task. However, this is not what we
426
     * want:  the task needs to be built from THIS collection builder, so that
427
     * it will be affected by whatever state is active in this builder.
428
     * To do this, we have two choices: 1) save and restore the builder
429
     * in the commandfile, or 2) clone the commandfile and set this builder
430
     * on the copy. 1) is vulnerable to failure in multithreaded environments
431
     * (currently not supported), while 2) might cause confusion if there
432
     * is shared state maintained in the commandfile, which is in the
433
     * domain of the user.
434
     *
435
     * Note that even though we are setting up the commandFile to
436
     * use this builder, getBuiltTask always creates a new builder
437
     * (which is constructed using all of the settings from the
438
     * commandFile's builder), and the new task is added to that.
439
     * We therefore need to transfer the newly built task into this
440
     * builder. The temporary builder is discarded.
441
     *
442
     * @param string $fn
443
     * @param array $args
444
     *
445
     * @return $this|mixed
446
     */
447
    public function __call($fn, $args)
448
    {
449
        if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
450
            $saveBuilder = $this->commandFile->getBuilder();
451
            $this->commandFile->setBuilder($this);
452
            $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
453
            $this->commandFile->setBuilder($saveBuilder);
454
            if (!$temporaryBuilder) {
455
                throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
456
            }
457
            $temporaryBuilder->getCollection()->transferTasks($this);
458
            return $this;
459
        }
460
        if (!isset($this->currentTask)) {
461
            throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
462
        }
463
        // If the method called is a method of the current task,
464
        // then call through to the current task's setter method.
465
        $result = call_user_func_array([$this->currentTask, $fn], $args);
466
467
        // If something other than a setter method is called, then return its result.
468
        $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
469
        if (isset($result) && ($result !== $currentTask)) {
470
            return $result;
471
        }
472
473
        return $this;
474
    }
475
476
    /**
477
     * Construct the desired task and add it to this builder.
478
     *
479
     * @param string|object $name
480
     * @param array $args
481
     *
482
     * @return $this
483
     */
484
    public function build($name, $args)
485
    {
486
        $reflection = new ReflectionClass($name);
487
        $task = $reflection->newInstanceArgs($args);
488
        if (!$task) {
489
            throw new \RuntimeException("Can not construct task $name");
490
        }
491
        $task = $this->fixTask($task, $args);
492
        $this->configureTask($name, $task);
0 ignored issues
show
Bug introduced by Greg Anderson
It seems like $name defined by parameter $name on line 484 can also be of type object; however, Robo\Collection\CollectionBuilder::configureTask() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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