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