Completed
Pull Request — master (#671)
by Antonio
02:54
created

Runner::setClassLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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