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;
13 use Consolidation\Config\Util\EnvConfig;
15 class Runner implements ContainerAwareInterface
17 const ROBOCLASS = 'RoboFile';
18 const ROBOFILE = 'RoboFile.php';
21 use ContainerAwareTrait;
34 * @var string working dir of Robo
41 protected $errorConditions = [];
44 * @var string GitHub Repo for SelfUpdate
46 protected $selfUpdateRepository = null;
49 * @var string filename to load configuration from (set to 'robo.yml' for RoboFiles)
51 protected $configFilename = 'conf.yml';
54 * @var string prefix for environment variable configuration overrides
56 protected $envConfigPrefix = false;
59 * @var \Composer\Autoload\ClassLoader
61 protected $classLoader = null;
66 protected $relativePluginNamespace;
71 * @param null|string $roboClass
72 * @param null|string $roboFile
74 public function __construct($roboClass = null, $roboFile = null)
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();
82 protected function errorCondition($msg, $errorType)
84 $this->errorConditions[$msg] = $errorType;
88 * @param \Symfony\Component\Console\Output\OutputInterface $output
92 protected function loadRoboFile($output)
94 // If we have not been provided an output object, make a temporary one.
96 $output = new \Symfony\Component\Console\Output\ConsoleOutput();
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)) {
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');
111 $realDir = realpath($this->dir);
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');
119 require_once $roboFilePath;
121 if (!class_exists($this->roboClass)) {
122 $this->errorCondition("Class {$this->roboClass} was not loaded.", 'red');
130 * @param null|string $appName
131 * @param null|string $appVersion
132 * @param null|\Symfony\Component\Console\Output\OutputInterface $output
136 public function execute($argv, $appName = null, $appVersion = null, $output = null)
138 $argv = $this->shebang($argv);
139 $argv = $this->processRoboOptions($argv);
141 if ($appName && $appVersion) {
142 $app = Robo::createDefaultApplication($appName, $appVersion);
144 $commandFiles = $this->getRoboFileCommands($output);
145 return $this->run($argv, $output, $app, $commandFiles, $this->classLoader);
149 * Get a list of locations where config files may be loaded
152 protected function getConfigFilePaths($userConfig)
154 $roboAppConfig = dirname(__DIR__) . '/' . basename($userConfig);
155 $configFiles = [$userConfig, $roboAppConfig];
156 if (dirname($userConfig) != '.') {
157 array_unshift($configFiles, basename($userConfig));
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
170 public function run($input = null, $output = null, $app = null, $commandFiles = [], $classLoader = null)
172 // Create default input and output objects if they were not provided
174 $input = new StringInput('');
176 if (is_array($input)) {
177 $input = new ArgvInput($input);
180 $output = new \Symfony\Component\Console\Output\ConsoleOutput();
182 $this->setInput($input);
183 $this->setOutput($output);
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);
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();
201 $app = Robo::application();
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);
212 if (!empty($this->relativePluginNamespace)) {
213 $commandClasses = $this->discoverCommandClasses($this->relativePluginNamespace);
214 $commandFiles = array_merge((array)$commandFiles, $commandClasses);
217 $this->registerCommandClasses($app, $commandFiles);
220 $statusCode = $app->run($input, $output);
221 } catch (TaskExitException $e) {
222 $statusCode = $e->getCode() ?: 1;
225 // If there were any error conditions in bootstrapping Robo,
226 // print them only if the requested command did not complete
229 foreach ($this->errorConditions as $msg => $color) {
230 $this->yell($msg, 40, $color);
237 * @param \Symfony\Component\Console\Output\OutputInterface $output
239 * @return null|string
241 protected function getRoboFileCommands($output)
243 if (!$this->loadRoboFile($output)) {
246 return $this->roboClass;
250 * @param \Robo\Application $app
251 * @param array $commandClasses
253 public function registerCommandClasses($app, $commandClasses)
255 foreach ((array)$commandClasses as $commandClass) {
256 $this->registerCommandClass($app, $commandClass);
261 * @param $relativeNamespace
263 * @return array|string[]
265 protected function discoverCommandClasses($relativeNamespace)
267 /** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
268 $discovery = Robo::service('relativeNamespaceDiscovery');
269 $discovery->setRelativeNamespace($relativeNamespace.'\Commands')
270 ->setSearchPattern('*Commands.php');
271 return $discovery->getClasses();
275 * @param \Robo\Application $app
276 * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
280 public function registerCommandClass($app, $commandClass)
282 $container = Robo::getContainer();
283 $roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
284 if (!$roboCommandFileInstance) {
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) {
294 return $roboCommandFileInstance;
298 * @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
300 * @return null|object
302 protected function instantiateCommandClass($commandClass)
304 $container = Robo::getContainer();
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)) {
314 $reflectionClass = new \ReflectionClass($commandClass);
315 if ($reflectionClass->isAbstract()) {
319 $commandFileName = "{$commandClass}Commands";
320 $container->share($commandFileName, $commandClass);
321 $commandClass = $container->get($commandFileName);
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);
330 if ($commandClass instanceof ContainerAwareInterface) {
331 $commandClass->setContainer($container);
333 return $commandClass;
336 public function installRoboHandlers()
338 register_shutdown_function(array($this, 'shutdown'));
339 set_error_handler(array($this, 'handleError'));
343 * Process a shebang script, if one was used to launch this Runner.
347 * @return array $args with shebang script removed
349 protected function shebang($args)
351 // Option 1: Shebang line names Robo, but includes no parameters.
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));
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));
371 * Determine if the specified argument is a path to a shebang script.
374 * @param string $filepath file to check
376 * @return bool Returns TRUE if shebang script was processed
378 protected function isShebangFile($filepath)
380 // Avoid trying to call $filepath on remote URLs
381 if ((strpos($filepath, '://') !== false) && (substr($filepath, 0, 7) != 'file://')) {
384 if (!is_file($filepath)) {
387 $fp = fopen($filepath, "r");
392 $result = $this->isShebangLine($line);
394 while ($line = fgets($fp)) {
396 if ($line == '<?php') {
397 $script = stream_get_contents($fp);
398 if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
399 $this->roboClass = $matches[1];
412 * Test to see if the provided line is a robo 'shebang' line.
414 * @param string $line
418 protected function isShebangLine($line)
420 return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
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.
432 protected function processRoboOptions($argv)
434 // loading from other directory
435 $pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
436 if ($pos === false) {
440 $passThru = array_search('--', $argv);
441 if (($passThru !== false) && ($passThru < $pos)) {
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]);
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;
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);
467 $this->dir = $realDir;
474 * @param string $needle
475 * @param string[] $haystack
479 protected function arraySearchBeginsWith($needle, $haystack)
481 for ($i = 0; $i < count($haystack); ++$i) {
482 if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
489 public function shutdown()
491 $error = error_get_last();
492 if (!is_array($error)) {
495 $this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
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.
505 public function handleError()
507 if (error_reporting() === 0) {
516 public function getSelfUpdateRepository()
518 return $this->selfUpdateRepository;
522 * @param $selfUpdateRepository
526 public function setSelfUpdateRepository($selfUpdateRepository)
528 $this->selfUpdateRepository = $selfUpdateRepository;
533 * @param string $configFilename
537 public function setConfigurationFilename($configFilename)
539 $this->configFilename = $configFilename;
544 * @param string $envConfigPrefix
548 public function setEnvConfigPrefix($envConfigPrefix)
550 $this->envConfigPrefix = $envConfigPrefix;
555 * @param \Composer\Autoload\ClassLoader $classLoader
559 public function setClassLoader(ClassLoader $classLoader)
561 $this->classLoader = $classLoader;
566 * @param string $relativeNamespace
570 public function setRelativePluginNamespace($relativeNamespace)
572 $this->relativePluginNamespace = $relativeNamespace;