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