Completed
Pull Request — master (#580)
by Greg
03:19
created

CollectionBuilder::defer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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