Completed
Pull Request — master (#602)
by Greg
02:58
created

Runner::errorCondtion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
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();
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
            $output->writeln("<error>Class ".$this->roboClass." was not loaded</error>");
96
            $this->errorCondtion("Class {$this->roboClass} was not loaded.", 'red');
97
            return false;
98
        }
99
        return true;
100
    }
101
102
    /**
103
     * @param array $argv
104
     * @param null|string $appName
105
     * @param null|string $appVersion
106
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
107
     *
108
     * @return int
109
     */
110
    public function execute($argv, $appName = null, $appVersion = null, $output = null)
111
    {
112
        $argv = $this->shebang($argv);
113
        $argv = $this->processRoboOptions($argv);
114
        $app = null;
115
        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...
116
            $app = Robo::createDefaultApplication($appName, $appVersion);
117
        }
118
        $commandFiles = $this->getRoboFileCommands($output);
0 ignored issues
show
Bug introduced by
It seems like $output defined by parameter $output on line 110 can be null; however, Robo\Runner::getRoboFileCommands() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
119
        return $this->run($argv, $output, $app, $commandFiles);
0 ignored issues
show
Documentation introduced by
$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...
Documentation introduced by
$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...
120
    }
121
122
    /**
123
     * @param null|\Symfony\Component\Console\Input\InputInterface $input
124
     * @param null|\Symfony\Component\Console\Output\OutputInterface $output
125
     * @param null|\Robo\Application $app
126
     * @param array[] $commandFiles
127
     *
128
     * @return int
129
     */
130
    public function run($input = null, $output = null, $app = null, $commandFiles = [])
