Completed
Push — master ( 79c04e...cd88c2 )
by Greg
03:02
created

Runner::discoverCommandClasses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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