diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cc73672 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{js,yml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1df7c10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +vendor +bin +coverage +*.log +*.phar +*.cache +*.lock diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cbe141a --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "codin/config", + "description": "Basic config file load supports PHP ans Json files", + "license": "Apache-2.0", + "type": "library", + "minimum-stability": "dev", + "prefer-stable": true, + "authors": [ + { + "name": "Kieron", + "email": "hello@madebykieron.co.uk", + "homepage": "http://madebykieron.co.uk", + "role": "Developer" + } + ], + "require": { + "php": ">=7.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "@stable", + "phpspec/phpspec": "@stable", + "phpstan/phpstan": "@stable" + }, + "autoload": { + "psr-4": { + "Codin\\Config\\": "src/" + } + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "bin-dir": "bin" + }, + "scripts": { + "psr": [ + "./bin/php-cs-fixer fix . --allow-risky=yes --rules=@PSR2,no_unused_imports,ordered_imports,ordered_interfaces,single_quote,trailing_comma_in_multiline_array" + ], + "test": [ + "phpstan analyse", + "phpspec run" + ], + "uninstall": [ + "rm -rf ./bin", + "rm -rf ./vendor", + "rm ./composer.lock" + ] + } +} diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..cb7f251 --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,6 @@ +formatter.name: pretty +stop_on_failure: true +code_generation: false +suites: + default: + src_path: ./src diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d54acba --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: max + paths: + - %currentWorkingDirectory%/src + bootstrapFiles: + - %currentWorkingDirectory%/vendor/autoload.php + inferPrivatePropertyTypeFromConstructor: true + checkMissingIterableValueType: false diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..7bffe2c --- /dev/null +++ b/readme.md @@ -0,0 +1,19 @@ +# Config + +Lightweight configuration file loader that supports PHP and JSON files + +```shell +$ cat path/to/config/foos.php + 'bar']; + +$ cat path/to/config/bars.json +{"bar":"baz"} +``` + +```php +$config = Codin\Config\Config::create('path/to/config'); +$config->has('foos.foo'); // true +$config->get('foos.foo'); // "bar" +$config->has('bars'); // true +$config->get('bars'); // ['bar' => 'baz'] +``` diff --git a/spec/Codin/Config/ConfigSpec.php b/spec/Codin/Config/ConfigSpec.php new file mode 100644 index 0000000..540995a --- /dev/null +++ b/spec/Codin/Config/ConfigSpec.php @@ -0,0 +1,25 @@ +beConstructedWith($loader); + $loader->has('foo')->shouldBeCalled()->willReturn(true); + $this->has('foo')->shouldReturn(true); + } + + public function it_should_get_items(ConfigAccessInterface $loader) + { + $this->beConstructedWith($loader); + $loader->get('foo', null)->shouldBeCalled()->willReturn('bar'); + $this->get('foo')->shouldReturn('bar'); + } +} diff --git a/spec/Codin/Config/Loaders/ChainedLoaderSpec.php b/spec/Codin/Config/Loaders/ChainedLoaderSpec.php new file mode 100644 index 0000000..96bbbba --- /dev/null +++ b/spec/Codin/Config/Loaders/ChainedLoaderSpec.php @@ -0,0 +1,24 @@ +beConstructedWith([$loader]); + $loader->has('foo')->shouldBeCalled()->willReturn(true); + $this->has('foo')->shouldReturn(true); + } + + public function it_should_get_items(ConfigAccessInterface $loader) + { + $this->beConstructedWith([$loader]); + $loader->has('foo')->shouldBeCalled()->willReturn(true); + $loader->get('foo', null)->shouldBeCalled()->willReturn('bar'); + $this->get('foo')->shouldReturn('bar'); + } +} diff --git a/spec/Codin/Config/Loaders/JsonFileLoaderSpec.php b/spec/Codin/Config/Loaders/JsonFileLoaderSpec.php new file mode 100644 index 0000000..2f25ccb --- /dev/null +++ b/spec/Codin/Config/Loaders/JsonFileLoaderSpec.php @@ -0,0 +1,37 @@ +beConstructedWith($path); + $this->has($name)->shouldReturn(true); + $this->has(sprintf('%s.%s', $name, 'foo'))->shouldReturn(true); + + unlink($filepath); + } + + public function it_should_get_items() + { + $path = sys_get_temp_dir(); + $name = sprintf('phpspec-test-%u', random_int(1, PHP_INT_MAX)); + $filepath = sprintf('%s/%s.json', $path, $name); + + file_put_contents($filepath, '{"foo":"bar"}'); + + $this->beConstructedWith($path); + $this->get($name)->shouldReturn(['foo' => 'bar']); + + unlink($filepath); + } +} diff --git a/spec/Codin/Config/Loaders/PhpFileLoaderSpec.php b/spec/Codin/Config/Loaders/PhpFileLoaderSpec.php new file mode 100644 index 0000000..ddced08 --- /dev/null +++ b/spec/Codin/Config/Loaders/PhpFileLoaderSpec.php @@ -0,0 +1,37 @@ + 'bar'];"); + + $this->beConstructedWith($path); + $this->has($name)->shouldReturn(true); + $this->has(sprintf('%s.%s', $name, 'foo'))->shouldReturn(true); + + unlink($filepath); + } + + public function it_should_get_items() + { + $path = sys_get_temp_dir(); + $name = sprintf('phpspec-test-%u', random_int(1, PHP_INT_MAX)); + $filepath = sprintf('%s/%s.php', $path, $name); + + file_put_contents($filepath, " 'bar'];"); + + $this->beConstructedWith($path); + $this->get($name)->shouldReturn(['foo' => 'bar']); + + unlink($filepath); + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..0573c22 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,36 @@ +loader = $loader; + } + + public static function create(string $path): ConfigAccessInterface + { + return new static(new Loaders\ChainedLoader([ + new Loaders\JsonFileLoader($path), + new Loaders\PhpFileLoader($path), + ])); + } + + public function has(string $key): bool + { + return $this->loader->has($key); + } + + public function get(string $key, $default = null) + { + return $this->loader->get($key, $default); + } +} diff --git a/src/ConfigAccessInterface.php b/src/ConfigAccessInterface.php new file mode 100644 index 0000000..a5f6225 --- /dev/null +++ b/src/ConfigAccessInterface.php @@ -0,0 +1,22 @@ +path = \realpath($path) ?: null; + + if (null === $this->path) { + throw new ConfigException(sprintf('config loader path not found "%s"', $path)); + } + } + + /** + * Get the path of a config file + */ + protected function filepath(string $name): ?string + { + $filepath = $this->path . '/' . $name . $this->extension; + + if (!\is_file($filepath)) { + return null; + } + + return $filepath; + } + + /** + * Load file contents + */ + abstract protected function load(string $file): ?array; + + /** + * Split key into parts, the first part will always be the file name + */ + protected function parts(string $name): array + { + if (\strlen($name) === 0) { + throw new ConfigException( + 'Parameter name cannot be empty' + ); + } + + $keys = \explode('.', $name); + + if (empty($keys)) { + throw new ConfigException( + 'Failed to extract keys from parameter name' + ); + } + + $file = \array_shift($keys); + + if (!$file) { + throw new ConfigException( + 'Failed to shift first key from keys' + ); + } + + return [$file, $keys]; + } + + /** + * {@inherit} + */ + public function has(string $key): bool + { + [$file, $keys] = $this->parts($key); + + $config = $this->load($file); + + if (null === $config) { + return false; + } + + foreach ($keys as $key) { + if (!\array_key_exists($key, $config)) { + return false; + } + $config = $config[$key]; + } + + return true; + } + + /** + * {@inherit} + */ + public function get(string $key, $default = null) + { + [$file, $keys] = $this->parts($key); + + $config = $this->load($file); + + if (null === $config) { + return $default; + } + + foreach ($keys as $key) { + if (!\array_key_exists($key, $config)) { + return $default; + } + $config = $config[$key]; + } + + return $config; + } +} diff --git a/src/Loaders/ChainedLoader.php b/src/Loaders/ChainedLoader.php new file mode 100644 index 0000000..8e4f11b --- /dev/null +++ b/src/Loaders/ChainedLoader.php @@ -0,0 +1,42 @@ + + */ + protected $loaders; + + public function __construct(array $loaders) + { + $this->loaders = $loaders; + } + + /** + * {@inherit} + */ + public function has(string $key): bool + { + $reduce = static function (bool $carry, ConfigAccessInterface $loader) use ($key) { + return $loader->has($key) ? true : $carry; + }; + return \array_reduce($this->loaders, $reduce, false); + } + + /** + * {@inherit} + */ + public function get(string $key, $default = null) + { + $reduce = static function ($carry, ConfigAccessInterface $loader) use ($key) { + return $loader->has($key) ? $loader->get($key) : $carry; + }; + return \array_reduce($this->loaders, $reduce, $default); + } +} diff --git a/src/Loaders/JsonFileLoader.php b/src/Loaders/JsonFileLoader.php new file mode 100644 index 0000000..7439ca3 --- /dev/null +++ b/src/Loaders/JsonFileLoader.php @@ -0,0 +1,45 @@ +filepath($file); + + if (null === $path) { + return null; + } + + $jsonStr = \file_get_contents($path); + + if (false === $jsonStr) { + throw new ConfigException( + 'Failed to read file: '.$path + ); + } + + $data = \json_decode($jsonStr, true); + + if (null === $data) { + throw new ConfigException( + 'json_decode error in '.$path.': ' . \json_last_error_msg() + ); + } + + return $data; + } +} diff --git a/src/Loaders/PhpFileLoader.php b/src/Loaders/PhpFileLoader.php new file mode 100644 index 0000000..765ec11 --- /dev/null +++ b/src/Loaders/PhpFileLoader.php @@ -0,0 +1,27 @@ +filepath($file); + + if (null === $path) { + return null; + } + + return require $path; + } +}