131
    {
132
        // Create default input and output objects if they were not provided
133
        if (!$input) {
134
            $input = new StringInput('');
135
        }
136
        if (is_array($input)) {
137
            $input = new ArgvInput($input);
138
        }
139
        if (!$output) {
140
            $output = new \Symfony\Component\Console\Output\ConsoleOutput();
141
        }
142
        $this->setInput($input);
143
        $this->setOutput($output);
144
145
        // If we were not provided a container, then create one
146
        if (!$this->getContainer()) {
147
            $config = Robo::createConfiguration(['robo.yml']);
148
            $container = Robo::createDefaultContainer($input, $output, $app, $config);
149
            $this->setContainer($container);
150
            // Automatically register a shutdown function and
151
            // an error handler when we provide the container.
152
            $this->installRoboHandlers();
153
        }
154
155
        if (!$app) {
156
            $app = Robo::application();
157
        }
158
        if (!isset($commandFiles)) {
159
            $this->errorCondtion("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
160
            $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
161
            $commandFiles = [];
162
        }
163
        $this->registerCommandClasses($app, $commandFiles);
164
165
        try {
166
            $statusCode = $app->run($input, $output);
167
        } catch (TaskExitException $e) {
168
            $statusCode = $e->getCode() ?: 1;
169
        }
170
171
        // If there were any error conditions in bootstrapping Robo,
172
        // print them only if the requested command did not complete
173
        // successfully.
174
        if ($statusCode) {
175
            foreach ($this->errorConditions as $msg => $color) {
176
                $this->yell($msg, 40, $color);
177
            }
178
        }
179
        return $statusCode;
180
    }
181
182
    /**
183
     * @param \Symfony\Component\Console\Output\OutputInterface $output
184
     *
185
     * @return null|string
186
     */
187
    protected function getRoboFileCommands($output)
188
    {
189
        if (!$this->loadRoboFile($output)) {
190
            return;
191
        }
192
        return $this->roboClass;
193
    }
194
195
    /**
196
     * @param \Robo\Application $app
197
     * @param array $commandClasses
198
     */
199
    public function registerCommandClasses($app, $commandClasses)
200
    {
201
        foreach ((array)$commandClasses as $commandClass) {
202
            $this->registerCommandClass($app, $commandClass);
203
        }
204
    }
205
206
    /**
207
     * @param \Robo\Application $app
208
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
209
     *
210
     * @return mixed|void
211
     */
212
    public function registerCommandClass($app, $commandClass)
213
    {
214
        $container = Robo::getContainer();
215
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
216
        if (!$roboCommandFileInstance) {
217
            return;
218
        }
219
220
        // Register commands for all of the public methods in the RoboFile.
221
        $commandFactory = $container->get('commandFactory');
222
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
223
        foreach ($commandList as $command) {
224
            $app->add($command);
225
        }
226
        return $roboCommandFileInstance;
227
    }
228
229
    /**
230
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
231
     *
232
     * @return null|object
233
     */
234
    protected function instantiateCommandClass($commandClass)
235
    {
236
        $container = Robo::getContainer();
237
238
        // Register the RoboFile with the container and then immediately
239
        // fetch it; this ensures that all of the inflectors will run.
240
        // If the command class is already an instantiated object, then
241
        // just use it exactly as it was provided to us.
242
        if (is_string($commandClass)) {
243
            if (!class_exists($commandClass)) {
244
                return;
245
            }
246
            $reflectionClass = new \ReflectionClass($commandClass);
247
            if ($reflectionClass->isAbstract()) {
248
                return;
249
            }
250
251
            $commandFileName = "{$commandClass}Commands";
252
            $container->share($commandFileName, $commandClass);
253
            $commandClass = $container->get($commandFileName);
254
        }
255
        // If the command class is a Builder Aware Interface, then
256
        // ensure that it has a builder.  Every command class needs
257
        // its own collection builder, as they have references to each other.
258
        if ($commandClass instanceof BuilderAwareInterface) {
259
            $builder = CollectionBuilder::create($container, $commandClass);
260
            $commandClass->setBuilder($builder);
261
        }
262
        if ($commandClass instanceof ContainerAwareInterface) {
263
            $commandClass->setContainer($container);
264
        }
265
        return $commandClass;
266
    }
267
268
    public function installRoboHandlers()
269
    {
270
        register_shutdown_function(array($this, 'shutdown'));
271
        set_error_handler(array($this, 'handleError'));
272
    }
273
274
    /**
275
     * Process a shebang script, if one was used to launch this Runner.
276
     *
277
     * @param array $args
278
     *
279
     * @return array $args with shebang script removed
280
     */
281
    protected function shebang($args)
282
    {
283
        // Option 1: Shebang line names Robo, but includes no parameters.
284
        // #!/bin/env robo
285
        // The robo class may contain multiple commands; the user may
286
        // select which one to run, or even get a list of commands or
287
        // run 'help' on any of the available commands as usual.
288 View Code Duplication
        if ((count($args) > 1) && $this->isShebangFile($args[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
289
            return array_merge([$args[0]], array_slice($args, 2));
290
        }
291
        // Option 2: Shebang line stipulates which command to run.
292
        // #!/bin/env robo mycommand
293
        // The robo class must contain a public method named 'mycommand'.
294
        // This command will be executed every time.  Arguments and options
295
        // may be provided on the commandline as usual.
296 View Code Duplication
        if ((count($args) > 2) && $this->isShebangFile($args[2])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
298
        }
299
        return $args;
300
    }
301
302
    /**
303
     * Determine if the specified argument is a path to a shebang script.
304
     * If so, load it.
305
     *
306
     * @param string $filepath file to check
307
     *
308
     * @return bool Returns TRUE if shebang script was processed
309
     */
310
    protected function isShebangFile($filepath)
311
    {
312
        if (!is_file($filepath)) {
313
            return false;
314
        }
315
        $fp = fopen($filepath, "r");
316
        if ($fp === false) {
317
            return false;
318
        }
319
        $line = fgets($fp);
320
        $result = $this->isShebangLine($line);
321
        if ($result) {
322
            while ($line = fgets($fp)) {
323
                $line = trim($line);
324
                if ($line == '<?php') {
325
                    $script = stream_get_contents($fp);
326
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
327
                        $this->roboClass = $matches[1];
328
                        eval($script);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
329
                        $result = true;
330
                    }
331
                }
332
            }
333
        }
334
        fclose($fp);
335
336
        return $result;
337
    }
338
339
    /**
340
     * Test to see if the provided line is a robo 'shebang' line.
341
     *
342
     * @param string $line
343
     *
344
     * @return bool
345
     */
346
    protected function isShebangLine($line)
347
    {
348
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
349
    }
350
351
    /**
352
     * Check for Robo-specific arguments such as --load-from, process them,
353
     * and remove them from the array.  We have to process --load-from before
354
     * we set up Symfony Console.
355
     *
356
     * @param array $argv
357
     *
358
     * @return array
359
     */
360
    protected function processRoboOptions($argv)
361
    {
362
        // loading from other directory
363
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
364
        if ($pos === false) {
365
            return $argv;
366
        }
367
368
        $passThru = array_search('--', $argv);
369
        if (($passThru !== false) && ($passThru < $pos)) {
370
            return $argv;
371
        }
372
373
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
374
            $this->dir = substr($argv[$pos], 12);
375
        } elseif (isset($argv[$pos +1])) {
376
            $this->dir = $argv[$pos +1];
377
            unset($argv[$pos +1]);
378
        }
379
        unset($argv[$pos]);
380
        // Make adjustments if '--load-from' points at a file.
381
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
382
            $this->roboFile = basename($this->dir);
383
            $this->dir = dirname($this->dir);
384
            $className = basename($this->roboFile, '.php');
385
            if ($className != $this->roboFile) {
386
                $this->roboClass = $className;
387
            }
388
        }
389
        // Convert directory to a real path, but only if the
390
        // path exists. We do not want to lose the original
391
        // directory if the user supplied a bad value.
392
        $realDir = realpath($this->dir);
393
        if ($realDir) {
394
            chdir($realDir);
395
            $this->dir = $realDir;
396
        }
397
398
        return $argv;
399
    }
400
401
    /**
402
     * @param string $needle
403
     * @param string[] $haystack
404
     *
405
     * @return bool|int
406
     */
407
    protected function arraySearchBeginsWith($needle, $haystack)
408
    {
409
        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...
410
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
411
                return $i;
412
            }
413
        }
414
        return false;
415
    }
416
417
    public function shutdown()
418
    {
419
        $error = error_get_last();
420
        if (!is_array($error)) {
421
            return;
422
        }
423
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
424
    }
425
426
    /**
427
     * This is just a proxy error handler that checks the current error_reporting level.
428
     * In case error_reporting is disabled the error is marked as handled, otherwise
429
     * the normal internal error handling resumes.
430
     *
431
     * @return bool
432
     */
433
    public function handleError()
434
    {
435
        if (error_reporting() === 0) {
436
            return true;
437
        }
438
        return false;
439
    }
440
}
441