Completed
Pull Request — master (#737)
by Greg
03:07
created

Runner::getConfigFilePaths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
1
<?php
2
namespace Robo;
3
4
use Composer\Autoload\ClassLoader;
5
use Symfony\Component\Console\Input\ArgvInput;
6
use Symfony\Component\Console\Input\StringInput;
7
use Robo\Contract\BuilderAwareInterface;
8
use Robo\Collection\CollectionBuilder;
9
use Robo\Common\IO;
10
use Robo\Exception\TaskExitException;
11
use League\Container\ContainerAwareInterface;
12
use League\Container\ContainerAwareTrait;
13
use Consolidation\Config\Util\EnvConfig;
14
15
class Runner implements ContainerAwareInterface
16
{
17
    const ROBOCLASS = 'RoboFile';
18
    const ROBOFILE = 'RoboFile.php';
19
20
    use IO;
21
    use ContainerAwareTrait;
22
23
    /**
24
     * @var string
25
     */
26
    protected $roboClass;
27
28
    /**
29
     * @var string
30
     */
31
    protected $roboFile;
32
33
    /**
34
     * @var string working dir of Robo
35
     */
36
    protected $dir;
37
38
    /**
39
     * @var string[]
40
     */
41
    protected $errorConditions = [];
42
43
    /**
44
     * @var string GitHub Repo for SelfUpdate
45
     */
46
    protected $selfUpdateRepository = null;
47
48
    /**
49
     * @var string filename to load configuration from (set to 'robo.yml' for RoboFiles)
50
     */
51
    protected $configFilename = 'conf.yml';
52
53
    /**
54
     * @var string prefix for environment variable configuration overrides
55
     */
56
    protected $envConfigPrefix = false;
57
58
    /**
59
     * @var \Composer\Autoload\ClassLoader
60
     */
61
    protected $classLoader = null;
62
63
    /**
64
     * @var string
65
     */
66
    protected $relativePluginNamespace;
67
68
    /**
69
     * Class Constructor
70
     *
71
     * @param null|string $roboClass
72
     * @param null|string $roboFile
73
     */
74
    public function __construct($roboClass = null, $roboFile = null)
75
    {
76
        // set the const as class properties to allow overwriting in child classes
77
        $this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
78
        $this->roboFile  = $roboFile ? $roboFile : self::ROBOFILE;
79
        $this->dir = getcwd();
80
    }
81
82
    protected function errorCondition($msg, $errorType)
83
    {
84
        $this->errorConditions[$msg] = $errorType;
85
    }
86
87
    /**
88
     * @param \Symfony\Component\Console\Output\OutputInterface $output
89
     *
90
     * @return bool
91
     */
92
    protected function loadRoboFile($output)
93
    {
94
        // If we have not been provided an output object, make a temporary one.
95
        if (!$output) {
96
            $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...
97
        }
98
99
        // If $this->roboClass is a single class that has not already
100
        // been loaded, then we will try to obtain it from $this->roboFile.
101
        // If $this->roboClass is an array, we presume all classes requested
102
        // are available via the autoloader.
103
        if (is_array($this->roboClass) || class_exists($this->roboClass)) {
104
            return true;
105
        }
106
        if (!file_exists($this->dir)) {
107
            $this->errorCondition("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
108
            return false;
109
        }
110
111
        $realDir = realpath($this->dir);
112
113
        $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
114
        if (!file_exists($roboFilePath)) {
115
            $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
116
            $this->errorCondition("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
117
            return false;
118
        }
119
        require_once $roboFilePath;
120
121
        if (!class_exists($this->roboClass)) {
122
            $this->errorCondition("Class {$this->roboClass} was not loaded.", 'red');
123
            return false;
124
        }
125
        return true;
126
    }
127
128
    /**
129
     * @param array $argv
130
     * @param null|string $appName
131
     * @param null|string $appVersion
132
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
133
     *
134
     * @return int
135
     */
136
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
137
    {
138
        $argv = $this->shebang($argv);
139
        $argv = $this->processRoboOptions($argv);
140
        $app = null;
141
        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...
142
            $app = Robo::createDefaultApplication($appName, $appVersion);
143
        }
144
        $commandFiles = $this->getRoboFileCommands($output);
0 ignored issues
show
Bug introduced by
It seems like $output defined by parameter $output on line 136 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...
145
        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...
146
    }
147
148
    /**
149
     * Get a list of locations where config files may be loaded
150
     * @return string[]
151
     */
152
    protected function getConfigFilePaths($userConfig) {
153
        $roboAppConfig = dirname(__DIR__) . '/' . basename($userConfig);
154
        $configFiles = [$userConfig, $roboAppConfig];
155
        if (dirname($userConfig) != '.') {
156
            array_unshift($configFiles, basename($userConfig));
157
        }
158
        return $configFiles;
159
    }
160
    /**
161
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
162
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
163
     * @param null|\Robo\Application $app
164
     * @param array[] $commandFiles
165
     * @param null|ClassLoader $classLoader
166
     *
167
     * @return int
168
     */
169
    public function run($input = null, $output = null, $app = null, $commandFiles = [], $classLoader = null)
170
    {
171
        // Create default input and output objects if they were not provided
172
        if (!$input) {
173
            $input = new StringInput('');
174
        }
175
        if (is_array($input)) {
176
            $input = new ArgvInput($input);
177
        }
178
        if (!$output) {
179
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
180
        }
181
        $this->setInput($input);
182
        $this->setOutput($output);
183
184
        // If we were not provided a container, then create one
185
        if (!$this->getContainer()) {
186
            $configFiles = $this->getConfigFilePaths($this->configFilename);
187
            $config = Robo::createConfiguration($configFiles);
188
            if ($this->envConfigPrefix) {
189
                $envConfig = new EnvConfig($this->envConfigPrefix);
190
                $config->addContext('env', $envConfig);
191
            }
192
            $container = Robo::createDefaultContainer($input, $output, $app, $config, $classLoader);
193
            $this->setContainer($container);
194
            // Automatically register a shutdown function and
195
            // an error handler when we provide the container.
196
            $this->installRoboHandlers();
197
        }
198
199
        if (!$app) {
200
            $app = Robo::application();
201
        }
202
        if ($app instanceof \Robo\Application) {
203
            $app->addSelfUpdateCommand($this->getSelfUpdateRepository());
204
            if (!isset($commandFiles)) {
205
                $this->errorCondition("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
206
                $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
207
                $commandFiles = [];
208
            }
209
        }
210
211
        if (!empty($this->relativePluginNamespace)) {
212
            $commandClasses = $this->discoverCommandClasses($this->relativePluginNamespace);
213
            $commandFiles = array_merge((array)$commandFiles, $commandClasses);
214
        }
215
216
        $this->registerCommandClasses($app, $commandFiles);
217
218
        try {
219
            $statusCode = $app->run($input, $output);
220
        } catch (TaskExitException $e) {
221
            $statusCode = $e->getCode() ?: 1;
222
        }
223
224
        // If there were any error conditions in bootstrapping Robo,
225
        // print them only if the requested command did not complete
226
        // successfully.
227
        if ($statusCode) {
228
            foreach ($this->errorConditions as $msg => $color) {
229
                $this->yell($msg, 40, $color);
230
            }
231
        }
232
        return $statusCode;
233
    }
234
235
    /**
236
     * @param \Symfony\Component\Console\Output\OutputInterface $output
237
     *
238
     * @return null|string
239
     */
240
    protected function getRoboFileCommands($output)
241
    {
242
        if (!$this->loadRoboFile($output)) {
243
            return;
244
        }
245
        return $this->roboClass;
246
    }
247
248
    /**
249
     * @param \Robo\Application $app
250
     * @param array $commandClasses
251
     */
252
    public function registerCommandClasses($app, $commandClasses)
253
    {
254
        foreach ((array)$commandClasses as $commandClass) {
255
            $this->registerCommandClass($app, $commandClass);
256
        }
257
    }
258
259
    /**
260
     * @param $relativeNamespace
261
     *
262
     * @return array|string[]
263
     */
264
    protected function discoverCommandClasses($relativeNamespace)
265
    {
266
        /** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
267
        $discovery = Robo::service('relativeNamespaceDiscovery');
268
        $discovery->setRelativeNamespace($relativeNamespace.'\Commands')
269
            ->setSearchPattern('*Commands.php');
270
        return $discovery->getClasses();
271
    }
272
273
    /**
274
     * @param \Robo\Application $app
275
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
276
     *
277
     * @return mixed|void
278
     */
279
    public function registerCommandClass($app, $commandClass)
280
    {
281
        $container = Robo::getContainer();
282
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
283
        if (!$roboCommandFileInstance) {
284
            return;
285
        }
286
287
        // Register commands for all of the public methods in the RoboFile.
288
        $commandFactory = $container->get('commandFactory');
289
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
290
        foreach ($commandList as $command) {
291
            $app->add($command);
292
        }
293
        return $roboCommandFileInstance;
294
    }
295
296
    /**
297
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
298
     *
299
     * @return null|object
300
     */
301
    protected function instantiateCommandClass($commandClass)
302
    {
303
        $container = Robo::getContainer();
304
305
        // Register the RoboFile with the container and then immediately
306
        // fetch it; this ensures that all of the inflectors will run.
307
        // If the command class is already an instantiated object, then
308
        // just use it exactly as it was provided to us.
309
        if (is_string($commandClass)) {
310
            if (!class_exists($commandClass)) {
311
                return;
312
            }
313
            $reflectionClass = new \ReflectionClass($commandClass);
314
            if ($reflectionClass->isAbstract()) {
315
                return;
316
            }
317
318
            $commandFileName = "{$commandClass}Commands";
319
            $container->share($commandFileName, $commandClass);
320
            $commandClass = $container->get($commandFileName);
321
        }
322
        // If the command class is a Builder Aware Interface, then
323
        // ensure that it has a builder.  Every command class needs
324
        // its own collection builder, as they have references to each other.
325
        if ($commandClass instanceof BuilderAwareInterface) {
326
            $builder = CollectionBuilder::create($container, $commandClass);
327
            $commandClass->setBuilder($builder);
328
        }
329
        if ($commandClass instanceof ContainerAwareInterface) {
330
            $commandClass->setContainer($container);
331
        }
332
        return $commandClass;
333
    }
334
335
    public function installRoboHandlers()
336
    {
337
        register_shutdown_function(array($this, 'shutdown'));
338
        set_error_handler(array($this, 'handleError'));
339
    }
340
341
    /**
342
     * Process a shebang script, if one was used to launch this Runner.
343
     *
344
     * @param array $args
345
     *
346
     * @return array $args with shebang script removed
347
     */
348
    protected function shebang($args)
349
    {
350
        // Option 1: Shebang line names Robo, but includes no parameters.
351
        // #!/bin/env robo
352
        // The robo class may contain multiple commands; the user may
353
        // select which one to run, or even get a list of commands or
354
        // run 'help' on any of the available commands as usual.
355 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...
356
            return array_merge([$args[0]], array_slice($args, 2));
357
        }
358
        // Option 2: Shebang line stipulates which command to run.
359
        // #!/bin/env robo mycommand
360
        // The robo class must contain a public method named 'mycommand'.
361
        // This command will be executed every time.  Arguments and options
362
        // may be provided on the commandline as usual.
363 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...
364
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
365
        }
366
        return $args;
367
    }
368
369
    /**
370
     * Determine if the specified argument is a path to a shebang script.
371
     * If so, load it.
372
     *
373
     * @param string $filepath file to check
374
     *
375
     * @return bool Returns TRUE if shebang script was processed
376
     */
377
    protected function isShebangFile($filepath)
378
    {
379
        if (!is_file($filepath)) {
380
            return false;
381
        }
382
        $fp = fopen($filepath, "r");
383
        if ($fp === false) {
384
            return false;
385
        }
386
        $line = fgets($fp);
387
        $result = $this->isShebangLine($line);
388
        if ($result) {
389
            while ($line = fgets($fp)) {
390
                $line = trim($line);
391
                if ($line == '<?php') {
392
                    $script = stream_get_contents($fp);
393
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
394
                        $this->roboClass = $matches[1];
395
                        eval($script);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
396
                        $result = true;
397
                    }
398
                }
399
            }
400
        }
401
        fclose($fp);
402
403
        return $result;
404
    }
405
406
    /**
407
     * Test to see if the provided line is a robo 'shebang' line.
408
     *
409
     * @param string $line
410
     *
411
     * @return bool
412
     */
413
    protected function isShebangLine($line)
414
    {
415
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
416
    }
417
418
    /**
419
     * Check for Robo-specific arguments such as --load-from, process them,
420
     * and remove them from the array.  We have to process --load-from before
421
     * we set up Symfony Console.
422
     *
423
     * @param array $argv
424
     *
425
     * @return array
426
     */
427
    protected function processRoboOptions($argv)
428
    {
429
        // loading from other directory
430
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
431
        if ($pos === false) {
432
            return $argv;
433
        }
434
435
        $passThru = array_search('--', $argv);
436
        if (($passThru !== false) && ($passThru < $pos)) {
437
            return $argv;
438
        }
439
440
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
441
            $this->dir = substr($argv[$pos], 12);
442
        } elseif (isset($argv[$pos +1])) {
443
            $this->dir = $argv[$pos +1];
444
            unset($argv[$pos +1]);
445
        }
446
        unset($argv[$pos]);
447
        // Make adjustments if '--load-from' points at a file.
448
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
449
            $this->roboFile = basename($this->dir);
450
            $this->dir = dirname($this->dir);
451
            $className = basename($this->roboFile, '.php');
452
            if ($className != $this->roboFile) {
453
                $this->roboClass = $className;
454
            }
455
        }
456
        // Convert directory to a real path, but only if the
457
        // path exists. We do not want to lose the original
458
        // directory if the user supplied a bad value.
459
        $realDir = realpath($this->dir);
460
        if ($realDir) {
461
            chdir($realDir);
462
            $this->dir = $realDir;
463
        }
464
465
        return $argv;
466
    }
