Completed
Pull Request — master (#604)
by Greg
02:48
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
     * @return bool
74
     */
75
    protected function loadRoboFile()
76
    {
77
        // If $this->roboClass is a single class that has not already
78
        // been loaded, then we will try to obtain it from $this->roboFile.
79
        // If $this->roboClass is an array, we presume all classes requested
80
        // are available via the autoloader.
81
        if (is_array($this->roboClass) || class_exists($this->roboClass)) {
82
            return true;
83
        }
84
        if (!file_exists($this->dir)) {
85
            $this->errorCondtion("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
86
            return false;
87
        }
88
89
        $realDir = realpath($this->dir);
90
91
        $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
92
        if (!file_exists($roboFilePath)) {
93
            $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
94
            $this->errorCondtion("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
95
            return false;
96
        }
97
        require_once $roboFilePath;
98
99
        if (!class_exists($this->roboClass)) {
100
            $this->errorCondtion("Class {$this->roboClass} was not loaded.", 'red');
101
            return false;
102
        }
103
        return true;
104
    }
105
106
    /**
107
     * @param array $argv
108
     * @param null|string $appName
109
     * @param null|string $appVersion
110
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
111
     *
112
     * @return int
113
     */
114
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
115
    {
116
        $argv = $this->shebang($argv);
117
        $argv = $this->processRoboOptions($argv);
118
        $app = null;
119
        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...
120
            $app = Robo::createDefaultApplication($appName, $appVersion);
121
        }
122
        $commandFiles = $this->getRoboFileCommands();
123
        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...
124
    }
125
126
    /**
127
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
128
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
129
     * @param null|\Robo\Application $app
130
     * @param array[] $commandFiles
131
     *
132
     * @return int
133
     */
134
    public function run($input = null, $output = null, $app = null, $commandFiles = [])
135
    {
136
        // Create default input and output objects if they were not provided
137
        if (!$input) {
138
            $input = new StringInput('');
139
        }
140
        if (is_array($input)) {
141
            $input = new ArgvInput($input);
142
        }
143
        if (!$output) {
144
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
145
        }
146
        $this->setInput($input);
147
        $this->setOutput($output);
148
149
        // If we were not provided a container, then create one
150
        if (!$this->getContainer()) {
151
            $config = Robo::createConfiguration(['robo.yml']);
152
            $container = Robo::createDefaultContainer($input, $output, $app, $config);
153
            $this->setContainer($container);
154
            // Automatically register a shutdown function and
155
            // an error handler when we provide the container.
156
            $this->installRoboHandlers();
157
        }
158
159
        if (!$app) {
160
            $app = Robo::application();
161
        }
162
        if (!isset($commandFiles)) {
163
            $this->errorCondtion("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
164
            $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
165
            $commandFiles = [];
166
        }
167
        $this->registerCommandClasses($app, $commandFiles);
168
169
        try {
170
            $statusCode = $app->run($input, $output);
171
        } catch (TaskExitException $e) {
172
            $statusCode = $e->getCode() ?: 1;
173
        }
174
175
        // If there were any error conditions in bootstrapping Robo,
176
        // print them only if the requested command did not complete
177
        // successfully.
178
        if ($statusCode) {
179
            foreach ($this->errorConditions as $msg => $color) {
180
                $this->yell($msg, 40, $color);
181
            }
182
        }
183
        return $statusCode;
184
    }
185
186
    public function setLoader(ClassLoader $loader)
187
    {
188
        return $this->setCommandFilePluginPrefixes($loader->getPrefixesPsr4());
189
    }
190
191
    public function setCommandFilePluginPrefixes($prefixes)
192
    {
193
        $this->commandFilePluginPrefixes = $prefixes;
194
        return $this;
195
    }
196
197
    public function setCommandFilePluginPattern($namespacePattern)
198
    {
199
        $this->namespacePattern = $namespacePattern;
200
        return $this;
201
    }
202
203
    protected function getPluginCommandClasses()
204
    {
205
        $commandClasses = [];
206
        if ((empty($this->commandFilePluginPrefixes)) || empty($this->namespacePattern)) {
207
            return [];
208
        }
209
        $pattern = '#' . $this->namespacePattern . '#';
210
        $commandSearchPaths = [];
211
        foreach ($this->commandFilePluginPrefixes as $baseNamespace => $paths) {
212
            if (preg_match($pattern, $baseNamespace)) {
213
                $commandSearchPaths[$baseNamespace] = $paths;
214
            }
215
        }
216
217
        $discovery = new CommandFileDiscovery();
218
        $discovery->setSearchLocations(['Commands']);
219
220
        foreach ($commandSearchPaths as $baseNamespace => $paths) {
221
            foreach ($paths as $path) {
222
                $discoveredCommandClasses = $discovery->discover($path, $baseNamespace);
223
                $commandClasses = array_merge($commandClasses, $discoveredCommandClasses);
224
            }
225
        }
226
227
        return $commandClasses;
228
    }
229
230
    /**
231
     * @return null|string
232
     */
233
    protected function getRoboFileCommands()
234
    {
235
        $commandClasses = $this->getPluginCommandClasses();
236
        if (!$this->loadRoboFile()) {
237
            return $commandClasses;
238
        }
239
        $commandClasses = array_merge($commandClasses, (array)$this->roboClass);
240
        return $commandClasses;
241
    }
242
243
    /**
244
     * @param \Robo\Application $app
245
     * @param array $commandClasses
246
     */
247
    public function registerCommandClasses($app, $commandClasses)
248
    {
249
        foreach ((array)$commandClasses as $commandClass) {
250
            $this->registerCommandClass($app, $commandClass);
251
        }
252
    }
253
254
    /**
255
     * @param \Robo\Application $app
256
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
257
     *
258
     * @return mixed|void
259
     */
260
    public function registerCommandClass($app, $commandClass)
261
    {
262
        $container = Robo::getContainer();
263
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
264
        if (!$roboCommandFileInstance) {
265
            return;
266
        }
267
268
        // Register commands for all of the public methods in the RoboFile.
269
        $commandFactory = $container->get('commandFactory');
270
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
271
        foreach ($commandList as $command) {
272
            $app->add($command);
273
        }
274
        return $roboCommandFileInstance;
275
    }
276
277
    /**
278
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
279
     *
280
     * @return null|object
281
     */
282
    protected function instantiateCommandClass($commandClass)
283
    {
284
        $container = Robo::getContainer();
285
286
        // Register the RoboFile with the container and then immediately
287
        // fetch it; this ensures that all of the inflectors will run.
288
        // If the command class is already an instantiated object, then
289
        // just use it exactly as it was provided to us.
290
        if (is_string($commandClass)) {
291
            if (!class_exists($commandClass)) {
292
                return;
293
            }
294
            $reflectionClass = new \ReflectionClass($commandClass);
295
            if ($reflectionClass->isAbstract()) {
296
                return;
297
            }
298
299
            $commandFileName = "{$commandClass}Commands";
300
            $container->share($commandFileName, $commandClass);
301
            $commandClass = $container->get($commandFileName);
302
        }
303
        // If the command class is a Builder Aware Interface, then
304
        // ensure that it has a builder.  Every command class needs
305
        // its own collection builder, as they have references to each other.
306
        if ($commandClass instanceof BuilderAwareInterface) {
307
            $builder = CollectionBuilder::create($container, $commandClass);
308
            $commandClass->setBuilder($builder);
309
        }
310
        if ($commandClass instanceof ContainerAwareInterface) {
311
            $commandClass->setContainer($container);
312
        }
313
        return $commandClass;
314
    }
315
316
    public function installRoboHandlers()
317
    {
318
        register_shutdown_function(array($this, 'shutdown'));
319
        set_error_handler(array($this, 'handleError'));
320
    }
321
322
    /**
323
     * Process a shebang script, if one was used to launch this Runner.
324
     *
325
     * @param array $args
326
     *
327
     * @return array $args with shebang script removed
328
     */
329
    protected function shebang($args)
330
    {
331
        // Option 1: Shebang line names Robo, but includes no parameters.
332
        // #!/bin/env robo
333
        // The robo class may contain multiple commands; the user may
334
        // select which one to run, or even get a list of commands or
335
        // run 'help' on any of the available commands as usual.
336 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...
337
            return array_merge([$args[0]], array_slice($args, 2));
338
        }
339
        // Option 2: Shebang line stipulates which command to run.
340
        // #!/bin/env robo mycommand
341
        // The robo class must contain a public method named 'mycommand'.
342
        // This command will be executed every time.  Arguments and options
343
        // may be provided on the commandline as usual.
344 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...
345
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
346
        }
347
        return $args;
348
    }
349
350
    /**
351
     * Determine if the specified argument is a path to a shebang script.
352
     * If so, load it.
353
     *
354
     * @param string $filepath file to check
355
     *
356
     * @return bool Returns TRUE if shebang script was processed
357
     */
358
    protected function isShebangFile($filepath)
359
    {
360
        if (!is_file($filepath)) {
361
            return false;
362
        }
363
        $fp = fopen($filepath, "r");
364
        if ($fp === false) {
365
            return false;
366
        }
367
        $line = fgets($fp);
368
        $result = $this->isShebangLine($line);
369
        if ($result) {
370
            while ($line = fgets($fp)) {
371
                $line = trim($line);
372
                if ($line == '<?php') {
373
                    $script = stream_get_contents($fp);
374
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
375
                        $this->roboClass = $matches[1];
376
                        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...
377
                        $result = true;
378
                    }
379
                }
380
            }
381
        }
382
        fclose($fp);
383
384
        return $result;
385
    }
