From 34b562a545bd4193d321369b476b2f1ee29d675d Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sun, 25 Mar 2018 13:42:46 +0300 Subject: [PATCH 1/3] OPENEUROPA-372: Allow custom commnds options. --- README.md | 40 +++++++++++++++++++++++++++++ src/Commands/DynamicCommands.php | 43 +++++++++++++++++++++++++++++--- src/TaskRunner.php | 22 +++++++++++++--- tests/CommandsTest.php | 6 ++--- tests/fixtures/setup.yml | 33 ++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 39969bd8..a1933d90 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,46 @@ At the moment the following tasks are supported (optional argument default value Tasks provided as plain-text strings will be executed as is in the current working directory. +### Advanced custom commands + +You can define also command options along with a custom command. Let's see how the previous defined `setup:behat` custom command can have its own command options: + +```yaml +commands: + setup:behat: + # When you need to define command options, the list of tasks should be + # placed under the 'tasks' key... + tasks: + - { task: "process", source: "behat.yml.dist", destination: "behat.yml" } + # ...and option definitions are under 'options' key. + options: + # The option name, without the leading double dash ('--'). + webdriver-url: + # Optional. If this key is present, the input option value is assigned + # to this configuration entry. This a key feature because in this way + # you're able to override configuration values, making it very helpful + # in CI flows. + config: behat.webdriver_url + # Optional. You can provide a list of shortcuts to the command, without + # adding the dash ('-') prefix. + shortcut: + - wdu + - wurl + # The mode of this option. See the Symfony `InputOption::VALUE_*` + # constants. Several options can be combined. + # @see \Symfony\Component\Console\Input\InputOption::VALUE_NONE + # @see \Symfony\Component\Console\Input\InputOption::VALUE_REQUIRED + # @see \Symfony\Component\Console\Input\InputOption::VALUE_OPTIONAL + # @see \Symfony\Component\Console\Input\InputOption::VALUE_IS_ARRAY + mode: 4 + # Optional. A description for this option. This is displayed when + # asking for help. E.g. `./vendor/bin/run setup:behat --help`. + description: 'The webdriver URL.' + # Optional. A default value when an optional option is not present in + # the input. + default: null +``` + ## Expose custom commands as PHP classes More complex commands can be provided by creating Task Runner command classes within your project's PSR-4 namespace. diff --git a/src/Commands/DynamicCommands.php b/src/Commands/DynamicCommands.php index 73b9322f..2f659c8f 100644 --- a/src/Commands/DynamicCommands.php +++ b/src/Commands/DynamicCommands.php @@ -2,10 +2,8 @@ namespace OpenEuropa\TaskRunner\Commands; -use Consolidation\AnnotatedCommand\AnnotationData; -use Consolidation\AnnotatedCommand\CommandData; +use Consolidation\AnnotatedCommand\AnnotatedCommand; use OpenEuropa\TaskRunner\Tasks as TaskRunnerTasks; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Event\ConsoleCommandEvent; /** @@ -18,6 +16,8 @@ class DynamicCommands extends AbstractCommands use TaskRunnerTasks\CollectionFactory\loadTasks; /** + * @dynamic-command true + * * @return \OpenEuropa\TaskRunner\Tasks\CollectionFactory\CollectionFactory */ public function runTasks() @@ -27,4 +27,41 @@ public function runTasks() return $this->taskCollectionFactory($tasks); } + + /** + * Bind input values of custom command options to config entries. + * + * @param \Symfony\Component\Console\Event\ConsoleCommandEvent $event + * + * @hook pre-command-event * + */ + public function bindInputOptionsToConfig(ConsoleCommandEvent $event) + { + $command = $event->getCommand(); + if (get_class($command) !== AnnotatedCommand::class && !is_subclass_of($command, AnnotatedCommand::class)) { + return; + } + + /** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */ + /** @var \Consolidation\AnnotatedCommand\AnnotationData $annotatedData */ + $annotatedData = $command->getAnnotationData(); + if (!$annotatedData->get('dynamic-command')) { + return; + } + + // Dynamic commands may define their own options bound to specific configuration. Dynamically set the + // configuration from command options. + $config = $this->getConfig(); + $commands = $config->get('commands'); + if (!empty($commands[$command->getName()]['options'])) { + foreach ($commands[$command->getName()]['options'] as $optionName => $option) { + if (!empty($option['config']) && $event->getInput()->hasOption($optionName)) { + $inputValue = $event->getInput()->getOption($optionName); + if ($inputValue !== null) { + $config->set($option['config'], $event->getInput()->getOption($optionName)); + } + } + } + } + } } diff --git a/src/TaskRunner.php b/src/TaskRunner.php index b294ebf2..f0bdae7f 100644 --- a/src/TaskRunner.php +++ b/src/TaskRunner.php @@ -4,11 +4,11 @@ use Composer\Autoload\ClassLoader; use Consolidation\AnnotatedCommand\CommandFileDiscovery; +use League\Container\ContainerAwareTrait; use OpenEuropa\TaskRunner\Commands\DynamicCommands; use OpenEuropa\TaskRunner\Contract\ComposerAwareInterface; -use OpenEuropa\TaskRunner\Services\Composer; use OpenEuropa\TaskRunner\Contract\FilesystemAwareInterface; -use League\Container\ContainerAwareTrait; +use OpenEuropa\TaskRunner\Services\Composer; use Robo\Application; use Robo\Common\ConfigAwareTrait; use Robo\Config\Config; @@ -211,13 +211,29 @@ private function getWorkingDir(InputInterface $input) */ private function registerDynamicCommands(Application $application) { - foreach ($this->getConfig()->get('commands', []) as $name => $tasks) { + foreach ($this->getConfig()->get('commands', []) as $name => $commandDefinition) { /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */ $commandFileName = DynamicCommands::class."Commands"; $commandClass = $this->container->get($commandFileName); $commandFactory = $this->container->get('commandFactory'); $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks'); $command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name); + + // Dynamic commands may define their own options. + if (!empty($commandDefinition['options'])) { + $defaults = array_fill_keys(['shortcut', 'mode', 'description', 'default'], null); + foreach ($commandDefinition['options'] as $optionName => $optionDefinition) { + $optionDefinition += $defaults; + $command->addOption( + "--$optionName", + $optionDefinition['shortcut'], + $optionDefinition['mode'], + $optionDefinition['description'], + $optionDefinition['default'] + ); + } + } + $application->add($command); } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 6b059dba..de310fd5 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -1,11 +1,9 @@ + +- command: "setup:behat --drupal-base-url=http://localhost/drupal" + source: behat.yml.dist + destination: behat.yml + configuration: + drupal: + base_url: http://127.0.0.1 + commands: + setup:behat: + tasks: + - { task: "process", source: "behat.yml.dist", destination: "behat.yml" } + options: + drupal-base-url: + config: drupal.base_url + shortcut: + - dbu + mode: 4 + description: 'The Drupal base URL.' + default: null + content: > + default: + extensions: + Behat\MinkExtension: + base_url: ${drupal.base_url} + formatters: + progress: ~ + expected: > + default: + extensions: + Behat\MinkExtension: + base_url: http://localhost/drupal + formatters: + progress: ~ From b083b4a1a851561b6c7060c5c409bbacaecf1474 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sun, 25 Mar 2018 17:29:11 +0300 Subject: [PATCH 2/3] OPENEUROPA-372: Allow propagation of options. - A custom command that runs other commands as task will inherit their options. - Input options passed to a custom command are propagating to sub-tasks. --- src/Commands/DynamicCommands.php | 9 +- src/TaskRunner.php | 86 ++++++++++++++----- .../CollectionFactory/CollectionFactory.php | 39 +++++++-- src/Tasks/CollectionFactory/loadTasks.php | 5 +- 4 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/Commands/DynamicCommands.php b/src/Commands/DynamicCommands.php index 2f659c8f..0599bdda 100644 --- a/src/Commands/DynamicCommands.php +++ b/src/Commands/DynamicCommands.php @@ -25,7 +25,14 @@ public function runTasks() $command = $this->input()->getArgument('command'); $tasks = $this->getConfig()->get("commands.{$command}"); - return $this->taskCollectionFactory($tasks); + $inputOptions = []; + foreach ($this->input()->getOptions() as $name => $value) { + if ($this->input()->hasParameterOption("--$name")) { + $inputOptions[$name] = $value; + } + } + + return $this->taskCollectionFactory($tasks, $inputOptions); } /** diff --git a/src/TaskRunner.php b/src/TaskRunner.php index f0bdae7f..e6217f59 100644 --- a/src/TaskRunner.php +++ b/src/TaskRunner.php @@ -3,6 +3,7 @@ namespace OpenEuropa\TaskRunner; use Composer\Autoload\ClassLoader; +use Consolidation\AnnotatedCommand\AnnotatedCommand; use Consolidation\AnnotatedCommand\CommandFileDiscovery; use League\Container\ContainerAwareTrait; use OpenEuropa\TaskRunner\Commands\DynamicCommands; @@ -63,7 +64,7 @@ class TaskRunner /** * TaskRunner constructor. * - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface|null $output */ public function __construct(InputInterface $input = null, OutputInterface $output = null) @@ -118,7 +119,7 @@ public function registerExternalCommands(ClassLoader $classLoader) foreach ($classLoader->getPrefixesPsr4() as $baseNamespace => $directoryList) { $directoryList = array_filter($directoryList, function ($path) { - return is_dir($path.'/TaskRunner/Commands'); + return is_dir($path . '/TaskRunner/Commands'); }); if (!empty($directoryList)) { @@ -149,7 +150,7 @@ private function getCommandDiscovery() private function createConfiguration() { return Robo::createConfiguration([ - __DIR__.'/../config/runner.yml', + __DIR__ . '/../config/runner.yml', 'runner.yml.dist', 'runner.yml', ]); @@ -174,9 +175,9 @@ private function createContainer(InputInterface $input, OutputInterface $output, // Add service inflectors. $container->inflector(ComposerAwareInterface::class) - ->invokeMethod('setComposer', ['task_runner.composer']); + ->invokeMethod('setComposer', ['task_runner.composer']); $container->inflector(FilesystemAwareInterface::class) - ->invokeMethod('setFilesystem', ['filesystem']); + ->invokeMethod('setFilesystem', ['filesystem']); return $container; } @@ -190,8 +191,8 @@ private function createApplication() { $application = new Application(self::APPLICATION_NAME, null); $application - ->getDefinition() - ->addOption(new InputOption('--working-dir', null, InputOption::VALUE_REQUIRED, 'Working directory, defaults to current working directory.', $this->workingDir)); + ->getDefinition() + ->addOption(new InputOption('--working-dir', null, InputOption::VALUE_REQUIRED, 'Working directory, defaults to current working directory.', $this->workingDir)); return $application; } @@ -211,30 +212,75 @@ private function getWorkingDir(InputInterface $input) */ private function registerDynamicCommands(Application $application) { - foreach ($this->getConfig()->get('commands', []) as $name => $commandDefinition) { + $customCommands = $this->getConfig()->get('commands', []); + foreach ($customCommands as $name => $commandDefinition) { /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */ - $commandFileName = DynamicCommands::class."Commands"; + $commandFileName = DynamicCommands::class . "Commands"; $commandClass = $this->container->get($commandFileName); $commandFactory = $this->container->get('commandFactory'); $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks'); $command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name); // Dynamic commands may define their own options. - if (!empty($commandDefinition['options'])) { - $defaults = array_fill_keys(['shortcut', 'mode', 'description', 'default'], null); - foreach ($commandDefinition['options'] as $optionName => $optionDefinition) { - $optionDefinition += $defaults; - $command->addOption( - "--$optionName", - $optionDefinition['shortcut'], - $optionDefinition['mode'], - $optionDefinition['description'], - $optionDefinition['default'] - ); + $this->addOptions($command, $commandDefinition); + + // Append also options of subsequent tasks. + foreach ($this->getTasks($name) as $taskEntry) { + // This is a 'run' task. + if (is_array($taskEntry) && isset($taskEntry['task']) && ($taskEntry['task'] === 'run') && !empty($taskEntry['command'])) { + if (!empty($customCommands[$taskEntry['command']])) { + // Add the options of another custom command. + $this->addOptions($command, $customCommands[$taskEntry['command']]); + } else { + // Add the options of an already registered command. + $subCommand = $this->application->get($taskEntry['command']); + $command->addOptions($subCommand->getDefinition()->getOptions()); + } } } $application->add($command); } } + + /** + * @param \Consolidation\AnnotatedCommand\AnnotatedCommand $command + * @param array $commandDefinition + */ + private function addOptions(AnnotatedCommand $command, array $commandDefinition) + { + // This command doesn't define any option. + if (empty($commandDefinition['options'])) { + return; + } + + $defaults = array_fill_keys(['shortcut', 'mode', 'description', 'default'], null); + foreach ($commandDefinition['options'] as $optionName => $optionDefinition) { + $optionDefinition += $defaults; + $command->addOption( + "--$optionName", + $optionDefinition['shortcut'], + $optionDefinition['mode'], + $optionDefinition['description'], + $optionDefinition['default'] + ); + } + } + + /** + * @param string $command + * + * @return array + * + * @throws \InvalidArgumentException + */ + private function getTasks($command) + { + $commands = $this->getConfig()->get('commands', []); + if (!isset($commands[$command])) { + throw new \InvalidArgumentException("Custom command '$command' not defined."); + } + + return !empty($commands[$command]['tasks']) ? $commands[$command]['tasks'] : $commands[$command]; + } } diff --git a/src/Tasks/CollectionFactory/CollectionFactory.php b/src/Tasks/CollectionFactory/CollectionFactory.php index 8ecc2922..fbd3544f 100644 --- a/src/Tasks/CollectionFactory/CollectionFactory.php +++ b/src/Tasks/CollectionFactory/CollectionFactory.php @@ -2,17 +2,13 @@ namespace OpenEuropa\TaskRunner\Tasks\CollectionFactory; -use OpenEuropa\TaskRunner\Traits\ConfigurationTokensTrait; -use Robo\Common\BuilderAwareTrait; -use Robo\Common\TaskIO; +use OpenEuropa\TaskRunner as TaskRunner; use Robo\Contract\BuilderAwareInterface; use Robo\Contract\SimulatedInterface; use Robo\Exception\TaskException; use Robo\LoadAllTasks; -use Robo\Task as Task; -use OpenEuropa\TaskRunner as TaskRunner; +use Robo\Robo; use Robo\Task\BaseTask; -use Robo\TaskAccessor; use Symfony\Component\Yaml\Yaml; /** @@ -32,14 +28,21 @@ class CollectionFactory extends BaseTask implements BuilderAwareInterface, Simul */ protected $tasks; + /** + * @var array + */ + protected $options; + /** * CollectionFactory constructor. * * @param array $tasks + * @param array $options */ - public function __construct(array $tasks = []) + public function __construct(array $tasks = [], array $options = []) { $this->tasks = $tasks; + $this->options = $options; } /** @@ -144,7 +147,27 @@ protected function taskFactory($task) return $this->taskProcessConfigFile($task['source'], $task['destination']); case "run": - return $this->taskExec($this->getConfig()->get('runner.bin_dir').'/run')->arg($task['command']); + $taskExec = $this->taskExec($this->getConfig()->get('runner.bin_dir').'/run')->arg($task['command']); + + $container = Robo::getContainer(); + /** @var \Robo\Application $app */ + $app = $container->get('application'); + /** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */ + $command = $app->get($task['command']); + $commandOptions = $command->getDefinition()->getOptions(); + + // Propagate any input option passed to the parent command. + foreach ($this->options as $name => $values) { + // But only if the called command has this option. + if (isset($commandOptions[$name])) { + $values = (array) $values; + foreach ($values as $value) { + $taskExec->option($name, $value); + } + } + } + + return $taskExec; default: throw new TaskException($this, "Task '{$task['task']}' not supported."); diff --git a/src/Tasks/CollectionFactory/loadTasks.php b/src/Tasks/CollectionFactory/loadTasks.php index 7e07c6eb..9f24f76e 100644 --- a/src/Tasks/CollectionFactory/loadTasks.php +++ b/src/Tasks/CollectionFactory/loadTasks.php @@ -11,11 +11,12 @@ trait loadTasks { /** * @param array $tasks + * @param array $options * * @return \OpenEuropa\TaskRunner\Tasks\CollectionFactory\CollectionFactory */ - public function taskCollectionFactory(array $tasks) + public function taskCollectionFactory(array $tasks, array $options = []) { - return $this->task(CollectionFactory::class, $tasks); + return $this->task(CollectionFactory::class, $tasks, $options); } } From 62f1f8fe1d644ec8ebc2e00e4ee62d8650c97ce2 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sun, 25 Mar 2018 17:53:21 +0300 Subject: [PATCH 3/3] OPENEUROPA-372: PHP CS. --- src/TaskRunner.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TaskRunner.php b/src/TaskRunner.php index e6217f59..ad791285 100644 --- a/src/TaskRunner.php +++ b/src/TaskRunner.php @@ -64,7 +64,7 @@ class TaskRunner /** * TaskRunner constructor. * - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface|null $output */ public function __construct(InputInterface $input = null, OutputInterface $output = null) @@ -119,7 +119,7 @@ public function registerExternalCommands(ClassLoader $classLoader) foreach ($classLoader->getPrefixesPsr4() as $baseNamespace => $directoryList) { $directoryList = array_filter($directoryList, function ($path) { - return is_dir($path . '/TaskRunner/Commands'); + return is_dir($path.'/TaskRunner/Commands'); }); if (!empty($directoryList)) { @@ -150,7 +150,7 @@ private function getCommandDiscovery() private function createConfiguration() { return Robo::createConfiguration([ - __DIR__ . '/../config/runner.yml', + __DIR__.'/../config/runner.yml', 'runner.yml.dist', 'runner.yml', ]); @@ -215,7 +215,7 @@ private function registerDynamicCommands(Application $application) $customCommands = $this->getConfig()->get('commands', []); foreach ($customCommands as $name => $commandDefinition) { /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */ - $commandFileName = DynamicCommands::class . "Commands"; + $commandFileName = DynamicCommands::class."Commands"; $commandClass = $this->container->get($commandFileName); $commandFactory = $this->container->get('commandFactory'); $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');