Completed
Pull Request — master (#604)
by Greg
02:46
created

Runner::getPluginCommandClasses()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 16
nc 10
nop 0
1
<?php
2
namespace Robo;
3
4
use Symfony\Component\Console\Input\ArgvInput;
5
use Symfony\Component\Console\Input\StringInput;
6
use Robo\Contract\BuilderAwareInterface;
7
use Robo\Collection\CollectionBuilder;
8
use Robo\Common\IO;
9
use Robo\Exception\TaskExitException;
10
use League\Container\ContainerAwareInterface;
11
use League\Container\ContainerAwareTrait;
12
use Composer\Autoload\ClassLoader;
13
use Consolidation\AnnotatedCommand\CommandFileDiscovery;
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[]
45
     */
46
    protected $commandFilePluginPrefixes;
47
48
    /**
49
     * @var string
50
     */
51
    protected $namespacePattern = '';
52
53
    /**
54
     * Class Constructor
55
     *
56
     * @param null|string $roboClass
57
     * @param null|string $roboFile
58
     */
59
    public function __construct($roboClass = null, $roboFile = null)
60
    {
61
        // set the const as class properties to allow overwriting in child classes
62
        $this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
63
        $this->roboFile  = $roboFile ? $roboFile : self::ROBOFILE;
64
        $this->dir = getcwd();
65
    }
66
67
    protected function errorCondtion($msg, $errorType)
68
    {
69
        $this->errorConditions[$msg] = $errorType;
70
    }
71
72
    /**
73
     * @param \Symfony\Component\Console\Output\OutputInterface $output
0 ignored issues
show
Bug introduced by
There is no parameter named $output. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
74
     *
75
     * @return bool
76
     */
77
    protected function loadRoboFile()
78
    {
79
        // If $this->roboClass is a single class that has not already
80
        // been loaded, then we will try to obtain it from $this->roboFile.
81
        // If $this->roboClass is an array, we presume all classes requested
82
        // are available via the autoloader.
83
        if (is_array($this->roboClass) || class_exists($this->roboClass)) {
84
            return true;
85
        }
86
        if (!file_exists($this->dir)) {
87
            $this->errorCondtion("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
88
            return false;
89
        }
90
91
        $realDir = realpath($this->dir);
92
93
        $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
94
        if (!file_exists($roboFilePath)) {
95
            $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
96
            $this->errorCondtion("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
97
            return false;
98
        }
99
        require_once $roboFilePath;
100
101
        if (!class_exists($this->roboClass)) {
102
            $this->errorCondtion("Class {$this->roboClass} was not loaded.", 'red');
103
            return false;
104
        }
105
        return true;
106
    }
107
108
    /**
109
     * @param array $argv
110
     * @param null|string $appName
111
     * @param null|string $appVersion
112
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
113
     *
114
     * @return int
115
     */
116
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
117
    {
118
        $argv = $this->shebang($argv);
119
        $argv = $this->processRoboOptions($argv);
120
        $app = null;
121
        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...
122
            $app = Robo::createDefaultApplication($appName, $appVersion);
123
        }
124
        $commandFiles = $this->getRoboFileCommands();
125
        return $this->run($argv, $output, $app, $commandFiles);
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...
126
    }
127
128
    /**
129
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
130
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
131
     * @param null|\Robo\Application $app
132
     * @param array[] $commandFiles
133
     *
134
     * @return int
135
     */
136
    public function run($input = null, $output = null, $app = null, $commandFiles = [])
137
    {
138
        // Create default input and output objects if they were not provided
139
        if (!$input) {
140
            $input = new StringInput('');
141
        }
142
        if (is_array($input)) {
143
            $input = new ArgvInput($input);
144
        }
145
        if (!$output) {
146
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
147
        }
148
        $this->setInput($input);
149
        $this->setOutput($output);
150
151
        // If we were not provided a container, then create one
152
        if (!$this->getContainer()) {
153
            $config = Robo::createConfiguration(['robo.yml']);
154
            $container = Robo::createDefaultContainer($input, $output, $app, $config);
155
            $this->setContainer($container);
156
            // Automatically register a shutdown function and
157
            // an error handler when we provide the container.
158
            $this->installRoboHandlers();
159
        }
160
161
        if (!$app) {
162
            $app = Robo::application();
163
        }
164
        if (!isset($commandFiles)) {
165
            $this->errorCondtion("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
166
            $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
167
            $commandFiles = [];
168
        }
169
        $this->registerCommandClasses($app, $commandFiles);
170
171
        try {
172
            $statusCode = $app->run($input, $output);
173
        } catch (TaskExitException $e) {
174
            $statusCode = $e->getCode() ?: 1;
175
        }
176
177
        // If there were any error conditions in bootstrapping Robo,
178
        // print them only if the requested command did not complete
179
        // successfully.
180
        if ($statusCode) {
181
            foreach ($this->errorConditions as $msg => $color) {
182
                $this->yell($msg, 40, $color);
183
            }
184
        }
185
        return $statusCode;
186
    }
187
188
    public function setLoader(ClassLoader $loader)
189
    {
190
        return $this->setCommandFilePluginPrefixes($loader->getPrefixesPsr4());
191
    }
192
193
    public function setCommandFilePluginPrefixes($prefixes)
194
    {
195
        $this->commandFilePluginPrefixes = $prefixes;
196
        return $this;
197
    }
198
199
    public function setCommandFilePluginPattern($namespacePattern)
200
    {
201
        $this->namespacePattern = $namespacePattern;
202
        return $this;
203
    }
204
205
    protected function getPluginCommandClasses()
206
    {
207
        $commandClasses = [];
208
        if ((!$this->commandFilePluginPrefixes) || empty($this->namespacePattern)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->commandFilePluginPrefixes of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
209
            return [];
210
        }
211
        $pattern = '#' . $this->namespacePattern . '#';
212
        $commandSearchPaths = [];
213
        foreach ($this->commandFilePluginPrefixes as $baseNamespace => $paths) {
214
            if (preg_match($pattern, $baseNamespace)) {
215
                $commandSearchPaths[$baseNamespace] = $paths;
216
            }
217
        }
218
219
        $discovery = new CommandFileDiscovery();
220
        $discovery->setSearchLocations(['Commands']);
221
222
        foreach ($commandSearchPaths as $baseNamespace => $paths) {
223
            foreach ($paths as $path) {
224
                $discoveredCommandClasses = $discovery->discover($path, $baseNamespace);
225
                $commandClasses = array_merge($commandClasses, $discoveredCommandClasses);
226
            }
227
        }
228
229
        return $commandClasses;
230
    }
231
232
    /**
233
     * @param \Symfony\Component\Console\Output\OutputInterface $output
0 ignored issues
show
Bug introduced by
There is no parameter named $output. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
234
     *
235
     * @return null|string
236
     */
237
    protected function getRoboFileCommands()
238
    {
239
        $commandClasses = $this->getPluginCommandClasses();
240
        if (!$this->loadRoboFile()) {
241
            return $commandClasses;
242
        }
243
        $commandClasses = array_merge($commandClasses, (array)$this->roboClass);
244
        return $commandClasses;
245
    }
246
247
    /**
248
     * @param \Robo\Application $app
249
     * @param array $commandClasses
250
     */
251
    public function registerCommandClasses($app, $commandClasses)
252
    {
253
        foreach ((array)$commandClasses as $commandClass) {
254
            $this->registerCommandClass($app, $commandClass);
255
        }
256
    }
257
258
    /**
259
     * @param \Robo\Application $app
260
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
261
     *
262
     * @return mixed|void
263
     */
264
    public function registerCommandClass($app, $commandClass)
265
    {
266
        $container = Robo::getContainer();
267
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
268
        if (!$roboCommandFileInstance) {
269
            return;
270
        }
271
272
        // Register commands for all of the public methods in the RoboFile.
273
        $commandFactory = $container->get('commandFactory');
274
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
275
        foreach ($commandList as $command) {
276
            $app->add($command);
277
        }
278
        return $roboCommandFileInstance;
279
    }
280
281
    /**
282
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
283
     *
284
     * @return null|object
285
     */
286
    protected function instantiateCommandClass($commandClass)
287
    {
288
        $container = Robo::getContainer();
289
290
        // Register the RoboFile with the container and then immediately
291
        // fetch it; this ensures that all of the inflectors will run.
292
        // If the command class is already an instantiated object, then
293
        // just use it exactly as it was provided to us.
294
        if (is_string($commandClass)) {
295
            if (!class_exists($commandClass)) {
296
                return;
297
            }
298
            $reflectionClass = new \ReflectionClass($commandClass);
299
            if ($reflectionClass->isAbstract()) {
300
                return;
301
            }
302
303
            $commandFileName = "{$commandClass}Commands";
304
            $container->share($commandFileName, $commandClass);
305
            $commandClass = $container->get($commandFileName);
306
        }
307
        // If the command class is a Builder Aware Interface, then
308
        // ensure that it has a builder.  Every command class needs
309
        // its own collection builder, as they have references to each other.
310
        if ($commandClass instanceof BuilderAwareInterface) {
311
            $builder = CollectionBuilder::create($container, $commandClass);
312
            $commandClass->setBuilder($builder);
313
        }
314
        if ($commandClass instanceof ContainerAwareInterface) {
315
            $commandClass->setContainer($container);
316
        }
317
        return $commandClass;
318
    }
319
320
    public function installRoboHandlers()
321
    {
322
        register_shutdown_function(array($this, 'shutdown'));
323
        set_error_handler(array($this, 'handleError'));
324
    }
325
326
    /**
327
     * Process a shebang script, if one was used to launch this Runner.
328
     *
329
     * @param array $args
330
     *
331
     * @return array $args with shebang script removed
332
     */
333
    protected function shebang($args)
334
    {
335
        // Option 1: Shebang line names Robo, but includes no parameters.
336
        // #!/bin/env robo
337
        // The robo class may contain multiple commands; the user may
338
        // select which one to run, or even get a list of commands or
339
        // run 'help' on any of the available commands as usual.
340 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...
341
            return array_merge([$args[0]], array_slice($args, 2));
342
        }
343
        // Option 2: Shebang line stipulates which command to run.
344
        // #!/bin/env robo mycommand
345
        // The robo class must contain a public method named 'mycommand'.
346
        // This command will be executed every time.  Arguments and options
347
        // may be provided on the commandline as usual.
348 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...
349
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
350
        }
351
        return $args;
352
    }
353
354
    /**
355
     * Determine if the specified argument is a path to a shebang script.
356
     * If so, load it.
357
     *
358
     * @param string $filepath file to check
359
     *
360
     * @return bool Returns TRUE if shebang script was processed
361
     */
362
    protected function isShebangFile($filepath)
363
    {
364
        if (!is_file($filepath)) {
365
            return false;
366
        }
367
        $fp = fopen($filepath, "r");
368
        if ($fp === false) {
369
            return false;
370
        }
371
        $line = fgets($fp);
372
        $result = $this->isShebangLine($line);
373
        if ($result) {
374
            while ($line = fgets($fp)) {
375
                $line = trim($line);
376
                if ($line == '<?php') {
377
                    $script = stream_get_contents($fp);
378
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
379
                        $this->roboClass = $matches[1];
380
                        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...
381
                        $result = true;
382
                    }
383
                }
384
            }
385
        }
386
        fclose($fp);
387
388
        return $result;
389
    }
