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

Allow 3rd party to add and alter the configuration #122

Merged
merged 19 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
951ed28
ISAICP-5825: Allow 3rd party to alter the configuration.
claudiu-cristea Apr 22, 2020
35f65c7
ISAICP-5825: Rename the new method to comply with ::createConfigurati…
claudiu-cristea Apr 22, 2020
19c7ae2
ISAICP-5825: Add a command that returns the resolved config.
claudiu-cristea Apr 23, 2020
75ff634
ISAICP-5825: Switch to a discovery config altering.
claudiu-cristea Apr 23, 2020
6acae8b
ISAICP-5825: Add documentation.
claudiu-cristea Apr 23, 2020
334ac9f
ISAICP-5825: Typos & wording improvements.
claudiu-cristea Apr 23, 2020
55b4a3c
ISAICP-5825: Make sure that variables are resolved only at the end.
claudiu-cristea Apr 23, 2020
008dfc2
ISAICP-5825: Init the working dir (was lost on refactoring).
claudiu-cristea Apr 24, 2020
8bff17f
ISAICP-5825: Fix some test docs. Remove unused statemenets.
claudiu-cristea Apr 24, 2020
50a48f3
ISAICP-5825: Use the Robo config class instead of manipulating arrays.
claudiu-cristea Apr 24, 2020
8eee721
ISAICP-5825: Bump the Robo minimum version to avoid a config bug.
claudiu-cristea Apr 24, 2020
d5bc78d
ISAICP-5825: Allow the 'config' command to show a key.
claudiu-cristea Apr 25, 2020
858ca06
ISAICP-5825: Fix coding standards.
claudiu-cristea Apr 25, 2020
5a0aabc
ISAICP-5825: Update documentation.
pfrenssen May 12, 2020
1ac9b1c
ISAICP-5825: Use the generic discovery also for default config provid…
claudiu-cristea May 15, 2020
3468015
ISAICP-5825: Remove stale '@package' tag from interface.
claudiu-cristea May 15, 2020
2410a96
ISAICP-5825: Move the relative config path to a constant.
claudiu-cristea May 15, 2020
98114fe
ISAICP-5825: Update documentation.
pfrenssen May 18, 2020
bb7bf3d
Merge branch 'master' into custom-configs
idimopoulos May 19, 2020
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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,45 @@ Task Runner commands can be customized in two ways:
this file to declare default options which are expected to work with your
application under regular circumstances. This file should be committed in
the project.
* Third parties might implement config providers to modify the config. A
config provider is a class implementing the `ConfigProviderInterface`.
Such a class should be placed under the `TaskRunner\ConfigProviders`
relative namespace. For instance when `Some\Namespace` points to `src/`
directory, then the config provider class should be placed under the
`src/TaskRunner/ConfigProviders` directory and will have the namespace set
to `Some\Namespace\TaskRunner\ConfigProviders`. The class name should end
with the `ConfigProvider` suffix. Use the `::provide()` method to alter
the configuration object. A `@priority` annotation tag can be defined in
the class docblock in order to determine the order in which the config
providers are running. If omitted, `@priority 0` is assumed. This
mechanism allows also to insert custom YAML config files in the flow, see
the following example:
```
namespace Some\Namespace\TaskRunner\ConfigProviders;

use OpenEuropa\TaskRunner\Contract\ConfigProviderInterface;
use OpenEuropa\TaskRunner\Traits\ConfigFromFilesTrait;
use Robo\Config\Config;

/**
* @priority 100
*/
class AddCustomFileConfigProvider implements ConfigProviderInterface
{
use ConfigFromFilesTrait;
public static function provide(Config $config): void
{
// Load the configuration from custom.yml and custom2.yml and
// apply it to the configuration object. This will override config
// from runner.yml.dist (which has priority 1500) but get
// overridden by the config from runner.yml (priority -1000).
static::importFromFiles($config, [
'custom.yml',
'custom2.yml',
]);
}
}
```
* `runner.yml` - project specific user overrides. This file is also located
in the root folder of the project that depends on the Task Runner. This
file can be used to override options with values that are specific to the
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"prefer-stable": true,
"require": {
"php": ">=7.2",
"consolidation/robo": "^1.4",
"consolidation/robo": "^1.4.7",
"gitonomy/gitlib": "^1.0",
"jakeasmith/http_build_url": "^1.0.1",
"nuvoleweb/robo-config": "^0.2.1"
Expand Down
42 changes: 42 additions & 0 deletions src/Commands/RunnerCommands.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace OpenEuropa\TaskRunner\Commands;

use Robo\Exception\AbortTasksException;
use Symfony\Component\Yaml\Yaml;

/**
* Commands to interact with the task runner itself.
*/
class RunnerCommands extends AbstractCommands
{

/**
* Displays the current configuration in YAML format.
*
* If no argument is passed the whole configuration is outputted. To display
* a specific configuration, pass the key as argument (e.g. `drupal.root`).
*
* @command config
pfrenssen marked this conversation as resolved.
Show resolved Hide resolved
*
* @param string|null $key Optional configuration key
* @return string
* @throws \Robo\Exception\AbortTasksException
*
* @todo Implement a `--format` option to allow different output formats.
*/
public function config(?string $key = null): string
{
if (!$key) {
$config = $this->getConfig()->export();
} else {
if (!$this->getConfig()->has($key)) {
throw new AbortTasksException("Invalid '$key' key.");
}
$config = $this->getConfig()->get($key);
}
return trim(Yaml::dump($config, 10, 2));
}
}
50 changes: 50 additions & 0 deletions src/Contract/ConfigProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace OpenEuropa\TaskRunner\Contract;

use OpenEuropa\TaskRunner\Traits\ConfigFromFilesTrait;
use Robo\Config\Config;

/**
* Interface for configuration providers.
*
* Classes implementing this interface:
* - Should have `TaskRunner\ConfigProviders` as relative namespace. For
* instance when `Some\Namespace` points to the `src/` directory, then the
* class should be placed in `src/TaskRunner/ConfigProviders` and will have
* `Some\Namespace\TaskRunner\ConfigProviders` as namespace.
* - The class name should end with the `ConfigProvider` suffix.
*/
interface ConfigProviderInterface
{
/**
* The relative path for an environment specific config file.
*
* If this file is found in the local environment's default configuration
* path (such as $XDG_CONFIG_HOME or $HOME/.config/) then it will
* automatically be included.
*
* Any custom config providers that read config from a hierarchical data
* storage are suggested to use this path.
*
* @var string
*/
const DEFAULT_CONFIG_LOCATION = 'openeuropa/taskrunner/runner.yml';

/**
* Adds or overrides configuration.
*
* Implementations should alter the `$config` object, passed to the method.
* A convenient way to provide additional config or override the existing
* one is to use the `ConfigFromFilesTrait::importFromFiles()` method and
* load overrides form custom config .yml files. But the $config object can
* be manipulated also directly using its methods, e.g. $config->().
*
* @param \Robo\Config\Config $config
*
* @see ConfigFromFilesTrait::importFromFiles()
*/
public static function provide(Config $config): void;
}
106 changes: 79 additions & 27 deletions src/TaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
namespace OpenEuropa\TaskRunner;

use Composer\Autoload\ClassLoader;
use Consolidation\AnnotatedCommand\Parser\Internal\DocblockTag;
use Consolidation\AnnotatedCommand\Parser\Internal\TagFactory;
use Consolidation\Config\Loader\ConfigProcessor;
use Gitonomy\Git\Repository;
use OpenEuropa\TaskRunner\Commands\ChangelogCommands;
use OpenEuropa\TaskRunner\Commands\DrupalCommands;
use OpenEuropa\TaskRunner\Commands\DynamicCommands;
use OpenEuropa\TaskRunner\Commands\ReleaseCommands;
use OpenEuropa\TaskRunner\Commands\RunnerCommands;
use OpenEuropa\TaskRunner\Contract\ComposerAwareInterface;
use OpenEuropa\TaskRunner\Contract\ConfigProviderInterface;
use OpenEuropa\TaskRunner\Contract\RepositoryAwareInterface;
use OpenEuropa\TaskRunner\Contract\TimeAwareInterface;
use OpenEuropa\TaskRunner\Services\Composer;
Expand Down Expand Up @@ -73,6 +78,7 @@ class TaskRunner
DrupalCommands::class,
DynamicCommands::class,
ReleaseCommands::class,
RunnerCommands::class,
];

