diff --git a/composer.json b/composer.json index ab310fd..fa8596b 100644 --- a/composer.json +++ b/composer.json @@ -4,29 +4,37 @@ "type": "project", "require": { "php": ">=7.1", - "dflydev/dot-access-data": "1.1", - "symfony/filesystem": "^3.3", - "symfony/yaml": "^3.3" + "dflydev/dot-access-data": "^1.1", + "symfony/filesystem": "^3.3" + }, "autoload": { "psr-4": { - "Sikofitt\\Configuration\\": "src/Sikofitt/Configuration" + "Sikofitt\\Config\\": "src/Sikofitt/Config" } }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.6", + "nette/neon": "^2.4", "phpro/grumphp": "^0.12.0", "phpstan/phpstan": "^0.8.5", "phpunit/phpunit": "~5.7|~6.3", "sensiolabs/security-checker": "^4.1", "squizlabs/php_codesniffer": "^3.1", - "symfony/var-dumper": "^3.3" + "symfony/var-dumper": "^3.3", + "symfony/yaml": "^3.3", + "webmozart/json": "^1.2" }, "autoload-dev": { "psr-4": { "Sikofitt\\Tests\\": "tests/Sikofitt/Tests" } }, + "suggest": { + "webmozart/json": "For using the webmozart json parser. Instead of the default php one.", + "nette/neon": "For parsing .neon files.", + "symfony/yaml": "For parsing yaml files." + }, "license": "GPL-3.0", "authors": [ { diff --git a/grumphp.yml b/grumphp.yml index a5509b1..e8cb104 100644 --- a/grumphp.yml +++ b/grumphp.yml @@ -29,10 +29,10 @@ parameters: phpversion: project: '7.1' phpcs: - standard: PSR2 + standard: 'phpcs.xml.dist' show_warnings: true encoding: UTF8 - triggered_by: [php] + triggered_by: ['php'] securitychecker: run_always: false xmllint: diff --git a/src/Sikofitt/Config/DotConfig.php b/src/Sikofitt/Config/DotConfig.php index 7992b18..bf5eae0 100644 --- a/src/Sikofitt/Config/DotConfig.php +++ b/src/Sikofitt/Config/DotConfig.php @@ -20,7 +20,15 @@ namespace Sikofitt\Config; use Dflydev\DotAccessData\Data; -use Symfony\Component\Config\FileLocatorInterface; +use Sikofitt\Config\Loader\IniFileLoader; +use Sikofitt\Config\Loader\JsonFileLoader; +use Sikofitt\Config\Loader\NeonFileLoader; +use Sikofitt\Config\Loader\PhpFileLoader; +use Sikofitt\Config\Loader\YamlFileLoader; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\DelegatingLoader; +use Symfony\Component\Config\Loader\GlobFileLoader; +use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Filesystem\Exception\FileNotFoundException; use Symfony\Component\Yaml\Yaml; @@ -33,26 +41,60 @@ class DotConfig { private $config; private $arrayConfig; + private $delegateLoader; /** * Configuration constructor. * - * @param \Symfony\Component\Config\FileLocatorInterface $fileLocator - * @param string $configDirectory + * @param string|array $configResource * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\Yaml\Exception\ParseException - * @throws \Symfony\Component\Config\Exception\FileLocatorFileNotFoundException + * @throws \Exception */ public function __construct( - FileLocatorInterface $fileLocator, - string $configDirectory + $configResource ) { - $file = $fileLocator->locate('config.yaml', $configDirectory); - $contents = file_get_contents($file); - $data = Yaml::parse($contents); - $this->arrayConfig = $data; - $this->config = new Data($data); + $this->arrayConfig = []; + + $locator = new FileLocator(); + $loaderResolver = new LoaderResolver(); + $loaderResolver->addLoader(new PhpFileLoader($locator)); + $loaderResolver->addLoader(new JsonFileLoader($locator)); + $loaderResolver->addLoader(new IniFileLoader($locator)); + $loaderResolver->addLoader(new GlobFileLoader($locator)); + + if (class_exists('Symfony\Component\Yaml\Yaml')) { + $loaderResolver->addLoader(new YamlFileLoader($locator)); + } + if (class_exists('Nette\Neon\Neon')) { + $loaderResolver->addLoader(new NeonFileLoader($locator)); + } + + $this->delegateLoader = new DelegatingLoader($loaderResolver); + + if (is_array($configResource)) { + $configResource = $this->loadDirectories($configResource); + $configResource = $this->loadFiles($configResource); + + $configResource = array_shift($configResource); + + if (!empty($configResource)) { + $this->arrayConfig = array_merge_recursive( + $configResource, + $this->arrayConfig + ); + } + } else { + if (is_file($configResource)) { + $this->arrayConfig = $this->delegateLoader->load($configResource); + } + } + + $this->config = new Data($this->arrayConfig); + } + + public function getArrayConfig(): array + { + return $this->arrayConfig; } /** @@ -70,7 +112,7 @@ class DotConfig $data[$key] = (array)$value; } } - + $data = Yaml::dump($data, 4, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); return (bool)file_put_contents($fileName, $data); } @@ -245,4 +287,54 @@ class DotConfig { return $this->config->has($key); } + + /** + * @param array $directories + * + * @throws \Exception + * @return array + */ + private function loadDirectories(array $directories): array + { + foreach ($directories as $key => $config) { + if (is_string($config) && is_dir($config)) { + $this->arrayConfig = array_merge_recursive( + $this->arrayConfig, + $this->delegateLoader->load($config . '/**', 'glob') + ); + unset($directories[$key]); + $this->arrayConfig = $this->mergeGlob($this->arrayConfig); + } + } + return $directories; + } + + /** + * @param array $files + * + * @throws \Exception + * @return array + */ + private function loadFiles(array $files): array + { + foreach ($files as $key => $config) { + if (is_string($config) && is_file($config)) { + $this->arrayConfig = array_merge_recursive($this->arrayConfig, $this->delegateLoader->load($config)); + unset($files[$key]); + } + } + + return $files; + } + + private function mergeGlob(array $globResource): array + { + $result = []; + + foreach ($globResource as $glob) { + $result = array_merge_recursive($result, $glob); + } + + return $result; + } } diff --git a/src/Sikofitt/Config/Loader/Exception/JsonDecodingException.php b/src/Sikofitt/Config/Loader/Exception/JsonDecodingException.php new file mode 100644 index 0000000..b20a320 --- /dev/null +++ b/src/Sikofitt/Config/Loader/Exception/JsonDecodingException.php @@ -0,0 +1,46 @@ +. + */ + +namespace Sikofitt\Config\Loader\Exception; + +use Throwable; + +/** + * An Error Exception. + * + * @link http://php.net/manual/en/class.errorexception.php + */ +class JsonDecodingException extends \ErrorException +{ + public function __construct( + $severity = E_RECOVERABLE_ERROR, + $fileName = __FILE__, + $line = __LINE__, + Throwable $previous + ) { + parent::__construct( + json_last_error_msg(), + json_last_error(), + $severity, + $fileName, + $line, + $previous + ); + } +} diff --git a/src/Sikofitt/Config/Loader/IniFileLoader.php b/src/Sikofitt/Config/Loader/IniFileLoader.php new file mode 100644 index 0000000..1fe32d8 --- /dev/null +++ b/src/Sikofitt/Config/Loader/IniFileLoader.php @@ -0,0 +1,57 @@ +. + */ + +namespace Sikofitt\Config\Loader; + +use Symfony\Component\Config\Loader\FileLoader; + +class IniFileLoader extends FileLoader +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * + * @throws \Exception If something went wrong + * @return array|bool + * + */ + public function load($resource, $type = null) + { + $data = parse_ini_file($resource, true); + + return false === $data ? [] : $data; + } + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + return + $type === 'ini' || + pathinfo($resource, PATHINFO_EXTENSION) === 'ini'; + } +} diff --git a/src/Sikofitt/Config/Loader/JsonFileLoader.php b/src/Sikofitt/Config/Loader/JsonFileLoader.php new file mode 100644 index 0000000..cad4c10 --- /dev/null +++ b/src/Sikofitt/Config/Loader/JsonFileLoader.php @@ -0,0 +1,62 @@ +. + */ + +namespace Sikofitt\Config\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Webmozart\Json\JsonDecoder; + +class JsonFileLoader extends FileLoader +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * + * @throws \Exception If something went wrong + * @return mixed + * + */ + public function load($resource, $type = null) + { + if (class_exists('Webmozart\Json\JsonDecoder')) { + $decoder = new JsonDecoder(); + + return (array)$decoder->decodeFile($resource); + } + + return \json_decode(file_get_contents($resource), true); + } + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null): bool + { + return + $type === 'json' || + pathinfo($resource, PATHINFO_EXTENSION) === 'json'; + } +} diff --git a/src/Sikofitt/Config/Loader/NeonFileLoader.php b/src/Sikofitt/Config/Loader/NeonFileLoader.php new file mode 100644 index 0000000..cb350fc --- /dev/null +++ b/src/Sikofitt/Config/Loader/NeonFileLoader.php @@ -0,0 +1,56 @@ +. + */ + +namespace Sikofitt\Config\Loader; + +use Nette\Neon\Neon; +use Symfony\Component\Config\Loader\FileLoader; + +class NeonFileLoader extends FileLoader +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * + * @throws \Exception If something went wrong + * @return mixed + * + */ + public function load($resource, $type = null) + { + return Neon::decode(file_get_contents($resource)); + } + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + return + $type === 'neon' || + pathinfo($resource, PATHINFO_EXTENSION) === 'neon'; + } +} diff --git a/src/Sikofitt/Config/Loader/PhpFileLoader.php b/src/Sikofitt/Config/Loader/PhpFileLoader.php new file mode 100644 index 0000000..d2fcfde --- /dev/null +++ b/src/Sikofitt/Config/Loader/PhpFileLoader.php @@ -0,0 +1,60 @@ +. + */ + +namespace Sikofitt\Config\Loader; + +use Symfony\Component\Config\Loader\FileLoader; + +class PhpFileLoader extends FileLoader +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * + * @throws \Exception If something went wrong + * @return array + * + */ + public function load($resource, $type = null): array + { + return + require $resource; + } + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null): bool + { + return + $type === 'php' || + in_array( + pathinfo($resource, PATHINFO_EXTENSION), + ['php', 'inc'], + true + ); + } +} diff --git a/src/Sikofitt/Config/Loader/YamlFileLoader.php b/src/Sikofitt/Config/Loader/YamlFileLoader.php new file mode 100644 index 0000000..94c39fb --- /dev/null +++ b/src/Sikofitt/Config/Loader/YamlFileLoader.php @@ -0,0 +1,55 @@ +. + */ + +namespace Sikofitt\Config\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Yaml\Yaml; + +class YamlFileLoader extends FileLoader +{ + /** + * Loads a resource. + * + * @param mixed $resource The resource + * @param string|null $type The resource type or null if unknown + * + * @throws \Exception If something went wrong + * @return mixed + */ + public function load($resource, $type = null) + { + return Yaml::parse(file_get_contents($resource)); + } + + /** + * Returns whether this class supports the given resource. + * + * @param mixed $resource A resource + * @param string|null $type The resource type or null if unknown + * + * @return bool True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + return + is_string($resource) && + in_array(pathinfo($resource, PATHINFO_EXTENSION), ['yaml', 'yml'], true); + } +} diff --git a/tests/fixtures/config.ini b/tests/fixtures/config.ini new file mode 100644 index 0000000..7cf66f5 --- /dev/null +++ b/tests/fixtures/config.ini @@ -0,0 +1,4 @@ +[config] +value = ini +ini_test[] = testing1 +ini_test[] = testing2 \ No newline at end of file diff --git a/tests/fixtures/config.json b/tests/fixtures/config.json new file mode 100644 index 0000000..81479f3 --- /dev/null +++ b/tests/fixtures/config.json @@ -0,0 +1,9 @@ +{ + "config": { + "value": "json", + "json_test": [ + "testing1", + "testing2" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/config.neon b/tests/fixtures/config.neon new file mode 100644 index 0000000..044b542 --- /dev/null +++ b/tests/fixtures/config.neon @@ -0,0 +1,5 @@ +config: + value: neon + neon_test: + - testing1 + - testing2 diff --git a/tests/fixtures/config.php b/tests/fixtures/config.php new file mode 100644 index 0000000..5dea2cc --- /dev/null +++ b/tests/fixtures/config.php @@ -0,0 +1,28 @@ +. + */ + +return [ + 'config' => [ + 'value' => 'php', + 'php_test' => [ + 'testing1', + 'testing2', + ], + ], +]; diff --git a/tests/fixtures/config.yaml b/tests/fixtures/config.yaml new file mode 100644 index 0000000..bd405df --- /dev/null +++ b/tests/fixtures/config.yaml @@ -0,0 +1,5 @@ +config: + value: 'yaml' + yaml_test: + - testing + - testing1 \ No newline at end of file