390
391
    /**
392
     * Test to see if the provided line is a robo 'shebang' line.
393
     *
394
     * @param string $line
395
     *
396
     * @return bool
397
     */
398
    protected function isShebangLine($line)
399
    {
400
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
401
    }
402
403
    /**
404
     * Check for Robo-specific arguments such as --load-from, process them,
405
     * and remove them from the array.  We have to process --load-from before
406
     * we set up Symfony Console.
407
     *
408
     * @param array $argv
409
     *
410
     * @return array
411
     */
412
    protected function processRoboOptions($argv)
413
    {
414
        // loading from other directory
415
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
416
        if ($pos === false) {
417
            return $argv;
418
        }
419
420
        $passThru = array_search('--', $argv);
421
        if (($passThru !== false) && ($passThru < $pos)) {
422
            return $argv;
423
        }
424
425
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
426
            $this->dir = substr($argv[$pos], 12);
427
        } elseif (isset($argv[$pos +1])) {
428
            $this->dir = $argv[$pos +1];
429
            unset($argv[$pos +1]);
430
        }
431
        unset($argv[$pos]);
432
        // Make adjustments if '--load-from' points at a file.
433
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
434
            $this->roboFile = basename($this->dir);
435
            $this->dir = dirname($this->dir);
436
            $className = basename($this->roboFile, '.php');
