Completed
Push — master ( 7106b9...4aeb0e )
by Greg
06:35
created

src/Runner.php (1 issue)

Severity

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
     * Class Constructor
43
     *
44
     * @param null|string $roboClass
45
     * @param null|string $roboFile
46
     */
47
    public function __construct($roboClass = null, $roboFile = null)
48
    {
49
        // set the const as class properties to allow overwriting in child classes
50
        $this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
51
        $this->roboFile  = $roboFile ? $roboFile : self::ROBOFILE;
52
        $this->dir = getcwd();
53
    }
54
55
    protected function errorCondtion($msg, $errorType)
56
    {
57
        $this->errorConditions[$msg] = $errorType;
58
    }
59
60
    /**
61
     * @param \Symfony\Component\Console\Output\OutputInterface $output
62
     *
63
     * @return bool
64
     */
65
    protected function loadRoboFile($output)
66
    {
67
        // If we have not been provided an output object, make a temporary one.
68
        if (!$output) {
69
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
0 ignored issues
show
$output is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
70
        }
71
72
        // If $this->roboClass is a single class that has not already
73
        // been loaded, then we will try to obtain it from $this->roboFile.
74
        // If $this->roboClass is an array, we presume all classes requested
75
        // are available via the autoloader.
76
        if (is_array($this->roboClass) || class_exists($this->roboClass)) {
77
            return true;
78
        }
79
        if (!file_exists($this->dir)) {
80
            $this->errorCondtion("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
81
            return false;
82
        }
83
84
        $realDir = realpath($this->dir);
85
86
        $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
87
        if (!file_exists($roboFilePath)) {
88
            $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
89
            $this->errorCondtion("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
90
            return false;
91
        }
92
        require_once $roboFilePath;
93
94
        if (!class_exists($this->roboClass)) {
95
            $this->errorCondtion("Class {$this->roboClass} was not loaded.", 'red');
96
            return false;
97
        }
98
        return true;
99
    }
100
101
    /**
102
     * @param array $argv
103
     * @param null|string $appName
104
     * @param null|string $appVersion
105
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
106
     *
107
     * @return int
108
     */
109
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
110
    {
111
        $argv = $this->shebang($argv);
112
        $argv = $this->processRoboOptions($argv);
113
        $app = null;
114
        if ($appName && $appVersion) {
115
            $app = Robo::createDefaultApplication($appName, $appVersion);
116
        }
117
        $commandFiles = $this->getRoboFileCommands($output);
118
        return $this->run($argv, $output, $app, $commandFiles);
119
    }
120
121
    /**
122
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
123
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
124
     * @param null|\Robo\Application $app
125
     * @param array[] $commandFiles
126
     *
127
     * @return int
128
     */
129
    public function run($input = null, $output = null, $app = null, $commandFiles = [])
130
    {
131
        // Create default input and output objects if they were not provided
132
        if (!$input) {
133
            $input = new StringInput('');
134
        }
135
        if (is_array($input)) {
136
            $input = new ArgvInput($input);
137
        }
138
        if (!$output) {
139
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
140
        }
141
        $this->setInput($input);
142
        $this->setOutput($output);
143
144
        // If we were not provided a container, then create one
145
        if (!$this->getContainer()) {
146
            $config = Robo::createConfiguration(['robo.yml']);
147
            $container = Robo::createDefaultContainer($input, $output, $app, $config);
148
            $this->setContainer($container);
149
            // Automatically register a shutdown function and
150
            // an error handler when we provide the container.
151
            $this->installRoboHandlers();
152
        }
153
154
        if (!$app) {
155
            $app = Robo::application();
156
        }
157
        if (!isset($commandFiles)) {
158
            $this->errorCondtion("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
159
            $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
160
            $commandFiles = [];
161
        }
162
        $this->registerCommandClasses($app, $commandFiles);
163
164
        try {
165
            $statusCode = $app->run($input, $output);
166
        } catch (TaskExitException $e) {
167
            $statusCode = $e->getCode() ?: 1;
168
        }
169
170
        // If there were any error conditions in bootstrapping Robo,
171
        // print them only if the requested command did not complete
172
        // successfully.
173
        if ($statusCode) {
174
            foreach ($this->errorConditions as $msg => $color) {
175
                $this->yell($msg, 40, $color);
176
            }
177
        }
178
        return $statusCode;
179
    }
180
181
    /**
182
     * @param \Symfony\Component\Console\Output\OutputInterface $output
183
     *
184
     * @return null|string
185
     */
186
    protected function getRoboFileCommands($output)
187
    {
188
        if (!$this->loadRoboFile($output)) {
189
            return;
190
        }
191
        return $this->roboClass;
192
    }
193
194
    /**
195
     * @param \Robo\Application $app
196
     * @param array $commandClasses
197
     */
198
    public function registerCommandClasses($app, $commandClasses)
199
    {
200
        foreach ((array)$commandClasses as $commandClass) {
201
            $this->registerCommandClass($app, $commandClass);
202
        }
203
    }
204
205
    /**
206
     * @param \Robo\Application $app
207
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
208
     *
209
     * @return mixed|void
210
     */
211
    public function registerCommandClass($app, $commandClass)
212
    {
213
        $container = Robo::getContainer();
214
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
215
        if (!$roboCommandFileInstance) {
216
            return;
217
        }
218
219
        // Register commands for all of the public methods in the RoboFile.
220
        $commandFactory = $container->get('commandFactory');
221
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
222
        foreach ($commandList as $command) {
223
            $app->add($command);
224
        }
225
        return $roboCommandFileInstance;
226
    }
227
228
    /**
229
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
230
     *
231
     * @return null|object
232
     */
233
    protected function instantiateCommandClass($commandClass)
234
    {
235
        $container = Robo::getContainer();
236
237
        // Register the RoboFile with the container and then immediately
238
        // fetch it; this ensures that all of the inflectors will run.
239
        // If the command class is already an instantiated object, then
240
        // just use it exactly as it was provided to us.
241
        if (is_string($commandClass)) {
242
            if (!class_exists($commandClass)) {
243
                return;
244
            }
245
            $reflectionClass = new \ReflectionClass($commandClass);
246
            if ($reflectionClass->isAbstract()) {
247
                return;
248
            }
249
250
            $commandFileName = "{$commandClass}Commands";
251
            $container->share($commandFileName, $commandClass);
252
            $commandClass = $container->get($commandFileName);
253
        }
254
        // If the command class is a Builder Aware Interface, then
255
        // ensure that it has a builder.  Every command class needs
256
        // its own collection builder, as they have references to each other.
257
        if ($commandClass instanceof BuilderAwareInterface) {
258
            $builder = CollectionBuilder::create($container, $commandClass);
259
            $commandClass->setBuilder($builder);
260
        }
261
        if ($commandClass instanceof ContainerAwareInterface) {
262
            $commandClass->setContainer($container);
263
        }
264
        return $commandClass;
265
    }
266
267
    public function installRoboHandlers()
268
    {
269
        register_shutdown_function(array($this, 'shutdown'));
270
        set_error_handler(array($this, 'handleError'));
271
    }
272
273
    /**
274
     * Process a shebang script, if one was used to launch this Runner.
275
     *
276
     * @param array $args
277
     *
278
     * @return array $args with shebang script removed
279
     */
280
    protected function shebang($args)
281
    {
282
        // Option 1: Shebang line names Robo, but includes no parameters.
283
        // #!/bin/env robo
284
        // The robo class may contain multiple commands; the user may
285
        // select which one to run, or even get a list of commands or
286
        // run 'help' on any of the available commands as usual.
287 View Code Duplication
        if ((count($args) > 1) && $this->isShebangFile($args[1])) {
288
            return array_merge([$args[0]], array_slice($args, 2));
289
        }
290
        // Option 2: Shebang line stipulates which command to run.
291
        // #!/bin/env robo mycommand
292
        // The robo class must contain a public method named 'mycommand'.
293
        // This command will be executed every time.  Arguments and options
294
        // may be provided on the commandline as usual.
295 View Code Duplication
        if ((count($args) > 2) && $this->isShebangFile($args[2])) {
296
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
297
        }
298
        return $args;
299
    }
300
301
    /**
302
     * Determine if the specified argument is a path to a shebang script.
303
     * If so, load it.
304
     *
305
     * @param string $filepath file to check
306
     *
307
     * @return bool Returns TRUE if shebang script was processed
308
     */
309
    protected function isShebangFile($filepath)
310
    {
311
        if (!is_file($filepath)) {
312
            return false;
313
        }
314
        $fp = fopen($filepath, "r");
315
        if ($fp === false) {
316
            return false;
317
        }
318
        $line = fgets($fp);
319
        $result = $this->isShebangLine($line);
320
        if ($result) {
321
            while ($line = fgets($fp)) {
322
                $line = trim($line);
323
                if ($line == '<?php') {
324
                    $script = stream_get_contents($fp);
325
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
326
                        $this->roboClass = $matches[1];
327
                        eval($script);
328
                        $result = true;
329
                    }
330
                }
331
            }
332
        }
333
        fclose($fp);
334
335
        return $result;
336
    }
337
338
    /**
339
     * Test to see if the provided line is a robo 'shebang' line.
340
     *
341
     * @param string $line
342
     *
343
     * @return bool
344
     */
345
    protected function isShebangLine($line)
346
    {
347
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
348
    }
349
350
    /**
351
     * Check for Robo-specific arguments such as --load-from, process them,
352
     * and remove them from the array.  We have to process --load-from before
353
     * we set up Symfony Console.
354
     *
355
     * @param array $argv
356
     *
357
     * @return array
358
     */
359
    protected function processRoboOptions($argv)
360
    {
361
        // loading from other directory
362
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
363
        if ($pos === false) {
364
            return $argv;
365
        }
366
367
        $passThru = array_search('--', $argv);
368
        if (($passThru !== false) && ($passThru < $pos)) {
369
            return $argv;
370
        }
371
372
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
373
            $this->dir = substr($argv[$pos], 12);
374
        } elseif (isset($argv[$pos +1])) {
375
            $this->dir = $argv[$pos +1];
376
            unset($argv[$pos +1]);
377
        }
378
        unset($argv[$pos]);
379
        // Make adjustments if '--load-from' points at a file.
380
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
381
            $this->roboFile = basename($this->dir);
382
            $this->dir = dirname($this->dir);
383
            $className = basename($this->roboFile, '.php');
384
            if ($className != $this->roboFile) {
385
                $this->roboClass = $className;
386
            }
387
        }
388
        // Convert directory to a real path, but only if the
389
        // path exists. We do not want to lose the original
390
        // directory if the user supplied a bad value.
391
        $realDir = realpath($this->dir);
392
        if ($realDir) {
393
            chdir($realDir);
394
            $this->dir = $realDir;
395
        }
396
397
        return $argv;
398
    }
399
400
    /**
401
     * @param string $needle
402
     * @param string[] $haystack
403
     *
404
     * @return bool|int
405
     */
406
    protected function arraySearchBeginsWith($needle, $haystack)
407
    {
408
        for ($i = 0; $i < count($haystack); ++$i) {
409
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
410
                return $i;
411
            }
412
        }
413
        return false;
414
    }
415
416
    public function shutdown()
417
    {
418
        $error = error_get_last();
419
        if (!is_array($error)) {
420
            return;
421
        }
422
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
423
    }
424
425
    /**
426
     * This is just a proxy error handler that checks the current error_reporting level.
427
     * In case error_reporting is disabled the error is marked as handled, otherwise
428
     * the normal internal error handling resumes.
429
     *
430
     * @return bool
431
     */
432
    public function handleError()
433
    {
434
        if (error_reporting() === 0) {
435
            return true;
436
        }
437
        return false;
438
    }
439
}
440