Completed
Pull Request — master (#615)
by
unknown
02:42
created

Runner::setSelfUpdateRepository()   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 1
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();
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...
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) {
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...
120
            $app = Robo::createDefaultApplication($appName, $appVersion);
121
        }
122
        $commandFiles = $this->getRoboFileCommands($output);
0 ignored issues
show
Bug introduced by
It seems like $output defined by parameter $output on line 114 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...
123
        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...
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
            $app->addSelfUpdateCommand($this->getSelfUpdateRepository());
164
        }
165
        if (!isset($commandFiles)) {
166
            $this->errorCondtion("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
167
            $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
168
            $commandFiles = [];
169
        }
170
        $this->registerCommandClasses($app, $commandFiles);
171
172
        try {
173
            $statusCode = $app->run($input, $output);
174
        } catch (TaskExitException $e) {
175
            $statusCode = $e->getCode() ?: 1;
176
        }
177
178
        // If there were any error conditions in bootstrapping Robo,
179
        // print them only if the requested command did not complete
180
        // successfully.
181
        if ($statusCode) {
182
            foreach ($this->errorConditions as $msg => $color) {
183
                $this->yell($msg, 40, $color);
184
            }
185
        }
186
        return $statusCode;
187
    }
188
189
    /**
190
     * @param \Symfony\Component\Console\Output\OutputInterface $output
191
     *
192
     * @return null|string
193
     */
194
    protected function getRoboFileCommands($output)
195
    {
196
        if (!$this->loadRoboFile($output)) {
197
            return;
198
        }
199
        return $this->roboClass;
200
    }
201
202
    /**
203
     * @param \Robo\Application $app
204
     * @param array $commandClasses
205
     */
206
    public function registerCommandClasses($app, $commandClasses)
207
    {
208
        foreach ((array)$commandClasses as $commandClass) {
209
            $this->registerCommandClass($app, $commandClass);
210
        }
211
    }
212
213
    /**
214
     * @param \Robo\Application $app
215
     * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
216
     *
217
     * @return mixed|void
218
     */
219
    public function registerCommandClass($app, $commandClass)
220
    {
221
        $container = Robo::getContainer();
222
        $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
223
        if (!$roboCommandFileInstance) {
224
            return;
225
        }
226
227
        // Register commands for all of the public methods in the RoboFile.
228
        $commandFactory = $container->get('commandFactory');
229
        $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
230
        foreach ($commandList as $command) {
231
            $app->add($command);
232
        }
233
        return $roboCommandFileInstance;
234
    }
235
236
    /**
237
     * @param string|BuilderAwareInterface|ContainerAwareInterface  $commandClass
238
     *
239
     * @return null|object
240
     */
241
    protected function instantiateCommandClass($commandClass)
242
    {
243
        $container = Robo::getContainer();
244
245
        // Register the RoboFile with the container and then immediately
246
        // fetch it; this ensures that all of the inflectors will run.
247
        // If the command class is already an instantiated object, then
248
        // just use it exactly as it was provided to us.
249
        if (is_string($commandClass)) {
250
            if (!class_exists($commandClass)) {
251
                return;
252
            }
253
            $reflectionClass = new \ReflectionClass($commandClass);
254
            if ($reflectionClass->isAbstract()) {
255
                return;
256
            }
257
258
            $commandFileName = "{$commandClass}Commands";
259
            $container->share($commandFileName, $commandClass);
260
            $commandClass = $container->get($commandFileName);
261
        }
262
        // If the command class is a Builder Aware Interface, then
263
        // ensure that it has a builder.  Every command class needs
264
        // its own collection builder, as they have references to each other.
265
        if ($commandClass instanceof BuilderAwareInterface) {
266
            $builder = CollectionBuilder::create($container, $commandClass);
267
            $commandClass->setBuilder($builder);
268
        }
269
        if ($commandClass instanceof ContainerAwareInterface) {
270
            $commandClass->setContainer($container);
271
        }
272
        return $commandClass;
273
    }
274
275
    public function installRoboHandlers()
276
    {
277
        register_shutdown_function(array($this, 'shutdown'));
278
        set_error_handler(array($this, 'handleError'));
279
    }
280
281
    /**
282
     * Process a shebang script, if one was used to launch this Runner.
283
     *
284
     * @param array $args
285
     *
286
     * @return array $args with shebang script removed
287
     */
288
    protected function shebang($args)
289
    {
290
        // Option 1: Shebang line names Robo, but includes no parameters.
291
        // #!/bin/env robo
292
        // The robo class may contain multiple commands; the user may
293
        // select which one to run, or even get a list of commands or
294
        // run 'help' on any of the available commands as usual.
295 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...
296
            return array_merge([$args[0]], array_slice($args, 2));
297
        }
