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

src/Runner.php (2 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) {
142
            $app = Robo::createDefaultApplication($appName, $appVersion);
143
        }
144
        $commandFiles = $this->getRoboFileCommands($output);
145
        return $this->run($argv, $output, $app, $commandFiles, $this->classLoader);
0 ignored issues
show
$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...
$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...
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) {
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