Completed
Pull Request — master (#604)
by Greg
05:30
created

Runner::setLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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