Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom commands options, rebase of PR #44 #157

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,62 @@ 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" }
# An array of aliases.
aliases:
- subh
# The description.
description: Setup Behat testing.
# An array of usage info.
usages:
- 'setup:behat # Usage without option.'
- 'setup:behat --webdriver-url=localhost:8888 # With long option.'
- 'setup:behat --wdu=localhost:8888 # With short option.'
- '--wdu=localhost:8888 # Same as above: Command is prefixed if not given.'
# ...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, one of:
# - none (the default behavior if omitted)
# - required
# - optional
# - required-array
# - optional-array
# See the Symfony `InputOption::VALUE_*` constants.
# @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: optional
# 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.
Expand Down
6 changes: 5 additions & 1 deletion src/Commands/AbstractCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace OpenEuropa\TaskRunner\Commands;

use Consolidation\AnnotatedCommand\AnnotationData;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Loader\YamlConfigLoader;
use Robo\Common\ConfigAwareTrait;
use Robo\Common\IO;
use Robo\Contract\BuilderAwareInterface;
Expand Down Expand Up @@ -46,7 +48,9 @@ public function getConfigurationFile()
*/
public function initializeRuntimeConfiguration(ConsoleCommandEvent $event)
{
Robo::loadConfiguration([$this->getConfigurationFile()], $this->getConfig());
$loader = new YamlConfigLoader();
$loadedConfig = $loader->load($this->getConfigurationFile());
$this->config->combine($loadedConfig->export());
}

/**
Expand Down
45 changes: 44 additions & 1 deletion src/Commands/DynamicCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace OpenEuropa\TaskRunner\Commands;

use Consolidation\AnnotatedCommand\AnnotatedCommand;
use OpenEuropa\TaskRunner\Tasks as TaskRunnerTasks;
use Robo\Robo;
use Symfony\Component\Console\Event\ConsoleCommandEvent;

/**
* Command class for dynamic commands.
Expand All @@ -19,15 +21,56 @@ class DynamicCommands extends AbstractCommands
use TaskRunnerTasks\CollectionFactory\loadTasks;

/**
* @dynamic-command true
*
* @return \OpenEuropa\TaskRunner\Tasks\CollectionFactory\CollectionFactory
*/
public function runTasks()
{
$commandName = $this->input()->getArgument('command');
/** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */
$command = Robo::application()->get($commandName);
$tasks = $command->getAnnotationData()['tasks'];
$tasksPath = $command->getAnnotationData()['tasks_path'];
// Get tasks only now to have variables updated with options data.
$tasks = $this->getConfig()->get($tasksPath);

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));
}
}
}
}
}
}
86 changes: 77 additions & 9 deletions src/TaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace OpenEuropa\TaskRunner;

use Composer\Autoload\ClassLoader;
use Consolidation\AnnotatedCommand\AnnotatedCommand;
use Consolidation\AnnotatedCommand\Parser\Internal\DocblockTag;
use Consolidation\AnnotatedCommand\Parser\Internal\TagFactory;
use Consolidation\Config\Loader\ConfigProcessor;
Expand All @@ -22,13 +23,15 @@
use OpenEuropa\TaskRunner\Contract\TimeAwareInterface;
use OpenEuropa\TaskRunner\Services\Composer;
use OpenEuropa\TaskRunner\Services\Time;
use OpenEuropa\TaskRunner\TaskRunner\ConfigUtility\SelfProcessingRoboConfig;
use Robo\Application;
use Robo\Common\ConfigAwareTrait;
use Robo\Config\Config;
use Robo\Robo;
use Robo\Runner as RoboRunner;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;

Expand Down Expand Up @@ -94,7 +97,9 @@ public function __construct(InputInterface $input, OutputInterface $output, Clas
$this->workingDir = $this->getWorkingDir($this->input);
chdir($this->workingDir);

$this->config = new Config();
// Let the self-processing wrapper handle resolving variables, so any
// later update will trigger re-resolving of variables.
$this->config = new SelfProcessingRoboConfig();
$this->application = $this->createApplication();
$this->application->setAutoExit(false);
$this->container = $this->createContainer(
Expand Down Expand Up @@ -167,17 +172,13 @@ public function getCommands($class)
*/
private function createConfiguration()
{
$config = new Config();
$config->set('runner.working_dir', realpath($this->workingDir));
$this->config->set('runner.working_dir', realpath($this->workingDir));

foreach ($this->getConfigProviders() as $class) {
/** @var \OpenEuropa\TaskRunner\Contract\ConfigProviderInterface $class */
$class::provide($config);
$class::provide($this->config);
}

// Resolve variables and import into config.
$processor = (new ConfigProcessor())->add($config->export());
$this->config->import($processor->export());
// Keep the container in sync.
$this->container->share('config', $this->config);
}
Expand Down Expand Up @@ -347,7 +348,7 @@ private function registerDynamicCommands(Application $application)
$this->runner->registerCommandClass($this->application, DynamicCommands::class);
$commandClass = $this->container->get($commandFileName);

foreach ($commands as $name => $tasks) {
foreach ($commands as $name => $commandDefinition) {
$aliases = [];
// This command has been already registered as an annotated command.
if ($application->has($name)) {
Expand All @@ -361,14 +362,81 @@ private function registerDynamicCommands(Application $application)
}

$commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
$commandInfo->addAnnotation('tasks', $tasks);
if (isset($commandDefinition['tasks'])) {
$commandInfo->addAnnotation('tasks_path', "commands.$name.tasks");
if (isset($commandDefinition['aliases'])) {
$aliases = array_unique(array_merge($aliases, $commandDefinition['aliases']));
}
}
else {
$commandInfo->addAnnotation('tasks_path', "commands.$name");

// @codingStandardsIgnoreLine
$message = 'Defining a dynamic command as a plain list of tasks is deprecated in openeuropa/task-runner:1.0.0 and is removed from openeuropa/task-runner:2.0.0. Define tasks in the "tasks" subkey of the custom command definition.';
@trigger_error($message, E_USER_DEPRECATED);
}
$command = $commandFactory->createCommand($commandInfo, $commandClass)
->setName($name)
->setAliases($aliases);
if (isset($commandDefinition['tasks'])) {
if (isset($commandDefinition['description'])) {
$command->setDescription($commandDefinition['description']);
}
if (isset($commandDefinition['usages'])) {
foreach ($commandDefinition['usages'] as $usage) {
$command->addUsage($usage);
}
}
}

// Dynamic commands may define their own options.
$this->addOptions($command, $commandDefinition);

$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'],
$this->mapOptionMode($optionDefinition['mode']),
$optionDefinition['description'],
$optionDefinition['default']
);
}
}

private function mapOptionMode($mode) {
$modes = [
'none' => InputOption::VALUE_NONE,
'required' => InputOption::VALUE_REQUIRED,
'optional' => InputOption::VALUE_OPTIONAL,
'required-array' => InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'optional-array' => InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
];
$modeKeys = implode('|', array_keys($modes));
return $modes[$mode]
?? $this->throwInvalidArgumentException("Unknown options mode '$mode', valid modes are [$modeKeys].");
}

private function throwInvalidArgumentException($message) {
throw new \InvalidArgumentException($message);
}

/**
* Discovers task runner commands that are provided by various packages.
*
Expand Down
Loading