Completed
Push — master ( 840b35...de5768 )
by Greg
03:10
created

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