Skip to content

Commit

Permalink
Merge pull request #122 from openeuropa/custom-configs
Browse files Browse the repository at this point in the history
Allow 3rd party to add and alter the configuration
  • Loading branch information
idimopoulos authored May 19, 2020
2 parents f4fa798 + bb7bf3d commit c9a6d9a
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 43 deletions.
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
*
* @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 @@ -72,6 +77,7 @@ class TaskRunner
ChangelogCommands::class,
DrupalCommands::class,
ReleaseCommands::class,
RunnerCommands::class,
];

/**
Expand All @@ -89,11 +95,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->setContainer($this->container);
Expand Down Expand Up @@ -150,48 +158,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$/');

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

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

return null;
return array_keys($classes);
}

/**
* @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

0 comments on commit c9a6d9a

Please sign in to comment.