298
        // Option 2: Shebang line stipulates which command to run.
299
        // #!/bin/env robo mycommand
300
        // The robo class must contain a public method named 'mycommand'.
301
        // This command will be executed every time.  Arguments and options
302
        // may be provided on the commandline as usual.
303 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...
304
            return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
305
        }
306
        return $args;
307
    }
308
309
    /**
310
     * Determine if the specified argument is a path to a shebang script.
311
     * If so, load it.
312
     *
313
     * @param string $filepath file to check
314
     *
315
     * @return bool Returns TRUE if shebang script was processed
316
     */
317
    protected function isShebangFile($filepath)
318
    {
319
        if (!is_file($filepath)) {
320
            return false;
321
        }
322
        $fp = fopen($filepath, "r");
323
        if ($fp === false) {
324
            return false;
325
        }
326
        $line = fgets($fp);
327
        $result = $this->isShebangLine($line);
328
        if ($result) {
329
            while ($line = fgets($fp)) {
330
                $line = trim($line);
331
                if ($line == '<?php') {
332
                    $script = stream_get_contents($fp);
333
                    if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
334
                        $this->roboClass = $matches[1];
335
                        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...
336
                        $result = true;
337
                    }
338
                }
339
            }
340
        }
341
        fclose($fp);
342
343
        return $result;
344
    }
345
346
    /**
347
     * Test to see if the provided line is a robo 'shebang' line.
348
     *
349
     * @param string $line
350
     *
351
     * @return bool
352
     */
353
    protected function isShebangLine($line)
354
    {
355
        return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
356
    }
357
358
    /**
359
     * Check for Robo-specific arguments such as --load-from, process them,
360
     * and remove them from the array.  We have to process --load-from before
361
     * we set up Symfony Console.
362
     *
363
     * @param array $argv
364
     *
365
     * @return array
366
     */
367
    protected function processRoboOptions($argv)
368
    {
369
        // loading from other directory
370
        $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
371
        if ($pos === false) {
372
            return $argv;
373
        }
374
375
        $passThru = array_search('--', $argv);
376
        if (($passThru !== false) && ($passThru < $pos)) {
377
            return $argv;
378
        }
379
380
        if (substr($argv[$pos], 0, 12) == '--load-from=') {
381
            $this->dir = substr($argv[$pos], 12);
382
        } elseif (isset($argv[$pos +1])) {
383
            $this->dir = $argv[$pos +1];
384
            unset($argv[$pos +1]);
385
        }
386
        unset($argv[$pos]);
387
        // Make adjustments if '--load-from' points at a file.
388
        if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
389
            $this->roboFile = basename($this->dir);
390
            $this->dir = dirname($this->dir);
391
            $className = basename($this->roboFile, '.php');
392
            if ($className != $this->roboFile) {
393
                $this->roboClass = $className;
394
            }
395
        }
396
        // Convert directory to a real path, but only if the
397
        // path exists. We do not want to lose the original
398
        // directory if the user supplied a bad value.
399
        $realDir = realpath($this->dir);
400
        if ($realDir) {
401
            chdir($realDir);
402
            $this->dir = $realDir;
403
        }
404
405
        return $argv;
406
    }
407
408
    /**
409
     * @param string $needle
410
     * @param string[] $haystack
411
     *
412
     * @return bool|int
413
     */
414
    protected function arraySearchBeginsWith($needle, $haystack)
415
    {
416
        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...
417
            if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
418
                return $i;
419
            }
420
        }
421
        return false;
422
    }
423
424
    public function shutdown()
425
    {
426
        $error = error_get_last();
427
        if (!is_array($error)) {
428
            return;
429
        }
430
        $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
431
    }
432
433
    /**
434
     * This is just a proxy error handler that checks the current error_reporting level.
435
     * In case error_reporting is disabled the error is marked as handled, otherwise
436
     * the normal internal error handling resumes.
437
     *
438
     * @return bool
439
     */
440
    public function handleError()
441
    {
442
        if (error_reporting() === 0) {
443
            return true;
444
        }
445
        return false;
446
    }
447
448
    /**
449
     * @return string
450
     */
451
    public function getSelfUpdateRepository()
452
    {
453
        return $this->selfUpdateRepository;
454
    }
455
456
    /**
457
     * @param string $selfUpdateRepository
458
     */
459
    public function setSelfUpdateRepository($selfUpdateRepository)
460
    {
461
        $this->selfUpdateRepository = $selfUpdateRepository;
462
    }
463
}
464