437
            if ($className != $this->roboFile) {
438
                $this->roboClass = $className;
439
            }
440
        }
441
        // Convert directory to a real path, but only if the
442
        // path exists. We do not want to lose the original
443
        // directory if the user supplied a bad value.
444
        $realDir = realpath($this->dir);
445
        if ($realDir) {
446
            chdir($realDir);
447
            $this->dir = $realDir;
448
        }
449
450
        return $argv;
451
    }
452
453
    /**
454
     * @param string $needle
455
     * @param string[] $haystack
456
     *
457
     * @return bool|int
458
     */
459
    protected function arraySearchBeginsWith($needle, $haystack)
460
    {
461
        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...
462
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
463
                return $i;
464
            }
465
        }
466
        return false;
467
    }
468
469
    public function shutdown()
470
    {
471
        $error = error_get_last();
472
        if (!is_array($error)) {
473
            return;
474
        }
475
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
476
    }
477
478
    /**
479
     * This is just a proxy error handler that checks the current error_reporting level.
480
     * In case error_reporting is disabled the error is marked as handled, otherwise
481
     * the normal internal error handling resumes.
482
     *
483
     * @return bool
484
     */
485
    public function handleError()
486
    {
487
        if (error_reporting() === 0) {
488
            return true;
489
        }
490
        return false;
491
    }
492
}
493