386
387
    /**
388
     * Test to see if the provided line is a robo 'shebang' line.
389
     *
390
     * @param string $line
391
     *
392
     * @return bool
393
     */
394
    protected function isShebangLine($line)
395
    {
396
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
397
    }
398
399
    /**
400
     * Check for Robo-specific arguments such as --load-from, process them,
401
     * and remove them from the array.  We have to process --load-from before
402
     * we set up Symfony Console.
403
     *
404
     * @param array $argv
405
     *
406
     * @return array
407
     */
408
    protected function processRoboOptions($argv)
409
    {
410
        // loading from other directory
411
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
412
        if ($pos === false) {
413
            return $argv;
414
        }
415
416
        $passThru = array_search('--', $argv);
417
        if (($passThru !== false) && ($passThru < $pos)) {
418
            return $argv;
419
        }
420
421
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
422
            $this->dir = substr($argv[$pos], 12);
423
        } elseif (isset($argv[$pos +1])) {
424
            $this->dir = $argv[$pos +1];
425
            unset($argv[$pos +1]);
426
        }
427
        unset($argv[$pos]);
428
        // Make adjustments if '--load-from' points at a file.
429
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
430
            $this->roboFile = basename($this->dir);
431
            $this->dir = dirname($this->dir);
432
            $className = basename($this->roboFile, '.php');
