Skip to content

Commit

Permalink
Merge pull request #10 from tarsana/config
Browse files Browse the repository at this point in the history
add config loader to Command
  • Loading branch information
webNeat authored Sep 14, 2017
2 parents ad5456b + 1e39d42 commit f939152
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 6 deletions.
2 changes: 0 additions & 2 deletions .github/ISSUE_TEMPLATE.md

This file was deleted.

43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ A library to build command line applications using PHP. This is part of the [Tar

- [Defining Arguments and Options](#defining-arguments-and-options)

- [Reading Arguments and Options Interactively](#reading-arguments-and-options-interactively) **New on version 1.1.0**
- [Reading Arguments and Options Interactively](#reading-arguments-and-options-interactively) **Since version 1.1.0**
- [Handeling The Filesystem](#handeling-the-filesystem)

- [Loading Configuration](#loading-configuration) **New on version 1.2.0**

- [Rendering Templates](#rendering-templates)

- [Adding SubCommands](#adding-sub-commands)
Expand Down Expand Up @@ -330,6 +332,43 @@ protected function init()
}
```

# Loading Configuration

In addition to the command line arguments, the user can provide data to your command via configuration files. This is useful because it lets you define a default configuration file and lets the user change some values with a custom configuration file.

Let's write an example command which have a global configuration file at `/home/user/.config.json`. It lets the user customize value via the file `config.json` in the current directory:

```php
class ConfigCommand extends Command {
protected function init()
{
// ...
$this->configPaths(['/home/user/.config.json', 'config.json']);
}

protected function execute()
{
// getting a config value
// assuming that $data is the merged content of the config files
$this->config('name'); // returns $data['name']
$this->config('foo.bar.baz'); // returns $data['foo']['bar']['baz']
$this->config(); // returns $data
}
}
```

- The method `configPaths` take a list of paths, loads them and merges them into one configuration (it use `array_replace_recursive` internally).

- The method `config` is used to retreive configuration values.

Note that:

- Only `json` files are supported as configuration files for the moment. Please open an issue or make a Pull Request to add other formats.

- `configPaths` will silently ignore paths which does not exist in the filesystem.

- A subcommand will always have the same configuration data as its parent command, unless `configPaths` is used to override it.

# Rendering Templates

The `Command` class gives also possibility to render templates. The default template engine is [Twig](https://twig.symfony.com) but you can use your favorite one by implementing the interfaces `TemplateLoaderInterface` and `TemplateInterface`.
Expand Down Expand Up @@ -588,6 +627,8 @@ Please take a look at the examples in the `examples` directory, and try using th

# Development Notes

- **Version 1.2.0** Commands can now load configuration from multiple JSON files.

- **Version 1.1.1** Fixed a bug with subcommands not having the default `--help`, `--version` and `-i` subcommands.

- **Version 1.1.0** The flag `-i` added to commands to enable interactive reading of arguments and options.
Expand Down
24 changes: 22 additions & 2 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use Tarsana\Command\Commands\HelpCommand;
use Tarsana\Command\Commands\InteractiveCommand;
use Tarsana\Command\Commands\VersionCommand;
use Tarsana\Command\Config\ConfigLoader;
use Tarsana\Command\Console\Console;
use Tarsana\Command\Console\ExceptionPrinter;
use Tarsana\Command\Interfaces\Console\ConsoleInterface;
Expand Down Expand Up @@ -33,6 +34,7 @@ class Command {
protected $console;
protected $fs;
protected $templatesLoader;
protected $config;

public static function create(callable $action = null) {
$command = new Command;
Expand All @@ -51,6 +53,7 @@ public function __construct()
->options([])
->console(new Console)
->fs(new Filesystem('.'))
->configPaths([])
->setupSubCommands()
->init();
}
Expand Down Expand Up @@ -233,20 +236,37 @@ public function templatesLoader(TemplateLoaderInterface $value = null)
return $this;
}

public function templatesPath(string $path, string $cachePath = null) {
public function templatesPath(string $path, string $cachePath = null)
{
$this->templatesLoader = new TemplateLoader($path, $cachePath);
foreach ($this->commands as $name => $command) {
$command->templatesLoader = $this->templatesLoader();
}
return $this;
}

public function template(string $name) {
public function template(string $name)
{
if (null === $this->templatesLoader)
throw new \Exception("Please initialize the templates loader before trying to load templates!");
return $this->templatesLoader->load($name);
}

public function configPaths(array $paths)
{
$configLoader = new ConfigLoader($this->fs);
$this->config = $configLoader->load($paths);
foreach ($this->commands as $name => $command) {
$command->config = $this->config;
}
return $this;
}

public function config(string $path = null)
{
return $this->config->get($path);
}

/**
* action getter and setter.
*
Expand Down
36 changes: 36 additions & 0 deletions src/Config/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php namespace Tarsana\Command\Config;

use Tarsana\Command\Interfaces\Config\ConfigInterface;

/**
* Stores and gets configuration.
*/
class Config implements ConfigInterface {
/**
* The raw configuration data.
*
* @var array
*/
protected $data;

public function __construct(array $data) {
$this->data = $data;
}

/**
* Gets a configuration value by path.
*/
public function get(string $path = null)
{
if (null === $path)
return $this->data;
$keys = explode('.', $path);
$value = $this->data;
foreach ($keys as $key) {
if (!is_array($value) || !array_key_exists($key, $value))
throw new \Exception("Unable to find a configuration value with path '{$path}'");
$value = $value[$key];
}
return $value;
}
}
48 changes: 48 additions & 0 deletions src/Config/ConfigLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php namespace Tarsana\Command\Config;

use Tarsana\Command\Config\Config;
use Tarsana\Command\Helpers\Decoders\JsonDecoder;
use Tarsana\Command\Interfaces\Config\ConfigInterface;
use Tarsana\Command\Interfaces\Config\ConfigLoaderInterface;
use Tarsana\IO\Interfaces\Filesystem as FilesystemInterface;

/**
* Loads configuration from multiple files.
*/
class ConfigLoader implements ConfigLoaderInterface {

protected static $decoders = [
'json' => JsonDecoder::class
];

protected $fs;

public function __construct(FilesystemInterface $fs)
{
$this->fs = $fs;
}

public function load(array $paths) : ConfigInterface
{
if (empty($paths))
return new Config([]);
$data = [];
foreach ($paths as $path) {
$data[] = $this->decode($path);
}
$data = call_user_func_array('array_replace_recursive', $data);
return new Config($data);
}

protected function decode(string $path) : array {
if (! $this->fs->isFile($path))
return [];
$file = $this->fs->file($path);
$ext = $file->extension();
if (! array_key_exists($ext, static::$decoders))
throw new \Exception("Unknown configuration file extension '{$ext}'");
$decoderClass = static::$decoders[$ext];
$decoder = new $decoderClass;
return $decoder->decode($file->content());
}
}
13 changes: 13 additions & 0 deletions src/Helpers/Decoders/JsonDecoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php namespace Tarsana\Command\Helpers\Decoders;

use Tarsana\Command\Interfaces\Helpers\DecoderInterface;


class JsonDecoder implements DecoderInterface {

public function decode(string $text) : array
{
return json_decode($text, true);
}

}
11 changes: 11 additions & 0 deletions src/Interfaces/Config/ConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php namespace Tarsana\Command\Interfaces\Config;

/**
* Stores and gets configuration.
*/
interface ConfigInterface {
/**
* Gets a configuration value by path.
*/
public function get(string $path);
}
12 changes: 12 additions & 0 deletions src/Interfaces/Config/ConfigLoaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php namespace Tarsana\Command\Interfaces\Config;

use Tarsana\Command\Interfaces\Config\ConfigInterface;

/**
* Loads configuration from multiple files.
*/
interface ConfigLoaderInterface {

public function load(array $paths) : ConfigInterface;

}
8 changes: 8 additions & 0 deletions src/Interfaces/Helpers/DecoderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php namespace Tarsana\Command\Interfaces\Helpers;


interface DecoderInterface {

public function decode(string $text) : array;

}
1 change: 1 addition & 0 deletions src/SubCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(Command $parent)
->console($parent->console())
->fs($parent->fs)
->templatesLoader($parent->templatesLoader);
$this->config = $parent->config;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Template/TemplateLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Tarsana\Command\Interfaces\Template\TemplateInterface;
use Tarsana\Command\Interfaces\Template\TemplateLoaderInterface;
use Tarsana\Command\Template\Twig\TwigLoader;
use Tarsana\IO\Filesystem;


Expand All @@ -18,7 +19,7 @@ class TemplateLoader implements TemplateLoaderInterface {
* @var array
*/
protected static $providers = [
'twig' => 'Tarsana\Command\Template\Twig\TwigLoader'
'twig' => TwigLoader::class
];

/**
Expand Down
29 changes: 29 additions & 0 deletions tests/Acceptance/LoadsConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php namespace Tarsana\Command\Tests\Acceptance;

use Tarsana\Command\Command as C;
use Tarsana\Tester\CommandTestCase;


class LoadsConfigurationTest extends CommandTestCase {

public function test_it_loads_configuration() {
$c = C::create(function($app) {
$app->configPaths(['/home/user/.config.json', 'config.json']);

$name = $app->config('name');
$repoURL = $app->config('repo.url');
$app->console()->line("{$name}:{$repoURL}");
});

$this->fs
->file('/home/user/.config.json', true)
->content(json_encode(['name' => 'user']));

$this->fs
->file('config.json', true)
->content(json_encode(['repo' => ['url' => 'tarsana']]));

$this->command($c)
->prints('user:tarsana<br>');
}
}
59 changes: 59 additions & 0 deletions tests/Unit/Config/ConfigLoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php namespace Tarsana\Command\Tests\Unit\Config;

use PHPUnit\Framework\TestCase;
use Tarsana\Command\Config\Config;
use Tarsana\Command\Config\ConfigLoader;
use Tarsana\IO\Filesystem;
use Tarsana\IO\Filesystem\Adapters\Memory;


class ConfigLoaderTest extends TestCase {

protected $fs;
protected $loader;

public function setUp() {
$adapter = new Memory;
$adapter->mkdir('.', 0777, true);
$this->fs = new Filesystem('.', $adapter);
$this->loader = new ConfigLoader($this->fs);
}

public function test_it_loads_single_config() {
$data = ['name' => 'foo', 'repo' => 'bar'];
$this->fs->file('config.json', true)
->content(json_encode($data));
$this->assertEquals($data, $this->loader->load(['config.json'])->get());
}

public function test_it_loads_many_configs() {
$data1 = ['name' => 'foo', 'repo' => 'bar'];
$data2 = ['repo' => ['type' => 'git']];
$data3 = ['repo' => ['name' => 'baz'], 'descr' => 'blabla'];
$merged = ['name' => 'foo', 'repo' => ['type' => 'git', 'name' => 'baz'], 'descr' => 'blabla'];

$this->fs->file('/opt/command/config.json', true)->content(json_encode($data1));
$this->fs->file('/home/user/config.json', true)->content(json_encode($data2));
$this->fs->file('config.json', true)->content(json_encode($data3));

$this->assertEquals($merged, $this->loader->load([
'/opt/command/config.json',
'/home/user/config.json',
'/projects/config.json', // this is missing
'config.json'
])->get());
}

public function test_it_loads_empty_config_when_no_path_is_given() {
$this->assertEquals([], $this->loader->load([])->get());
}

/**
* @expectedException Exception
*/
public function test_it_throws_exception_when_unknown_extension() {
$this->fs->file('config.xml', true);
$this->loader->load(['config.xml']);
}

}
Loading

0 comments on commit f939152

Please sign in to comment.