467
468
    /**
469
     * @param string $needle
470
     * @param string[] $haystack
471
     *
472
     * @return bool|int
473
     */
474
    protected function arraySearchBeginsWith($needle, $haystack)
475
    {
476
        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...
477
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
478
                return $i;
479
            }
480
        }
481
        return false;
482
    }
483
484
    public function shutdown()
485
    {
486
        $error = error_get_last();
487
        if (!is_array($error)) {
488
            return;
489
        }
490
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
491
    }
492
493
    /**
494
     * This is just a proxy error handler that checks the current error_reporting level.
495
     * In case error_reporting is disabled the error is marked as handled, otherwise
496
     * the normal internal error handling resumes.
497
     *
498
     * @return bool
499
     */
500
    public function handleError()
501
    {
502
        if (error_reporting() === 0) {
503
            return true;
504
        }
505
        return false;
506
    }
507
508
    /**
509
     * @return string
510
     */
511
    public function getSelfUpdateRepository()
512
    {
513
        return $this->selfUpdateRepository;
514
    }
515
516
    /**
517
     * @param $selfUpdateRepository
518
     *
519
     * @return $this
520
     */
521
    public function setSelfUpdateRepository($selfUpdateRepository)
522
    {
523
        $this->selfUpdateRepository = $selfUpdateRepository;
524
        return $this;
525
    }
526
527
    /**
528
     * @param string $configFilename
529
     *
530
     * @return $this
531
     */
532
    public function setConfigurationFilename($configFilename)
533
    {
534
        $this->configFilename = $configFilename;
535
        return $this;
536
    }
537
538
    /**
539
     * @param string $envConfigPrefix
540
     *
541
     * @return $this
542
     */
543
    public function setEnvConfigPrefix($envConfigPrefix)
544
    {
545
        $this->envConfigPrefix = $envConfigPrefix;
546
        return $this;
547
    }
548
549
    /**
550
     * @param \Composer\Autoload\ClassLoader $classLoader
551
     *
552
     * @return $this
553
     */
554
    public function setClassLoader(ClassLoader $classLoader)
555
    {
556
        $this->classLoader = $classLoader;
557
        return $this;
558
    }
559
560
    /**
561
     * @param string $relativeNamespace
562
     *
563
     * @return $this
564
     */
565
    public function setRelativePluginNamespace($relativeNamespace)
566
    {
567
        $this->relativePluginNamespace = $relativeNamespace;
568
        return $this;
569
    }
570
}
571