commit b27234e5633dd99d30b24bad533c01bf35eba5d8 Author: Ron Rise Date: Wed Nov 1 18:01:51 2023 -0400 Herping the derp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7bd3bb5 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +stages: + - deploy + +deploy: + stage: deploy + image: alpine:latest + only: + - tags + tags: + - build + script: + - apk add curl + - 'curl -iL --header "Job-Token: $CI_JOB_TOKEN" --data tag=${CI_COMMIT_TAG} "${CI_API_V4_URL}/projects/$CI_PROJECT_ID/packages/composer"' + environment: production diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bbf03fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,195 @@ +# Changelog + +All notable changes to `Config` will be documented in this file + +## 2.2.0 - 2020-12-07 + +### Added +- Serialization support (#127) +- Support for Properties files (#128) + +### Fixed +- Test enhancement (#126) +- Typehint on Xml parser, parse method (#130) + +### Fixed + +## 2.1.0 - 2019-09-01 + +### Added +- Support for writing configuration back to file and string (#122) + +## 2.0.2 - 2019-04-06 + +### Fixed +- Implementations of `ParserInterface` and cleanup (#120) +- Tests for PHP 7 + +## 2.0.1 - 2019-02-02 + +### Fixed +- Parsing PHP file (#114) +- Parsing PHP string with `$config` variable (#118) + +## 2.0.0 - 2018-10-03 + +### Added +- Usage of short array syntax (#109) +- Support for string parsers (#111) + +### Breaking changes +- Changes of interface and parsers + +## 1.1.0 - 2018-08-22 + +### Added +- Added support for PHP constants in YAML (#112) + +## 1.0.1 - 2018-03-31 + +### Fixed +- Possibility to use an own file parser (#103) + +## 1.0.0 - 2018-03-03 + +### Added +- Merge support (#96) +- Set PHP 5.5.9 as minimum required version (#75 and #99) + +### Fixed +- Fix PHP 5.6 test (#100) +- Edit PHP versions tested on Travis (#101) +- Add more info about the symfony/yaml requirement (#97 and #102) + +### Breaking changes +- PHP 5.3 and 5.4 are no longer supported. + +## 0.10.0 - 2016-02-11 + +### Added +- Package-level exceptions so callers can catch exceptions at package-level +- Added support for files suffixed with the `.dist` extension + +### Fixed +- Rearranged error-handling in `FileParser\Json` for better test coverage +- Project-wide code style fixes to adhere to PSR-2 +- Fixes `has()` method returning `false` on `null` values in a config field + +## 0.9.1 - 2016-01-23 + +### Added +- PHP 7.0 is now tested on Travis + +## 0.9.0 - 2015-10-22 + +### Added +- Added namespace to example in `README.md` +- Added `has()` method to `ConfigInterface` and implemented in `AbstractConfig` +- Added `all()` method to `ConfigInterface` and implemented in `AbstractConfig` +- Added documentation for new methods +- `AbstractConfig` now implements the `Iterator` interface + +### Fixed +- PSR-2 compliance +- Give YamlParser file content instead of path +- Updated `AbstractConfig` constructor to only accept arrays +- Removed check to fix loading an empty array +- Fix for #44: Warnings emitted if configuration file is empty +- Fix for #55: Unset cache after a set + + +## 0.8.2 - 2015-03-21 + +### Fixed +- Some code smells in `Config` +- Updated README.md + + +## 0.8.1 - 2015-03-21 + +### Fixed +- Various things relating to recent repo transfer + + +## 0.8.0 - 2015-03-21 + +### Added +- Individual `FileParser` classes for each filetype, and a `FileParserInterface` to type-hint methods with +- Optional paths; you can now prefix a path with '?' and `Config` will skip the file if it doesn't exist + +### Fixed +- Made the Symfony YAML component a suggested dependency +- Parent constructor was not being called from `Config` + + +## 0.7.1 - 2015-02-24 + +### Added +- Moved file logic into file-specific loaders + +### Fixed +- Corrected class name in README.md + + +## 0.7.0 - 2015-02-23 + +### Fixed +- Removed kludgy hack for YAML/YML + + +## 0.6.0 - 2015-02-23 + +### Added +- Can now extend `AbstractConfig` to create simple subclasses without any file IO + + +## 0.5.0 - 2015-02-23 + +### Added +- Moved file logic into file-specific loaders + +### Fixed +- Cleaned up exception class constructors, PSR-2 compliance + + +## 0.4.0 - 2015-02-22 + +### Fixed +- Moved file logic into file-specific loaders + + +## 0.3.0 - 2015-02-22 + +### Fixed +- Created new classes `ConfigInterface` and `AbstractConfig` to simplify code + + +## 0.2.1 - 2015-02-22 + +### Added +- Array and directory support in constructor + +### Fixed +- Corrected deprecated usage of `Symfony\Yaml` + +## 0.2.0 - 2015-02-21 + +### Added +- Array and directory support in constructor + +### Fixed +- Now can load .YAML and .YML files + + +## 0.1.0 - 2014-11-27 + +### Added +- Uses PSR-4 for autoloading +- Supports YAML +- Now uses custom exceptions + + +## 0.0.1 - 2014-11-19 + +### Added +- Tagged first release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5f9dabb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [GitHub](https://github.com/noodlehaus/config). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Use Git Flow** - Don't ask us to pull from your master branch. Set up [Git Flow](http://nvie.com/posts/a-successful-git-branching-model/) and create a new feature branch from `develop` + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. + + +## Running Tests + +``` bash +$ phpunit +``` + + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e26e6ee --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright © 2015 Hassan Khan + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d211359 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "siteworxpro/config", + "type": "library", + "description": "Lightweight configuration file loader that supports PHP, INI, XML, JSON, and YAML files", + "keywords": ["configuration", "config", "json", "yaml", "yml", "ini", "xml", "unframework", "microphp"], + "homepage": "http://hassankhan.me/config/", + "license": "MIT", + "authors": [ + { + "name": "Hassan Khan", + "role": "Developer", + "homepage": "http://hassankhan.me/" + }, + { + "name": "Ron Rise", + "role": "Contributer" + } + ], + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "scrutinizer/ocular": "~1.1", + "squizlabs/php_codesniffer": "~3.6.1", + "slevomat/coding-standard": "^7.0.18", + "symfony/yaml": "~3.4", + "ext-dom": "*", + "ext-libxml": "*" + }, + "suggest": { + "symfony/yaml": "~3.4" + }, + "autoload": { + "psr-4": { + "Siteworx\\Config\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Siteworx\\Config\\Test\\": "tests" + } + } +} diff --git a/rules.xml b/rules.xml new file mode 100644 index 0000000..12a5c0a --- /dev/null +++ b/rules.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AbstractConfig.php b/src/AbstractConfig.php new file mode 100644 index 0000000..14390cc --- /dev/null +++ b/src/AbstractConfig.php @@ -0,0 +1,294 @@ + + * @author Hassan Khan + * @link https://github.com/noodlehaus/config + * @license MIT + */ +abstract class AbstractConfig implements ArrayAccess, ConfigInterface, Iterator +{ + + /** + * Stores the configuration data + * + * @var array|null + */ + protected ?array $data = null; + + /** + * Caches the configuration data + * + * @var array + */ + protected array $cache = []; + + /** + * Constructor method and sets default options, if any + * + * @param array $data + */ + #[Pure] + public function __construct(array $data) + { + $this->data = array_merge($this->getDefaults(), $data); + } + + /** + * Override this method in your own subclass to provide an array of default + * options and values + * + * @return array + * + * @codeCoverageIgnore + */ + protected function getDefaults(): array + { + return []; + } + + /** + * ConfigInterface Methods + */ + + /** + * {@inheritDoc} + */ + public function get(string $key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * {@inheritDoc} + */ + public function set(string $key, mixed $value): void + { + $segs = explode('.', $key); + $root = &$this->data; + $cacheKey = ''; + + // Look for the key, creating nested keys if needed + while ($part = array_shift($segs)) { + if ($cacheKey !== '') { + $cacheKey .= '.'; + } + $cacheKey .= $part; + if (!isset($root[$part]) && count($segs)) { + $root[$part] = []; + } + $root = &$root[$part]; + + //Unset all old nested cache + if (isset($this->cache[$cacheKey])) { + unset($this->cache[$cacheKey]); + } + + //Unset all old nested cache in case of array + if (count($segs) === 0) { + foreach ($this->cache as $cacheLocalKey => $cacheValue) { + if (str_starts_with($cacheLocalKey, $cacheKey)) { + unset($this->cache[$cacheLocalKey]); + } + } + } + } + + // Assign value at target node + $this->cache[$key] = $root = $value; + } + + /** + * {@inheritDoc} + */ + public function has(string $key): bool + { + // Check if already cached + if (isset($this->cache[$key])) { + return true; + } + + $segments = explode('.', $key); + $root = $this->data; + + // nested case + foreach ($segments as $segment) { + if (array_key_exists($segment, $root)) { + $root = $root[$segment]; + continue; + } + + return false; + } + + // Set cache for the given key + $this->cache[$key] = $root; + + return true; + } + + /** + * Merge config from another instance + * + * @param ConfigInterface $config + * @return ConfigInterface + */ + public function merge(ConfigInterface $config): ConfigInterface + { + $this->data = array_replace_recursive($this->data, $config->all()); + return $this; + } + + /** + * {@inheritDoc} + */ + public function all(): array + { + return $this->data; + } + + /** + * ArrayAccess Methods + */ + + /** + * Gets a value using the offset as a key + * + * @param string $offset + * + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + /** + * Checks if a key exists + * + * @param string $offset + * + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return $this->has($offset); + } + + /** + * Sets a value using the offset as a key + * + * @param mixed $offset + * @param mixed $value + * + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->set($offset, $value); + } + + /** + * Deletes a key and its value + * + * @param string $offset + * + * @return void + */ + public function offsetUnset(mixed $offset): void + { + $this->set($offset, null); + } + + /** + * Iterator Methods + */ + + /** + * Returns the data array element referenced by its internal cursor + * + * @return mixed The element referenced by the data array's internal cursor. + * If the array is empty or there is no element at the cursor, the + * function returns false. If the array is undefined, the function + * returns null + */ + public function current(): mixed + { + return is_array($this->data) ? current($this->data) : null; + } + + /** + * Returns the data array index referenced by its internal cursor + * + * @return int|string|null The index referenced by the data array's internal cursor. + * If the array is empty or undefined or there is no element at the + * cursor, the function returns null + */ + public function key(): int|string|null + { + return is_array($this->data) ? key($this->data) : null; + } + + /** + * Moves the data array's internal cursor forward one element + * + * @return mixed The element referenced by the data array's internal cursor + * after the move is completed. If there are no more elements in the + * array after the move, the function returns false. If the data array + * is undefined, the function returns null + */ + #[\ReturnTypeWillChange] + public function next(): mixed + { + return is_array($this->data) ? next($this->data) : null; + } + + /** + * Moves the data array's internal cursor to the first element + * + * @return mixed The element referenced by the data array's internal cursor + * after the move is completed. If the data array is empty, the function + * returns false. If the data array is undefined, the function returns + * null + */ + #[\ReturnTypeWillChange] + public function rewind(): mixed + { + return is_array($this->data) ? reset($this->data) : null; + } + + /** + * Tests whether the iterator's current index is valid + * + * @return bool True if the current index is valid; false otherwise + */ + public function valid(): bool + { + return is_array($this->data) && key($this->data) !== null; + } + + /** + * Remove a value using the offset as a key + * + * @param string $key + * + * @return void + */ + public function remove(string $key): void + { + $this->offsetUnset($key); + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..f9ec4a7 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,324 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Config extends AbstractConfig +{ + + /** + * All formats supported by Config. + * + * @var array + */ + protected array $supportedParsers = [ + Php::class, + Ini::class, + Json::class, + Xml::class, + Yaml::class, + Properties::class, + Serialize::class + ]; + + /** + * All formats supported by Config. + * + * @var array + */ + protected array $supportedWriters = [ + Writer\Ini::class, + Writer\Json::class, + Writer\Xml::class, + Writer\Yaml::class, + Writer\Properties::class, + Writer\Serialize::class + ]; + + /** + * Static method for loading a Config instance. + * + * @param string|array $paths Filenames or string with configuration + * @param ParserInterface|null $parser Configuration parser + * @param bool $loadFromString + * @return Config + * @throws EmptyDirectoryException + * @throws FileNotFoundException + * @throws UnsupportedFormatException + */ + public static function load( + string | array $paths, + ?ParserInterface $parser = null, + bool $loadFromString = false + ): Config { + return new static($paths, $parser, $loadFromString); + } + + /** + * Loads a Config instance. + * + * @param string|array $values Filenames or string with configuration + * @param ParserInterface | null $parser Configuration parser + * @throws EmptyDirectoryException + * @throws FileNotFoundException + * @throws UnsupportedFormatException + */ + private function __construct(string | array $values, ParserInterface $parser = null, bool $loadFromString = false) + { + if ($loadFromString && !is_array($values) && !file_exists($values)) { + if ($parser === null) { + throw new \InvalidArgumentException('Parser is required to be provided for a string'); + } + + $this->loadFromString($values, $parser); + } else { + $this->loadFromFile($values, $parser); + } + + parent::__construct($this->data); + } + + /** + * Loads configuration from file. + * + * @param string|array $path Filenames or directories with configuration + * @param ParserInterface | null $parser Configuration parser + * + * @throws EmptyDirectoryException If `$path` is an empty directory + * @throws FileNotFoundException + * @throws UnsupportedFormatException + */ + protected function loadFromFile(string | array $path, ?ParserInterface $parser = null): void + { + $paths = $this->getValidPaths($path); + $this->data = []; + + foreach ($paths as $filePath) { + if ($parser === null) { + // Get file information + $info = pathinfo($filePath); + $parts = explode('.', $info['basename']); + $extension = array_pop($parts); + + // Skip the `dist` extension + if ($extension === 'dist') { + $extension = array_pop($parts); + } + + // Get file parser + $parser = $this->getParser($extension); + + // Try to load file + $newData = $parser->parseFile($filePath); + $oldData = $this->data; + + $this->data = array_merge($oldData, $newData); + + // Clean parser + $parser = null; + } else { + $newData = $parser->parseFile($filePath); + $oldData = $this->data; + + $this->data = array_merge($oldData, $newData); + } + } + } + + /** + * Writes configuration to file. + * + * @param string $filename Filename to save configuration to + * @param WriterInterface|null $writer Configuration writer + * + * @throws Exception\WriteException if the data could not be written to the file + * @throws UnsupportedFormatException + */ + public function toFile(string $filename, WriterInterface $writer = null): void + { + if ($writer === null) { + // Get file information + $info = pathinfo($filename); + $parts = explode('.', $info['basename']); + $extension = array_pop($parts); + + // Skip the `dist` extension + if ($extension === 'dist') { + $extension = array_pop($parts); + } + + // Get file writer + $writer = $this->getWriter($extension); + + // Try to save file + $writer->toFile($this->all(), $filename); + + // Clean writer + $writer = null; + } else { + // Try to load file using specified writer + $writer->toFile($this->all(), $filename); + } + } + + /** + * Loads configuration from string. + * + * @param string $configuration String with configuration + * @param ParserInterface $parser Configuration parser + */ + protected function loadFromString(string $configuration, ParserInterface $parser): void + { + $this->data = []; + + // Try to parse string + $this->data = array_replace_recursive($this->data, $parser->parseString($configuration)); + } + + /** + * Writes configuration to string. + * + * @param WriterInterface $writer Configuration writer + * @param boolean $pretty Encode pretty + */ + public function toString(WriterInterface $writer, bool $pretty = true): string + { + return $writer->toString($this->all(), $pretty); + } + + /** + * Gets a parser for a given file extension. + * + * @throws UnsupportedFormatException If `$extension` is an unsupported file format + */ + protected function getParser(string $extension): ParserInterface + { + foreach ($this->supportedParsers as $parser) { + if (in_array($extension, $parser::getSupportedExtensions(), true)) { + return new $parser(); + } + } + + // If none exist, then throw an exception + throw new UnsupportedFormatException('Unsupported configuration format'); + } + + /** + * Gets a writer for a given file extension. + * + * @throws UnsupportedFormatException If `$extension` is an unsupported file format + */ + protected function getWriter(string $extension): WriterInterface + { + foreach ($this->supportedWriters as $writer) { + if (in_array($extension, $writer::getSupportedExtensions(), true)) { + return new $writer(); + } + } + + // If none exist, then throw an exception + throw new UnsupportedFormatException('Unsupported configuration format' . $extension); + } + + /** + * Gets an array of paths + * + * @param array $path + * + * @return array + * + * @throws FileNotFoundException|EmptyDirectoryException If a file is not found at `$path` + */ + protected function getPathsFromArray(array $path): array + { + $paths = []; + + foreach ($path as $unverifiedPath) { + try { + // Check if `$unverifiedPath` is optional + // If it exists, then it's added to the list + // If it doesn't, it throws an exception which we catch + if ($unverifiedPath[0] !== '?') { + $validPaths = $this->getValidPaths($unverifiedPath); + $originalPaths = $paths; + $paths = array_merge($originalPaths, $validPaths); + continue; + } + + $optionalPath = ltrim($unverifiedPath, '?'); + + $validPaths = $this->getValidPaths($optionalPath); + $originalPaths = $paths; + + $paths = array_merge($originalPaths, $validPaths); + } catch (FileNotFoundException $e) { + // If `$unverifiedPath` is optional, then skip it + if ($unverifiedPath[0] === '?') { + continue; + } + + // Otherwise, rethrow the exception + throw $e; + } + } + + return $paths; + } + + /** + * Checks `$path` to see if it is either an array, a directory, or a file. + * + * @param string|array $path + * + * @return array + * + * @throws EmptyDirectoryException If `$path` is an empty directory + * + * @throws FileNotFoundException If a file is not found at `$path` + */ + protected function getValidPaths(string | array $path): array + { + if (is_array($path)) { + return $this->getPathsFromArray($path); + } + + // If `$path` is a directory + if (is_dir($path)) { + $paths = glob($path . '/*.*'); + if (empty($paths)) { + throw new EmptyDirectoryException("Configuration directory: [$path] is empty"); + } + + return $paths; + } + + // If `$path` is not a file, throw an exception + if (!file_exists($path)) { + throw new FileNotFoundException("Configuration file: [$path] cannot be found"); + } + + return [$path]; + } +} diff --git a/src/ConfigInterface.php b/src/ConfigInterface.php new file mode 100644 index 0000000..f0e5cc2 --- /dev/null +++ b/src/ConfigInterface.php @@ -0,0 +1,55 @@ + + * @author Hassan Khan + * @link https://github.com/noodlehaus/config + * @license MIT + */ +interface ConfigInterface +{ + /** + * Gets a configuration setting using a simple or nested key. + * Nested keys are similar to JSON paths that use the dot + * dot notation. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get(string $key, mixed $default = null): mixed; + + /** + * Function for setting configuration values, using + * either simple or nested keys. + * + * @param string $key + * @param mixed $value + * + * @return void + */ + public function set(string $key, mixed $value): void; + + /** + * Function for checking if configuration values exist, using + * either simple or nested keys. + * + * @param string $key + * + * @return boolean + */ + public function has(string $key): bool; + + /** + * Get all the configuration items + * + * @return array + */ + public function all(): array; +} diff --git a/src/ErrorException.php b/src/ErrorException.php new file mode 100644 index 0000000..b26f534 --- /dev/null +++ b/src/ErrorException.php @@ -0,0 +1,7 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +abstract class AbstractParser implements ParserInterface +{ + + /** + * String with configuration + * + * @var string + */ + protected string $config; + + /** + * Sets the string with configuration + * + * @param string $config + * + * @codeCoverageIgnore + */ + public function __construct(string $config) + { + $this->config = $config; + } +} diff --git a/src/Parser/Ini.php b/src/Parser/Ini.php new file mode 100644 index 0000000..71295c7 --- /dev/null +++ b/src/Parser/Ini.php @@ -0,0 +1,115 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Ini implements ParserInterface +{ + /** + * {@inheritDoc} + * Parses an INI file as an array + * + * @throws ParseException If there is an error parsing the INI file + */ + public function parseFile(string $filename): array + { + $data = parse_ini_file($filename, true); + return $this->parse($data, $filename); + } + + /** + * {@inheritDoc} + * Parses an INI string as an array + * + * @throws ParseException If there is an error parsing the INI string + */ + public function parseString(string $config): array + { + try { + $data = parse_ini_string($config, true); + } catch (\Exception $exception) { + throw new ParseException(['message' => $exception->getMessage()]); + } + + return $this->parse($data); + } + + /** + * Completes parsing of INI data + * + * @param array | null $data + * @param string | null $filename + * + * @return array + * @throws ParseException If there is an error parsing the INI data + */ + protected function parse(?array $data = null, ?string $filename = null): array + { + if (!$data) { + $error = error_get_last(); + + // Parse functions may return NULL but set no error if the string contains no parsable data + if (!is_array($error)) { + $error["message"] = "No parsable content in data."; + } + + $error["file"] = $filename; + + // if string contains no parsable data, no error is set, resulting in any previous error + // persisting in error_get_last(). in php 7 this can be addressed with error_clear_last() + if (function_exists("error_clear_last")) { + error_clear_last(); + } + + throw new ParseException($error); + } + + return $this->expandDottedKey($data); + } + + /** + * Expand array with dotted keys to multidimensional array + * + * @param array $data + * + * @return array + */ + protected function expandDottedKey(array $data): array + { + foreach ($data as $key => $value) { + $found = strpos($key, '.'); + if ($found !== false) { + $newKey = substr($key, 0, $found); + $remainder = substr($key, $found + 1); + + $expandedValue = $this->expandDottedKey([$remainder => $value]); + if (isset($data[$newKey])) { + $data[$newKey] = array_merge_recursive($data[$newKey], $expandedValue); + } else { + $data[$newKey] = $expandedValue; + } + unset($data[$key]); + } + } + return $data; + } + + /** + * {@inheritDoc} + */ + public static function getSupportedExtensions(): array + { + return ['ini']; + } +} diff --git a/src/Parser/Json.php b/src/Parser/Json.php new file mode 100644 index 0000000..3a3a06c --- /dev/null +++ b/src/Parser/Json.php @@ -0,0 +1,85 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Json implements ParserInterface +{ + /** + * {@inheritDoc} + * Parses an JSON file as an array + * + * @throws ParseException If there is an error parsing the JSON file + */ + public function parseFile(string $filename): array + { + try { + $data = json_decode(file_get_contents($filename), true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ParseException(['message' => $exception->getMessage()]); + } + + return (array) $this->parse($data, $filename); + } + + /** + * {@inheritDoc} + * Parses an JSON string as an array + * + * @throws ParseException|\JsonException If there is an error parsing the JSON string + */ + public function parseString(string $config): array + { + $data = json_decode($config, true, 512, JSON_THROW_ON_ERROR); + + return (array) $this->parse($data); + } + + /** + * Completes parsing of JSON data + * + * @param array | null $data + * @param string | null $filename + * @return array | null + * + * @throws ParseException If there is an error parsing the JSON data + */ + protected function parse(array $data = null, string $filename = null): ?array + { + if (json_last_error() !== JSON_ERROR_NONE) { + $error_message = 'Syntax error'; + if (function_exists('json_last_error_msg')) { + $error_message = json_last_error_msg(); + } + + $error = [ + 'message' => $error_message, + 'type' => json_last_error(), + 'file' => $filename, + ]; + + throw new ParseException($error); + } + + return $data; + } + + /** + * {@inheritDoc} + */ + public static function getSupportedExtensions(): array + { + return ['json']; + } +} diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 0000000..728be7a --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,41 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +interface ParserInterface +{ + /** + * Parses a configuration from file `$filename` and gets its contents as an array + * + * @param string $filename + * + * @return array + */ + public function parseFile(string $filename): array; + + /** + * Parses a configuration from string `$config` and gets its contents as an array + * + * @param string $config + * + * @return array + */ + public function parseString(string $config): array; + + /** + * Returns an array of allowed file extensions for this parser + * + * @return array + */ + public static function getSupportedExtensions(): array; +} diff --git a/src/Parser/Php.php b/src/Parser/Php.php new file mode 100644 index 0000000..89df0ad --- /dev/null +++ b/src/Parser/Php.php @@ -0,0 +1,132 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Php implements ParserInterface +{ + /** + * {@inheritDoc} + * Loads a PHP file and gets its' contents as an array + * + * @throws ParseException If the PHP file throws an exception + * @throws UnsupportedFormatException If the PHP file does not return an array + */ + public function parseFile(string $filename): array + { + // Run the fileEval the string, if it throws an exception, rethrow it + try { + $data = include $filename; + } catch (\Exception $exception) { + throw new ParseException( + [ + 'message' => 'PHP file threw an exception', + 'exception' => $exception, + ] + ); + } + + if (!is_array($data) && !is_callable($data) && $data !== null) { + throw new ParseException( + [ + 'message' => 'PHP file threw an exception' + ] + ); + } + + return (array) $this->parse($data, $filename); + } + + /** + * {@inheritDoc} + * Loads a PHP string and gets its' contents as an array + * + * @throws ParseException If the PHP string throws an exception + * @throws UnsupportedFormatException If the PHP string does not return an array + */ + public function parseString(string $config): array + { + // Handle PHP start tag + $config = trim($config); + if (str_starts_with($config, '' . $config; + } + + // Eval the string, if it throws an exception, rethrow it + try { + $data = $this->isolate($config); + } catch (\Exception $exception) { + throw new ParseException( + [ + 'message' => 'PHP string threw an exception', + 'exception' => $exception, + ] + ); + } + + // Complete parsing + return (array) $this->parse($data); + } + + /** + * Completes parsing of PHP data + * + * @param array | callable | null $data + * @param string | null $filename + * + * @return array | null + * @throws UnsupportedFormatException + */ + protected function parse(array | callable $data = null, string $filename = null): ?array + { + // If we have a callable, run it and expect an array back + if (is_callable($data)) { + $data = $data(); + } + + // Check for array, if it's anything else, throw an exception + if (!is_array($data)) { + throw new UnsupportedFormatException('PHP data does not return an array'); + } + + return $data; + } + + /** + * Runs PHP string in isolated method + * + * @param string $EGsfKPdue7ahnMTy + * + * @return array + */ + protected function isolate(string $EGsfKPdue7ahnMTy): array + { + $eval = eval($EGsfKPdue7ahnMTy); + + if (is_callable($eval)) { + return $eval(); + } + + return $eval; + } + + /** + * {@inheritDoc} + */ + public static function getSupportedExtensions(): array + { + return ['php']; + } +} diff --git a/src/Parser/Properties.php b/src/Parser/Properties.php new file mode 100644 index 0000000..d621880 --- /dev/null +++ b/src/Parser/Properties.php @@ -0,0 +1,60 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Properties implements ParserInterface +{ + /** + * {@inheritdoc} + * Parses a Properties file as an array. + */ + public function parseFile(string $filename): array + { + return $this->parse(file_get_contents($filename)); + } + + /** + * {@inheritdoc} + * Parses a Properties string as an array. + */ + public function parseString(string $config): array + { + return $this->parse($config); + } + + private function parse(string $txtProperties): array + { + $result = []; + + // first remove all escaped whitespace characters: + $txtProperties = preg_replace('/(?parse($data, $filename); + } + + /** + * {@inheritdoc} + * + * @throws ParseException + */ + public function parseString(string $config): array + { + return (array) $this->parse($config); + } + + + /** + * Completes parsing of JSON data + * + * @param string | null $data + * @param string | null $filename + * @return array|null + * + * @throws ParseException If there is an error parsing the serialized data + */ + protected function parse(string $data = null, string $filename = null): ?array + { + try { + $serializedData = unserialize($data, ['allowed_classes' => false]); + } catch (\Exception $exception) { + throw new ParseException(['message' => $exception->getMessage()]); + } + + return $serializedData; + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['txt']; + } +} diff --git a/src/Parser/Xml.php b/src/Parser/Xml.php new file mode 100644 index 0000000..4d9d74a --- /dev/null +++ b/src/Parser/Xml.php @@ -0,0 +1,87 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Xml implements ParserInterface +{ + /** + * {@inheritDoc} + * Parses an XML file as an array + * + * @throws ParseException If there is an error parsing the XML file + */ + public function parseFile(string $filename): array + { + libxml_use_internal_errors(true); + $data = simplexml_load_string(file_get_contents($filename), null, LIBXML_NOERROR); + + if ($data === false) { + throw new ParseException(error_get_last() ?? ['message' => 'Invalid XML']); + } + + return (array) $this->parse($data, $filename); + } + + /** + * {@inheritDoc} + * Parses an XML string as an array + * + * @throws ParseException|\JsonException If there is an error parsing the XML string + */ + public function parseString(string $config): array + { + libxml_use_internal_errors(true); + $data = simplexml_load_string($config, null, LIBXML_NOERROR); + + return (array) $this->parse($data); + } + + /** + * Completes parsing of XML data + * + * @param \SimpleXMLElement | null $data + * @param string | null $filename + * + * @return array|null + * + * @throws ParseException If there is an error parsing the XML data + * @throws \JsonException + */ + protected function parse(\SimpleXMLElement $data = null, string $filename = null): ?array + { + if ($data === false) { + $errors = libxml_get_errors(); + $latestError = array_pop($errors); + $error = [ + 'message' => $latestError->message, + 'type' => $latestError->level, + 'code' => $latestError->code, + 'file' => $filename, + 'line' => $latestError->line, + ]; + throw new ParseException($error); + } + + return json_decode(json_encode($data, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR); + } + + /** + * {@inheritDoc} + */ + public static function getSupportedExtensions(): array + { + return ['xml']; + } +} diff --git a/src/Parser/Yaml.php b/src/Parser/Yaml.php new file mode 100644 index 0000000..45fd112 --- /dev/null +++ b/src/Parser/Yaml.php @@ -0,0 +1,84 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Yaml implements ParserInterface +{ + /** + * {@inheritDoc} + * Loads a YAML/YML file as an array + * + * @throws ParseException If there is an error parsing the YAML file + */ + public function parseFile(string $filename): array + { + try { + $data = YamlParser::parseFile($filename, YamlParser::PARSE_CONSTANT); + } catch (Exception $exception) { + throw new ParseException( + [ + 'message' => 'Error parsing YAML file', + 'exception' => $exception, + ] + ); + } + + return (array) $this->parse($data); + } + + /** + * {@inheritDoc} + * Loads a YAML/YML string as an array + * + * @throws ParseException If If there is an error parsing the YAML string + */ + public function parseString(string $config): array + { + try { + $data = YamlParser::parse($config, YamlParser::PARSE_CONSTANT); + } catch (Exception $exception) { + throw new ParseException( + [ + 'message' => 'Error parsing YAML string', + 'exception' => $exception, + ] + ); + } + + return (array) $this->parse($data); + } + + /** + * Completes parsing of YAML/YML data + * + * @param array | null $data + * + * @return array|null + */ + protected function parse(array $data = null): ?array + { + return $data; + } + + /** + * {@inheritDoc} + */ + public static function getSupportedExtensions(): array + { + return ['yaml', 'yml']; + } +} diff --git a/src/Writer/AbstractWriter.php b/src/Writer/AbstractWriter.php new file mode 100644 index 0000000..f38908c --- /dev/null +++ b/src/Writer/AbstractWriter.php @@ -0,0 +1,37 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +abstract class AbstractWriter implements WriterInterface +{ + /** + * {@inheritdoc} + */ + public function toFile(array $config, string $filename): string + { + if (!is_writable($filename)) { + throw new WriteException(['file' => $filename]); + } + + $contents = $this->toString($config); + $success = file_put_contents($filename, $contents); + if ($success === false) { + throw new WriteException(['file' => $filename]); + } + + return $contents; + } +} diff --git a/src/Writer/Ini.php b/src/Writer/Ini.php new file mode 100644 index 0000000..83b9184 --- /dev/null +++ b/src/Writer/Ini.php @@ -0,0 +1,60 @@ + + * @author Hassan Khan + * @author Filip Š + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Ini extends AbstractWriter +{ + /** + * {@inheritdoc} + * Writes an array to a Ini string. + */ + public function toString(array $config, bool $pretty = true): string + { + return $this->toINI($config); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['ini']; + } + + /** + * Converts array to INI string. + * + * @param array $arr Array to be converted + * @param array $parent Parent array + * + * @return string Converted array as INI + * + * @see https://stackoverflow.com/a/17317168/6523409/ + */ + protected function toINI(array $arr, array $parent = []): string + { + $converted = ''; + + foreach ($arr as $k => $v) { + if (is_array($v)) { + $sec = array_merge((array) $parent, (array) $k); + $converted .= '[' . implode('.', $sec) . ']' . PHP_EOL; + $converted .= $this->toINI($v, $sec); + } else { + $converted .= $k . '=' . $v . PHP_EOL; + } + } + + return $converted; + } +} diff --git a/src/Writer/Json.php b/src/Writer/Json.php new file mode 100644 index 0000000..9b25a56 --- /dev/null +++ b/src/Writer/Json.php @@ -0,0 +1,54 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Json extends AbstractWriter +{ + /** + * {@inheritdoc} + * Writes an array to a JSON file. + */ + public function toFile(array $config, string $filename): string + { + $data = $this->toString($config); + $success = file_put_contents($filename, $data . PHP_EOL); + if ($success === false) { + throw new WriteException(['file' => $filename]); + } + + return $data; + } + + /** + * {@inheritdoc} + * Writes an array to a JSON string. + */ + public function toString(array $config, bool $pretty = true): string + { + $flags = JSON_THROW_ON_ERROR; + $flags += $pretty ? (JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) : 0; + + return json_encode($config, $flags); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['json']; + } +} diff --git a/src/Writer/Properties.php b/src/Writer/Properties.php new file mode 100644 index 0000000..be4449a --- /dev/null +++ b/src/Writer/Properties.php @@ -0,0 +1,62 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Properties extends AbstractWriter +{ + /** + * {@inheritdoc} + * Writes an array to a Properties string. + */ + public function toString(array $config, bool $pretty = true): string + { + return $this->toProperties($config); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['properties']; + } + + /** + * Converts array to Properties string. + * + * @param array $arr Array to be converted + * + * @return string Converted array as Properties + */ + protected function toProperties(array $arr): string + { + $converted = ''; + + foreach ($arr as $key => $value) { + if (is_array($value)) { + continue; + } + + // Escape all space, ; and = characters in the key: + $key = addcslashes($key, ' :='); + + // Escape all backslashes and newlines in the value: + $value = preg_replace('/([\r\n\t\f\v\\\])/', '\\\$1', $value); + + $converted .= $key . ' = ' . $value . PHP_EOL; + } + + return $converted; + } +} diff --git a/src/Writer/Serialize.php b/src/Writer/Serialize.php new file mode 100644 index 0000000..c86c649 --- /dev/null +++ b/src/Writer/Serialize.php @@ -0,0 +1,28 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +interface WriterInterface +{ + /** + * Writes a configuration from `$config` to `$filename`. + * + * @param array $config + * @param string $filename + * + * @throws WriteException if the data could not be written to the file + * + * @return string + */ + public function toFile(array $config, string $filename): string; + + /** + * Writes a configuration from `$config` to a string. + * + * @param array $config + * @param bool $pretty + * + * @return string + */ + public function toString(array $config, bool $pretty = true): string; + + /** + * Returns an array of allowed file extensions for this writer. + * + * @return array + */ + public static function getSupportedExtensions(): array; +} diff --git a/src/Writer/Xml.php b/src/Writer/Xml.php new file mode 100644 index 0000000..a4d4817 --- /dev/null +++ b/src/Writer/Xml.php @@ -0,0 +1,75 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Xml extends AbstractWriter +{ + /** + * {@inheritdoc} + * Writes an array to a Xml string. + */ + public function toString(array $config, bool $pretty = true): string + { + $xml = $this->toXML($config); + if ($pretty === false) { + return $xml; + } + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml); + + return $dom->saveXML(); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['xml']; + } + + /** + * Converts array to XML string. + * + * @param array $arr Array to be converted + * @param string $rootElement I specified will be taken as root element + * @param SimpleXMLElement|null $xml If specified content will be appended + * + * @return string Converted array as XML + * + * @throws \Exception + * @see https://www.kerstner.at/2011/12/php-array-to-xml-conversion/ + */ + protected function toXML(array $arr, string $rootElement = '', ?SimpleXMLElement $xml = null): string + { + if ($xml === null) { + $xml = new SimpleXMLElement($rootElement); + } + foreach ($arr as $k => $v) { + if (is_array($v)) { + $this->toXML($v, $k, $xml->addChild($k)); + } else { + $xml->addChild($k, $v); + } + } + + return $xml->asXML(); + } +} diff --git a/src/Writer/Yaml.php b/src/Writer/Yaml.php new file mode 100644 index 0000000..ca2283f --- /dev/null +++ b/src/Writer/Yaml.php @@ -0,0 +1,36 @@ + + * @author Hassan Khan + * @author Filip Š + * @author Mark de Groot + * @link https://github.com/noodlehaus/config + * @license MIT + */ +class Yaml extends AbstractWriter +{ + /** + * {@inheritdoc} + * Writes an array to a Yaml string. + */ + public function toString(array $config, bool $pretty = true): string + { + return YamlParser::dump($config); + } + + /** + * {@inheritdoc} + */ + public static function getSupportedExtensions(): array + { + return ['yaml']; + } +}