Completed
Push — master ( 8b8afe...5f2bbe )
by Greg
02:21
created

src/Runner.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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