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