/**
Expand All @@ -90,11 +96,13 @@ public function __construct(InputInterface $input, OutputInterface $output, Clas
$this->workingDir = $this->getWorkingDir($this->input);
chdir($this->workingDir);

$this->config = $this->createConfiguration();
$this->config = new Config();
$this->application = $this->createApplication();
$this->application->setAutoExit(false);
$this->container = $this->createContainer($this->input, $this->output, $this->application, $this->config, $classLoader);

$this->createConfiguration();

// Create and initialize runner.
$this->runner = new RoboRunner();
$this->runner->setRelativePluginNamespace('TaskRunner');
Expand Down Expand Up @@ -133,48 +141,92 @@ public function getCommands($class)
}

/**
* Create default configuration.
*
* @return Config
* Parses the configuration files, and merges them into the Config object.
*/
private function createConfiguration()
{
$config = new Config();
$config->set('runner.working_dir', realpath($this->workingDir));
Robo::loadConfiguration([
__DIR__.'/../config/runner.yml',
'runner.yml.dist',
'runner.yml',
$this->getLocalConfigurationFilepath(),
], $config);

return $config;

foreach ($this->getConfigProviders() as $class) {
/** @var \OpenEuropa\TaskRunner\Contract\ConfigProviderInterface $class */
$class::provide($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);
}

/**
* Get the local configuration filepath.
* Discovers all config providers.
*
* @param string $configuration_file
* The default filepath.
* @return string[]
* An array of fully qualified class names of available config providers.
*
* @return string|null
* The local configuration file path, or null if it doesn't exist.
* @throws \ReflectionException
* Thrown if a config provider doesn't have a valid annotation.
*/
private function getLocalConfigurationFilepath($configuration_file = 'openeuropa/taskrunner/runner.yml')
private function getConfigProviders(): array
{
if ($config = getenv('OPENEUROPA_TASKRUNNER_CONFIG')) {
return $config;
/** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
$discovery = Robo::service('relativeNamespaceDiscovery');
$discovery->setRelativeNamespace('TaskRunner\ConfigProviders')
->setSearchPattern('/.*ConfigProvider\.php$/');

// Discover config providers.
foreach ($discovery->getClasses() as $class) {
if (is_subclass_of($class, ConfigProviderInterface::class)) {
$classes[$class] = $this->getConfigProviderPriority($class);
}
}

if ($config = getenv('XDG_CONFIG_HOME')) {
return $config . '/' . $configuration_file;
}
// High priority modifiers run first.
arsort($classes, SORT_NUMERIC);

if ($home = getenv('HOME')) {
return getenv('HOME') . '/.config/' . $configuration_file;
}
return array_keys($classes);
}

return null;
/**
* @param string $class
* @return float
* @throws \ReflectionException
*/
private function getConfigProviderPriority($class)
{
$priority = 0.0;
$reflectionClass = new \ReflectionClass($class);
if ($docBlock = $reflectionClass->getDocComment()) {
// Remove the leading /** and the trailing */
$docBlock = preg_replace('#^\s*/\*+\s*#', '', $docBlock);
$docBlock = preg_replace('#\s*\*+/\s*#', '', $docBlock);

// Nothing left? Exit.
if (empty($docBlock)) {
return $priority;
}

$tagFactory = new TagFactory();
foreach (explode("\n", $docBlock) as $row) {
// Remove trailing whitespace and leading space + '*'s
$row = rtrim($row);
$row = preg_replace('#^[ \t]*\**#', '', $row);
$tagFactory->parseLine($row);
}

$priority = array_reduce($tagFactory->getTags(), function ($priority, DocblockTag $tag) {
if ($tag->getTag() === 'priority') {
$value = $tag->getContent();
if (is_numeric($value)) {
$priority = (float) $value;
}
}
return $priority;
}, $priority);
}
return $priority;
}

/**
Expand Down
42 changes: 42 additions & 0 deletions src/TaskRunner/ConfigProviders/DefaultConfigProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace OpenEuropa\TaskRunner\TaskRunner\ConfigProviders;

use OpenEuropa\TaskRunner\Contract\ConfigProviderInterface;
use OpenEuropa\TaskRunner\Traits\ConfigFromFilesTrait;
use Robo\Config\Config;

/**
* Provides the basic default configuration for the task runner.
*
* This will import the following files:
* - The example configuration provided in the task runner library itself.
* - The default configuration "runner.yml.dist" shipped in the root folder of
* the project which uses the task runner.
*
* This serves as a safe default implementation which can be overridden by
* environment specific configuration and user preferences.
*
* The config provider priority is very high in order to ensure that will run at
* the very beginning. However, in some very special circumstances, third-party
* config providers are abie to set priorities higher than this.
*
* @priority 1500
*/
class DefaultConfigProvider implements ConfigProviderInterface
{
use ConfigFromFilesTrait;

/**
* {@inheritdoc}
*/
public static function provide(Config $config): void
{
static::importFromFiles($config, [
__DIR__.'/../../../config/runner.yml',
'runner.yml.dist',
]);
}
}
Loading