433
            if ($className != $this->roboFile) {
434
                $this->roboClass = $className;
435
            }
436
        }
437
        // Convert directory to a real path, but only if the
438
        // path exists. We do not want to lose the original
439
        // directory if the user supplied a bad value.
440
        $realDir = realpath($this->dir);
441
        if ($realDir) {
442
            chdir($realDir);
443
            $this->dir = $realDir;
444
        }
445
446
        return $argv;
447
    }
448
449
    /**
450
     * @param string $needle
451
     * @param string[] $haystack
452
     *
453
     * @return bool|int
454
     */
455
    protected function arraySearchBeginsWith($needle, $haystack)
456
    {
457
        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...
458
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
459
                return $i;
460
            }
461
        }
462
        return false;
463
    }
464
465
    public function shutdown()
466
    {
467
        $error = error_get_last();
468
        if (!is_array($error)) {
469
            return;
470
        }
471
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
472
    }
473
474
    /**
475
     * This is just a proxy error handler that checks the current error_reporting level.
476
     * In case error_reporting is disabled the error is marked as handled, otherwise
477
     * the normal internal error handling resumes.
478
     *
479
     * @return bool
480
     */
481
    public function handleError()
482
    {
483
        if (error_reporting() === 0) {
484
            return true;
485
        }
486
        return false;
487
    }
488
}
489