Skip to content

Commit

Permalink
Add custom commands options, rebase of PR openeuropa#44
Browse files Browse the repository at this point in the history
  • Loading branch information
Merlin committed Nov 10, 2021
1 parent f7d9115 commit 37ea075
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 8 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,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.
Expand Down
49 changes: 48 additions & 1 deletion src/Commands/DynamicCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace OpenEuropa\TaskRunner\Commands;

use Consolidation\AnnotatedCommand\AnnotatedCommand;
use OpenEuropa\TaskRunner\Tasks as TaskRunnerTasks;
use Robo\Robo;

Expand All @@ -19,6 +20,8 @@ class DynamicCommands extends AbstractCommands
use TaskRunnerTasks\CollectionFactory\loadTasks;

/**
* @dynamic-command true
*
* @return \OpenEuropa\TaskRunner\Tasks\CollectionFactory\CollectionFactory
*/
public function runTasks()
Expand All @@ -28,6 +31,50 @@ public function runTasks()
$command = Robo::application()->get($commandName);
$tasks = $command->getAnnotationData()['tasks'];

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);
}

/**
* 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));
}
}
}
}
}
}
47 changes: 46 additions & 1 deletion 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 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,58 @@ private function registerDynamicCommands(Application $application)
}

$commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
$tasks = $commandDefinition['tasks'] ?? $commandDefinition;
$commandInfo->addAnnotation('tasks', $tasks);
$command = $commandFactory->createCommand($commandInfo, $commandClass)
->setName($name)
->setAliases($aliases);

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

// Append also options of subsequent tasks.
foreach ($tasks 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']
);
}
}

/**
* Discovers task runner commands that are provided by various packages.
*
Expand Down
56 changes: 52 additions & 4 deletions src/Tasks/CollectionFactory/CollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Robo\Contract\SimulatedInterface;
use Robo\Exception\TaskException;
use Robo\LoadAllTasks;
use Robo\Robo;
use Robo\Task\BaseTask;
use Symfony\Component\Yaml\Yaml;

Expand All @@ -26,14 +27,21 @@ class CollectionFactory extends BaseTask implements BuilderAwareInterface, Simul
*/
protected $tasks;

/**
* @var array
*/
protected $options;

/**
* Constructs a new CollectionFactory.
*
* @param array $tasks
* @param array $options
*/
public function __construct(array $tasks = [])
public function __construct(array $tasks = [], array $options =[])
{
$this->tasks = $tasks;
$this->options = $options;
}

/**
Expand Down Expand Up @@ -172,9 +180,17 @@ protected function taskFactory($task)
if (!empty($task['arguments'])) {
$taskExec->args($task['arguments']);
}
if (!empty($task['options'])) {
$taskExec->options($task['options'], '=');
}

$options = $task['options'] ?? [];

// Propagate any input option passed to the parent command,
// but only if the command eats it, and only if it is not set
// explicitly.
$commandOptions = $this->fetchCommandOptions($task['command']);
$options += $this->filterInputOptions($commandOptions);

$taskExec->options($options, '=');

return $taskExec;

case "process-php":
Expand Down Expand Up @@ -226,6 +242,38 @@ protected function taskFactory($task)
}
}

/**
* @param string $commandName
* @return array
*/
protected function fetchCommandOptions($commandName) {
$container = Robo::getContainer();
/** @var \Robo\Application $app */
$app = $container->get('application');
/** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */
$command = $app->get($commandName);
$commandOptions = $command->getDefinition()->getOptions();
return $commandOptions;
}

/**
* @param array $commandOptions
* @return array
*/
protected function filterInputOptions(array $commandOptions): array {
$options = [];
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) {
$options[$name] = $value;
}
}
}
return $options;
}

/**
* Sets the given safe default value for the option with the given name.
*
Expand Down
5 changes: 3 additions & 2 deletions src/Tasks/CollectionFactory/loadTasks.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,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);
}
}
33 changes: 33 additions & 0 deletions tests/fixtures/setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,36 @@
<env name="BASE_URL" value="http://127.0.0.1:8888"/>
</php>
</phpunit>
- 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: ~

0 comments on commit 37ea075

Please sign in to comment.