Completed
Push — master ( 79c04e...cd88c2 )
by Greg
03:02
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
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();
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) {
131
            $app = Robo::createDefaultApplication($appName, $appVersion);
132
        }
133
        $commandFiles = $this->getRoboFileCommands($output);
134
        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...
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])) {
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])) {
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);
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) {
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