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;
10 use Robo\Exception\TaskExitException;
11 use League\Container\ContainerAwareInterface;
12 use League\Container\ContainerAwareTrait;
14 class Runner implements ContainerAwareInterface
16 const ROBOCLASS = 'RoboFile';
17 const ROBOFILE = 'RoboFile.php';
20 use ContainerAwareTrait;
33 * @var string working dir of Robo
40 protected $errorConditions = [];
43 * @var string GitHub Repo for SelfUpdate
45 protected $selfUpdateRepository = null;
48 * @var \Composer\Autoload\ClassLoader
50 protected $classLoader = null;
55 protected $relativePluginNamespace;
60 * @param null|string $roboClass
61 * @param null|string $roboFile
63 public function __construct($roboClass = null, $roboFile = null)
65 // set the const as class properties to allow overwriting in child classes
66 $this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
67 $this->roboFile = $roboFile ? $roboFile : self::ROBOFILE;
68 $this->dir = getcwd();
71 protected function errorCondition($msg, $errorType)
73 $this->errorConditions[$msg] = $errorType;
77 * @param \Symfony\Component\Console\Output\OutputInterface $output
81 protected function loadRoboFile($output)
83 // If we have not been provided an output object, make a temporary one.
85 $output = new \Symfony\Component\Console\Output\ConsoleOutput();
88 // If $this->roboClass is a single class that has not already
89 // been loaded, then we will try to obtain it from $this->roboFile.
90 // If $this->roboClass is an array, we presume all classes requested
91 // are available via the autoloader.
92 if (is_array($this->roboClass) || class_exists($this->roboClass)) {
95 if (!file_exists($this->dir)) {
96 $this->errorCondition("Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.", 'red');
100 $realDir = realpath($this->dir);
102 $roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
103 if (!file_exists($roboFilePath)) {
104 $requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
105 $this->errorCondition("Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile.", 'red');
108 require_once $roboFilePath;
110 if (!class_exists($this->roboClass)) {
111 $this->errorCondition("Class {$this->roboClass} was not loaded.", 'red');
119 * @param null|string $appName
120 * @param null|string $appVersion
121 * @param null|\Symfony\Component\Console\Output\OutputInterface $output
125 public function execute($argv, $appName = null, $appVersion = null, $output = null)
127 $argv = $this->shebang($argv);
128 $argv = $this->processRoboOptions($argv);
130 if ($appName && $appVersion) {
131 $app = Robo::createDefaultApplication($appName, $appVersion);
133 $commandFiles = $this->getRoboFileCommands($output);
134 return $this->run($argv, $output, $app, $commandFiles, $this->classLoader);
138 * @param null|\Symfony\Component\Console\Input\InputInterface $input
139 * @param null|\Symfony\Component\Console\Output\OutputInterface $output
140 * @param null|\Robo\Application $app
141 * @param array[] $commandFiles
142 * @param null|ClassLoader $classLoader
146 public function run($input = null, $output = null, $app = null, $commandFiles = [], $classLoader = null)
148 // Create default input and output objects if they were not provided
150 $input = new StringInput('');
152 if (is_array($input)) {
153 $input = new ArgvInput($input);
156 $output = new \Symfony\Component\Console\Output\ConsoleOutput();
158 $this->setInput($input);
159 $this->setOutput($output);
161 // If we were not provided a container, then create one
162 if (!$this->getContainer()) {
163 $userConfig = 'robo.yml';
164 $roboAppConfig = dirname(__DIR__) . '/robo.yml';
165 $config = Robo::createConfiguration([$userConfig, $roboAppConfig]);
166 $container = Robo::createDefaultContainer($input, $output, $app, $config, $classLoader);
167 $this->setContainer($container);
168 // Automatically register a shutdown function and
169 // an error handler when we provide the container.
170 $this->installRoboHandlers();
174 $app = Robo::application();
176 if ($app instanceof \Robo\Application) {
177 $app->addSelfUpdateCommand($this->getSelfUpdateRepository());
178 if (!isset($commandFiles)) {
179 $this->errorCondition("Robo is not initialized here. Please run `robo init` to create a new RoboFile.", 'yellow');
180 $app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
185 if (!empty($this->relativePluginNamespace)) {
186 $commandClasses = $this->discoverCommandClasses($this->relativePluginNamespace);
187 $commandFiles = array_merge((array)$commandFiles, $commandClasses);
190 $this->registerCommandClasses($app, $commandFiles);
193 $statusCode = $app->run($input, $output);
194 } catch (TaskExitException $e) {
195 $statusCode = $e->getCode() ?: 1;
198 // If there were any error conditions in bootstrapping Robo,
199 // print them only if the requested command did not complete
202 foreach ($this->errorConditions as $msg => $color) {
203 $this->yell($msg, 40, $color);
210 * @param \Symfony\Component\Console\Output\OutputInterface $output
212 * @return null|string
214 protected function getRoboFileCommands($output)
216 if (!$this->loadRoboFile($output)) {
219 return $this->roboClass;
223 * @param \Robo\Application $app
224 * @param array $commandClasses
226 public function registerCommandClasses($app, $commandClasses)
228 foreach ((array)$commandClasses as $commandClass) {
229 $this->registerCommandClass($app, $commandClass);
234 * @param $relativeNamespace
236 * @return array|string[]
238 protected function discoverCommandClasses($relativeNamespace)
240 /** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
241 $discovery = Robo::service('relativeNamespaceDiscovery');
242 $discovery->setRelativeNamespace($relativeNamespace.'\Commands')
243 ->setSearchPattern('*Commands.php');
244 return $discovery->getClasses();
248 * @param \Robo\Application $app
249 * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
253 public function registerCommandClass($app, $commandClass)
255 $container = Robo::getContainer();
256 $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
257 if (!$roboCommandFileInstance) {
261 // Register commands for all of the public methods in the RoboFile.
262 $commandFactory = $container->get('commandFactory');
263 $commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
264 foreach ($commandList as $command) {
267 return $roboCommandFileInstance;
271 * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
273 * @return null|object
275 protected function instantiateCommandClass($commandClass)
277 $container = Robo::getContainer();
279 // Register the RoboFile with the container and then immediately
280 // fetch it; this ensures that all of the inflectors will run.
281 // If the command class is already an instantiated object, then
282 // just use it exactly as it was provided to us.
283 if (is_string($commandClass)) {
284 if (!class_exists($commandClass)) {
287 $reflectionClass = new \ReflectionClass($commandClass);
288 if ($reflectionClass->isAbstract()) {
292 $commandFileName = "{$commandClass}Commands";
293 $container->share($commandFileName, $commandClass);
294 $commandClass = $container->get($commandFileName);
296 // If the command class is a Builder Aware Interface, then
297 // ensure that it has a builder. Every command class needs
298 // its own collection builder, as they have references to each other.
299 if ($commandClass instanceof BuilderAwareInterface) {
300 $builder = CollectionBuilder::create($container, $commandClass);
301 $commandClass->setBuilder($builder);
303 if ($commandClass instanceof ContainerAwareInterface) {
304 $commandClass->setContainer($container);
306 return $commandClass;
309 public function installRoboHandlers()
311 register_shutdown_function(array($this, 'shutdown'));
312 set_error_handler(array($this, 'handleError'));
316 * Process a shebang script, if one was used to launch this Runner.
320 * @return array $args with shebang script removed
322 protected function shebang($args)
324 // Option 1: Shebang line names Robo, but includes no parameters.
326 // The robo class may contain multiple commands; the user may
327 // select which one to run, or even get a list of commands or
328 // run 'help' on any of the available commands as usual.
329 if ((count($args) > 1) && $this->isShebangFile($args[1])) {
330 return array_merge([$args[0]], array_slice($args, 2));
332 // Option 2: Shebang line stipulates which command to run.
333 // #!/bin/env robo mycommand
334 // The robo class must contain a public method named 'mycommand'.
335 // This command will be executed every time. Arguments and options
336 // may be provided on the commandline as usual.
337 if ((count($args) > 2) && $this->isShebangFile($args[2])) {
338 return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
344 * Determine if the specified argument is a path to a shebang script.
347 * @param string $filepath file to check
349 * @return bool Returns TRUE if shebang script was processed
351 protected function isShebangFile($filepath)
353 if (!is_file($filepath)) {
356 $fp = fopen($filepath, "r");
361 $result = $this->isShebangLine($line);
363 while ($line = fgets($fp)) {
365 if ($line == '<?php') {
366 $script = stream_get_contents($fp);
367 if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
368 $this->roboClass = $matches[1];
381 * Test to see if the provided line is a robo 'shebang' line.
383 * @param string $line
387 protected function isShebangLine($line)
389 return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
393 * Check for Robo-specific arguments such as --load-from, process them,
394 * and remove them from the array. We have to process --load-from before
395 * we set up Symfony Console.
401 protected function processRoboOptions($argv)
403 // loading from other directory
404 $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
405 if ($pos === false) {
409 $passThru = array_search('--', $argv);
410 if (($passThru !== false) && ($passThru < $pos)) {
414 if (substr($argv[$pos], 0, 12) == '--load-from=') {
415 $this->dir = substr($argv[$pos], 12);
416 } elseif (isset($argv[$pos +1])) {
417 $this->dir = $argv[$pos +1];
418 unset($argv[$pos +1]);
421 // Make adjustments if '--load-from' points at a file.
422 if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
423 $this->roboFile = basename($this->dir);
424 $this->dir = dirname($this->dir);
425 $className = basename($this->roboFile, '.php');
426 if ($className != $this->roboFile) {
427 $this->roboClass = $className;
430 // Convert directory to a real path, but only if the
431 // path exists. We do not want to lose the original
432 // directory if the user supplied a bad value.
433 $realDir = realpath($this->dir);
436 $this->dir = $realDir;
443 * @param string $needle
444 * @param string[] $haystack
448 protected function arraySearchBeginsWith($needle, $haystack)
450 for ($i = 0; $i < count($haystack); ++$i) {
451 if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
458 public function shutdown()
460 $error = error_get_last();
461 if (!is_array($error)) {
464 $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
468 * This is just a proxy error handler that checks the current error_reporting level.
469 * In case error_reporting is disabled the error is marked as handled, otherwise
470 * the normal internal error handling resumes.
474 public function handleError()
476 if (error_reporting() === 0) {
485 public function getSelfUpdateRepository()
487 return $this->selfUpdateRepository;
491 * @param $selfUpdateRepository
495 public function setSelfUpdateRepository($selfUpdateRepository)
497 $this->selfUpdateRepository = $selfUpdateRepository;
502 * @param \Composer\Autoload\ClassLoader $classLoader
506 public function setClassLoader(ClassLoader $classLoader)
508 $this->classLoader = $classLoader;
513 * @param string $relativeNamespace
517 public function setRelativePluginNamespace($relativeNamespace)
519 $this->relativePluginNamespace = $relativeNamespace;