Completed
Push — master ( 76ad1f...386b33 )
by Greg
05:49
created

Runner::run()   F

Complexity

Conditions 14
Paths 1728

Size

Total Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 65
rs 2.1
c 0
b 0
f 0
cc 14
nc 1728
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Robo;
4
5
use Composer\Autoload\ClassLoader;
6
use Symfony\Component\Console\Input\ArgvInput;
7
use Symfony\Component\Console\Input\StringInput;
8
use Robo\Contract\BuilderAwareInterface;
9
use Robo\Collection\CollectionBuilder;
10
use Robo\Common\IO;
11
use Robo\Exception\TaskExitException;
12
use League\Container\ContainerAwareInterface;
13
use League\Container\ContainerAwareTrait;
14
use Consolidation\Config\Util\EnvConfig;
15
16
class Runner implements ContainerAwareInterface
17
{
18
    use IO;
19
    use ContainerAwareTrait;
20
21
    const ROBOCLASS = 'RoboFile';
22
    const ROBOFILE = 'RoboFile.php';
23
24
    /**
25
     * @var string
26
     */
27
    protected $roboClass;
28
29
    /**
30
     * @var string
31
     */
32
    protected $roboFile;
33
34
    /**
35
     * Working dir of Robo.
36
     *
37
     * @var string
38
     */
39
    protected $dir;
40
41
    /**
42
     * @var string[]
43
     */
44
    protected $errorConditions = [];
45
46
    /**
47
     * GitHub Repo for SelfUpdate.
48
     *
49
     * @var string
50
     */
51
    protected $selfUpdateRepository = null;
52
53
    /**
54
     * Filename to load configuration from (set to 'robo.yml' for RoboFiles).
55
     *
56
     * @var string
57
     */
58
    protected $configFilename = 'conf.yml';
59
60
    /**
61
     * @var string prefix for environment variable configuration overrides
62
     */
63
    protected $envConfigPrefix = false;
64
65
    /**
66
     * @var null|\Composer\Autoload\ClassLoader
67
     */
68
    protected $classLoader = null;
69
70
    /**
71
     * @var string
72
     */
73
    protected $relativePluginNamespace;
74
75
    /**
76
     * Class Constructor
77
     *
78
     * @param null|string $roboClass
79
     * @param null|string $roboFile
80
     */
81
    public function __construct($roboClass = null, $roboFile = null)
82
    {
83
        // set the const as class properties to allow overwriting in child classes
84
        $this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
85
        $this->roboFile  = $roboFile ? $roboFile : self::ROBOFILE;
86
        $this->dir = getcwd();
87
    }
88
89
    /**
90
     * @param string $msg
91
     * @param string $errorType
92
     */
93
    protected function errorCondition($msg, $errorType)
94
    {
95
        $this->errorConditions[$msg] = $errorType;
96
    }
97
98
    /**
99
     * @param \Symfony\Component\Console\Output\OutputInterface $output
100
     *
101
     * @return bool
102
     */
103
    protected function loadRoboFile($output)
104
    {
105
        // If we have not been provided an output object, make a temporary one.
106
        if (!$output) {
107
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
0 ignored issues
show
Unused Code introduced by
$output is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
108
        }
109
110
        // If $this->roboClass is a single class that has not already
111
        // been loaded, then we will try to obtain it from $this->roboFile.
112
        // If $this->roboClass is an array, we presume all classes requested
113
        // are available via the autoloader.
114
        if (is_array($this->roboClass) || class_exists($this->roboClass)) {
115
            return true;
116
        }
117
        if (!file_exists($this->dir)) {
118
            $this->errorCondition("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
119
            return false;
120
        }
121
122
        $realDir = realpath($this->dir);
123
124
        $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
125
        if (!file_exists($roboFilePath)) {
126
            $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
127
            $this->errorCondition("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
128
            return false;
129
        }
130
        require_once $roboFilePath;
131
132
        if (!class_exists($this->roboClass)) {
133
            $this->errorCondition("Class {$this->roboClass} was not loaded.", 'red');
134
            return false;
135
        }
136
        return true;
137
    }
138
139
    /**
140
     * @param array $argv
141
     * @param null|string $appName
142
     * @param null|string $appVersion
143
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
144
     *
145
     * @return int
146
     */
147
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
148
    {
149
        $argv = $this->shebang($argv);
150
        $argv = $this->processRoboOptions($argv);
151
        $app = null;
152
        if ($appName && $appVersion) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $appName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $appVersion of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
153
            $app = Robo::createDefaultApplication($appName, $appVersion);
154
        }
155
        $commandFiles = $this->getRoboFileCommands($output);
0 ignored issues
show
Bug introduced by
It seems like $output defined by parameter $output on line 147 can be null; however, Robo\Runner::getRoboFileCommands() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
156
        return $this->run($argv, $output, $app, $commandFiles, $this->classLoader);
0 ignored issues
show
Documentation introduced by
$argv is of type array, but the function expects a null|object<Symfony\Comp...e\Input\InputInterface>.

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...
Documentation introduced by
$commandFiles is of type null|string, but the function expects a array<integer,array>.

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...
157
    }
158
159
    /**
160
     * Get a list of locations where config files may be loaded
161
     *
162
     * @param string $userConfig
163
     *
164
     * @return string[]
165
     */
166
    protected function getConfigFilePaths($userConfig)
167
    {
168
        // Look for application config at the root of the application.
169
        // Find the root relative to this file, considering that Robo itself
170
        // might be the application, or it might be in the `vendor` directory.
171
        $roboAppConfig = dirname(__DIR__) . '/' . basename($userConfig);
172
        if (basename(dirname(__DIR__, 3)) == 'vendor') {
173
            $roboAppConfig = dirname(__DIR__, 4) . '/' . basename($userConfig);
174
        }
175
        $configFiles = [$roboAppConfig, $userConfig];
176
        if (dirname($userConfig) != '.') {
177
            $configFiles[] = basename($userConfig);
178
        }
179
        return $configFiles;
180
    }
181
182
    /**
183
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
184
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
185
     * @param null|\Robo\Application $app
186
     * @param array[] $commandFiles
187
     * @param null|ClassLoader $classLoader
188
     *
189
     * @return int
190
     */
191
    public function run($input = null, $output = null, $app = null, $commandFiles = [], $classLoader = null)
192
    {
193
        // Create default input and output objects if they were not provided
194
        if (!$input) {
195
            $input = new StringInput('');
196
        }
197
        if (is_array($input)) {
198
            $input = new ArgvInput($input);
199
        }
200
        if (!$output) {
201
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
202
        }
203
        $this->setInput($input);
204
        $this->setOutput($output);
205
206
        // If we were not provided a container, then create one
207
        if (!$this->getContainer()) {
208
            $configFiles = $this->getConfigFilePaths($this->configFilename);
209
            $config = Robo::createConfiguration($configFiles);
210
            if ($this->envConfigPrefix) {
211
                $envConfig = new EnvConfig($this->envConfigPrefix);
212
                $config->addContext('env', $envConfig);
213
            }
214
            $container = Robo::createDefaultContainer($input, $output, $app, $config, $classLoader);
215
            $this->setContainer($container);
216
            // Automatically register a shutdown function and
217
            // an error handler when we provide the container.
218
            $this->installRoboHandlers();
219
        }
220
221
        if (!$app) {
222
            $app = Robo::application();
223
        }
224
        if ($app instanceof \Robo\Application) {
225
            $app->addSelfUpdateCommand($this->getSelfUpdateRepository());
226
            if (!isset($commandFiles)) {
227
                $this->errorCondition("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
228
                $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
229
                $commandFiles = [];
230
            }
231
        }
232
233
        if (!empty($this->relativePluginNamespace)) {
234
            $commandClasses = $this->discoverCommandClasses($this->relativePluginNamespace);
235
            $commandFiles = array_merge((array)$commandFiles, $commandClasses);
236
        }
237
238
        $this->registerCommandClasses($app, $commandFiles);
239
240
        try {
241
            $statusCode = $app->run($input, $output);
242
        } catch (TaskExitException $e) {
243
            $statusCode = $e->getCode() ?: 1;
244
        }
245
246
        // If there were any error conditions in bootstrapping Robo,
247
        // print them only if the requested command did not complete
248
        // successfully.
249
        if ($statusCode) {
250
            foreach ($this->errorConditions as $msg => $color) {
251
                $this->yell($msg, 40, $color);
252
            }
253
        }
254
        return $statusCode;
255
    }
256
257
    /**
258
     * @param \Symfony\Component\Console\Output\OutputInterface $output
259
     *
260
     * @return null|string
261
     */
262
    protected function getRoboFileCommands($output)
263
    {
264
        if (!$this->loadRoboFile($output)) {
265
            return;
266
        }
267
        return $this->roboClass;
268
    }
269
270
    /**
271
     * @param \Robo\Application $app
272
     * @param array $commandClasses
273
     */
274
    public function registerCommandClasses($app, $commandClasses)
275
    {
276
        foreach ((array)$commandClasses as $commandClass) {
277
            $this->registerCommandClass($app, $commandClass);
278
        }
279
    }
280
281
    /**
282
     * @param string $relativeNamespace
283
     *
284
     * @return string[]
285
     */
286
    protected function discoverCommandClasses($relativeNamespace)
287
    {
288
        /** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
289
        $discovery = Robo::service('relativeNamespaceDiscovery');
290
        $discovery->setRelativeNamespace($relativeNamespace . '\Commands')
291
            ->setSearchPattern('/.*Commands?\.php$/');
292
        return $discovery->getClasses();
293
    }
294
295
    /**
296
     * @param \Robo\Application $app
297
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
298
     *
299
     * @return null|object
300
     */
301
    public function registerCommandClass($app, $commandClass)
302
    {
303
        $container = Robo::getContainer();
304
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
305
        if (!$roboCommandFileInstance) {
306
            return;
307
        }
308
309
        // Register commands for all of the public methods in the RoboFile.
310
        $commandFactory = $container->get('commandFactory');
311
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
312
        foreach ($commandList as $command) {
313
            $app->add($command);
314
        }
315
        return $roboCommandFileInstance;
316
    }
317
318
    /**
319
     * @param string|\Robo\Contract\BuilderAwareInterface|\League\Container\ContainerAwareInterface $commandClass
320
     *
321
     * @return null|object
322
     */
323
    protected function instantiateCommandClass($commandClass)
324
    {
325
        $container = Robo::getContainer();
326
327
        // Register the RoboFile with the container and then immediately
328
        // fetch it; this ensures that all of the inflectors will run.
329
        // If the command class is already an instantiated object, then
330
        // just use it exactly as it was provided to us.
331
        if (is_string($commandClass)) {
332
            if (!class_exists($commandClass)) {
333
                return;
334
            }
335
            $reflectionClass = new \ReflectionClass($commandClass);
336
            if ($reflectionClass->isAbstract()) {
337
                return;
338
            }
339
340
            $commandFileName = "{$commandClass}Commands";
341
            $container->share($commandFileName, $commandClass);
342
            $commandClass = $container->get($commandFileName);
343
        }
344
        // If the command class is a Builder Aware Interface, then
345
        // ensure that it has a builder.  Every command class needs
346
        // its own collection builder, as they have references to each other.
347
        if ($commandClass instanceof BuilderAwareInterface) {
348
            $builder = CollectionBuilder::create($container, $commandClass);
0 ignored issues
show
Compatibility introduced by
$commandClass of type object<Robo\Contract\BuilderAwareInterface> is not a sub-type of object<Robo\Tasks>. It seems like you assume a concrete implementation of the interface Robo\Contract\BuilderAwareInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
349
            $commandClass->setBuilder($builder);
350
        }
351
        if ($commandClass instanceof ContainerAwareInterface) {
352
            $commandClass->setContainer($container);
353
        }
354
        return $commandClass;
355
    }
356
357
    public function installRoboHandlers()
358
    {
359
        register_shutdown_function(array($this, 'shutdown'));
360
        set_error_handler(array($this, 'handleError'));
361
    }
362
363
    /**
364
     * Process a shebang script, if one was used to launch this Runner.
365
     *
366
     * @param array $args
367
     *
368
     * @return array $args
369
     *   With shebang script removed.
370
     */
371
    protected function shebang($args)
372
    {
373
        // Option 1: Shebang line names Robo, but includes no parameters.
374
        // #!/bin/env robo
375
        // The robo class may contain multiple commands; the user may
376
        // select which one to run, or even get a list of commands or
377
        // run 'help' on any of the available commands as usual.
378 View Code Duplication
        if ((count($args) > 1) && $this->isShebangFile($args[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
379
            return array_merge([$args[0]], array_slice($args, 2));
380
        }
381
        // Option 2: Shebang line stipulates which command to run.
382
        // #!/bin/env robo mycommand
383
        // The robo class must contain a public method named 'mycommand'.
384
        // This command will be executed every time.  Arguments and options
385
        // may be provided on the commandline as usual.
386 View Code Duplication
        if ((count($args) > 2) && $this->isShebangFile($args[2])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
387
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
388
        }
389
        return $args;
390
    }
391
392
    /**
393
     * Determine if the specified argument is a path to a shebang script.
394
     * If so, load it.
395
     *
396
     * @param string $filepath
397
     *   File to check.
398
     *
399
     * @return bool
400
     *   Returns TRUE if shebang script was processed.
401
     */
402
    protected function isShebangFile($filepath)
403
    {
404
        // Avoid trying to call $filepath on remote URLs
405
        if ((strpos($filepath, '://') !== false) && (substr($filepath, 0, 7) != 'file://')) {
406
            return false;
407
        }
408
        if (!is_file($filepath)) {
409
            return false;
410
        }
411
        $fp = fopen($filepath, "r");
412
        if ($fp === false) {
413
            return false;
414
        }
415
        $line = fgets($fp);
416
        $result = $this->isShebangLine($line);
417
        if ($result) {
418
            while ($line = fgets($fp)) {
419
                $line = trim($line);
420
                if ($line == '<?php') {
421
                    $script = stream_get_contents($fp);
422
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
423
                        $this->roboClass = $matches[1];
424
                        eval($script);
425
                        $result = true;
426
                    }
427
                }
428
            }
429
        }
430
        fclose($fp);
431
432
        return $result;
433
    }
434
435
    /**
436
     * Test to see if the provided line is a robo 'shebang' line.
437
     *
438
     * @param string $line
439
     *
440
     * @return bool
441
     */
442
    protected function isShebangLine($line)
443
    {
444
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
445
    }
446
447
    /**
448
     * Check for Robo-specific arguments such as --load-from, process them,
449
     * and remove them from the array.  We have to process --load-from before
450
     * we set up Symfony Console.
451
     *
452
     * @param array $argv
453
     *
454
     * @return array
455
     */
456
    protected function processRoboOptions($argv)
457
    {
458
        // loading from other directory
459
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
460
        if ($pos === false) {
461
            return $argv;
462
        }
463
464
        $passThru = array_search('--', $argv);
465
        if (($passThru !== false) && ($passThru < $pos)) {
466
            return $argv;
467
        }
468
469
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
470
            $this->dir = substr($argv[$pos], 12);
471
        } elseif (isset($argv[$pos + 1])) {
472
            $this->dir = $argv[$pos + 1];
473
            unset($argv[$pos + 1]);
474
        }
475
        unset($argv[$pos]);
476
        // Make adjustments if '--load-from' points at a file.
477
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
478
            $this->roboFile = basename($this->dir);
479
            $this->dir = dirname($this->dir);
480
            $className = basename($this->roboFile, '.php');
481
            if ($className != $this->roboFile) {
482
                $this->roboClass = $className;
483
            }
484
        }
485
        // Convert directory to a real path, but only if the
486
        // path exists. We do not want to lose the original
487
        // directory if the user supplied a bad value.
488
        $realDir = realpath($this->dir);
489
        if ($realDir) {
490
            chdir($realDir);
491
            $this->dir = $realDir;
492
        }
493
494
        return $argv;
495
    }
496
497
    /**
498
     * @param string $needle
499
     * @param string[] $haystack
500
     *
501
     * @return bool|int
502
     */
503
    protected function arraySearchBeginsWith($needle, $haystack)
504
    {
505
        for ($i = 0; $i < count($haystack); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
506
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
507
                return $i;
508
            }
509
        }
510
        return false;
511
    }
512
513
    public function shutdown()
514
    {
515
        $error = error_get_last();
516
        if (!is_array($error)) {
517
            return;
518
        }
519
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
520
    }
521
522
    /**
523
     * This is just a proxy error handler that checks the current error_reporting level.
524
     * In case error_reporting is disabled the error is marked as handled, otherwise
525
     * the normal internal error handling resumes.
526
     *
527
     * @return bool
528
     */
529
    public function handleError()
530
    {
531
        if (error_reporting() === 0) {
532
            return true;
533
        }
534
        return false;
535
    }
536
537
    /**
538
     * @return string
539
     */
540
    public function getSelfUpdateRepository()
541
    {
542
        return $this->selfUpdateRepository;
543
    }
544
545
    /**
546
     * @param $selfUpdateRepository
547
     *
548
     * @return $this
549
     */
550
    public function setSelfUpdateRepository($selfUpdateRepository)
551
    {
552
        $this->selfUpdateRepository = $selfUpdateRepository;
553
        return $this;
554
    }
555
556
    /**
557
     * @param string $configFilename
558
     *
559
     * @return $this
560
     */
561
    public function setConfigurationFilename($configFilename)
562
    {
563
        $this->configFilename = $configFilename;
564
        return $this;
565
    }
566
567
    /**
568
     * @param string $envConfigPrefix
569
     *
570
     * @return $this
571
     */
572
    public function setEnvConfigPrefix($envConfigPrefix)
573
    {
574
        $this->envConfigPrefix = $envConfigPrefix;
575
        return $this;
576
    }
577
578
    /**
579
     * @param \Composer\Autoload\ClassLoader $classLoader
580
     *
581
     * @return $this
582
     */
583
    public function setClassLoader(ClassLoader $classLoader)
584
    {
585
        $this->classLoader = $classLoader;
586
        return $this;
587
    }
588
589
    /**
590
     * @param string $relativeNamespace
591
     *
592
     * @return $this
593
     */
594
    public function setRelativePluginNamespace($relativeNamespace)
595
    {
596
        $this->relativePluginNamespace = $relativeNamespace;
597
        return $this;
598
    }
599
}
600