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

OPENEUROPA-372: Allow custom commands to define options #44

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 48 additions & 4 deletions src/Commands/DynamicCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -18,13 +16,59 @@ class DynamicCommands extends AbstractCommands
use TaskRunnerTasks\CollectionFactory\loadTasks;

/**
* @dynamic-command true
*
* @return \OpenEuropa\TaskRunner\Tasks\CollectionFactory\CollectionFactory
*/
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);
}

/**
* 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));
}
}
}
}
}
}
76 changes: 69 additions & 7 deletions src/TaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
namespace OpenEuropa\TaskRunner;

use Composer\Autoload\ClassLoader;
use Consolidation\AnnotatedCommand\AnnotatedCommand;
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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -211,14 +212,75 @@ private function getWorkingDir(InputInterface $input)
*/
private function registerDynamicCommands(Application $application)
{
foreach ($this->getConfig()->get('commands', []) as $name => $tasks) {
$customCommands = $this->getConfig()->get('commands', []);
foreach ($customCommands 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.
$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];
}
}
39 changes: 31 additions & 8 deletions src/Tasks/CollectionFactory/CollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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.");
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 @@ -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);
}
}
6 changes: 2 additions & 4 deletions tests/CommandsTest.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<?php

namespace OpenEuropa\TaskRunner\Tests\Commands;
namespace OpenEuropa\TaskRunner\Tests;

use Consolidation\AnnotatedCommand\CommandFileDiscovery;
use OpenEuropa\TaskRunner\Commands\ChangelogCommands;
use OpenEuropa\TaskRunner\TaskRunner;
use OpenEuropa\TaskRunner\Tests\AbstractTest;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Yaml\Yaml;

/**
* Class DrupalCommandsTest.
*
* @package OpenEuropa\TaskRunner\Tests\Commands
* @package OpenEuropa\TaskRunner\Tests
*/
class CommandsTest extends AbstractTest
{
Expand Down
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: ~