From 1a420ec08d6b0d205fdc171bfd05623dca0e1fc0 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 ++ .travis.yml | 35 + CONTRIBUTING.md | 229 +++++++ LICENSE.txt | 27 + README.md | 10 + composer.json | 45 ++ phpunit.xml.dist | 42 ++ phpunit.xml.travis | 45 ++ src/Config.php | 404 +++++++++++ src/Exception/ExceptionInterface.php | 19 + src/Exception/InvalidArgumentException.php | 19 + src/Exception/RuntimeException.php | 19 + src/Factory.php | 155 +++++ src/Processor/Constant.php | 89 +++ src/Processor/Filter.php | 94 +++ src/Processor/ProcessorInterface.php | 37 + src/Processor/Queue.php | 58 ++ src/Processor/Token.php | 247 +++++++ src/Processor/Translator.php | 146 ++++ src/Reader/Ini.php | 207 ++++++ src/Reader/Json.php | 110 +++ src/Reader/ReaderInterface.php | 35 + src/Reader/Xml.php | 201 ++++++ src/Reader/Yaml.php | 164 +++++ src/ReaderPluginManager.php | 54 ++ src/Writer/AbstractWriter.php | 83 +++ src/Writer/Ini.php | 189 ++++++ src/Writer/Json.php | 32 + src/Writer/PhpArray.php | 33 + src/Writer/WriterInterface.php | 37 + src/Writer/Xml.php | 97 +++ src/Writer/Yaml.php | 91 +++ test/ConfigTest.php | 635 ++++++++++++++++++ test/FactoryTest.php | 143 ++++ test/ProcessorTest.php | 495 ++++++++++++++ test/Reader/AbstractReaderTestCase.php | 56 ++ test/Reader/IniTest.php | 103 +++ test/Reader/JsonTest.php | 70 ++ test/Reader/TestAssets/DummyReader.php | 35 + test/Reader/TestAssets/Ini/include-base.ini | 1 + test/Reader/TestAssets/Ini/include-target.ini | 1 + test/Reader/TestAssets/Ini/invalid.ini | 2 + test/Reader/TestAssets/Json/include-base.json | 3 + .../TestAssets/Json/include-base_nested.json | 3 + .../TestAssets/Json/include-target.json | 3 + test/Reader/TestAssets/Json/invalid.json | 3 + test/Reader/TestAssets/Xml/array.xml | 31 + test/Reader/TestAssets/Xml/include-base.xml | 4 + test/Reader/TestAssets/Xml/include-target.xml | 2 + test/Reader/TestAssets/Xml/invalid.xml | 4 + test/Reader/TestAssets/Yaml/include-base.yaml | 1 + .../TestAssets/Yaml/include-target.yaml | 1 + test/Reader/XmlTest.php | 98 +++ test/Reader/YamlTest.php | 91 +++ test/TestAssets/Ini/include-base.ini | 2 + test/TestAssets/Ini/include-base2.ini | 2 + test/TestAssets/Php/include-base.php | 6 + test/TestAssets/Php/include-base2.php | 6 + test/TestAssets/Php/include-base3.php | 6 + test/TestAssets/Xml/include-base.xml | 6 + test/TestAssets/Xml/include-base2.xml | 6 + test/TestAssets/bad.ext | 0 test/TestAssets/dummy.dum | 1 + test/Writer/AbstractWriterTestCase.php | 93 +++ test/Writer/IniTest.php | 53 ++ test/Writer/JsonTest.php | 54 ++ test/Writer/PhpArrayTest.php | 55 ++ test/Writer/TestAssets/PhpReader.php | 19 + test/Writer/XmlTest.php | 87 +++ test/Writer/YamlTest.php | 73 ++ test/Writer/_files/allsections.ini | 26 + test/Writer/_files/allsections.json | 45 ++ test/Writer/_files/allsections.xml | 35 + test/Writer/_files/allsections.yaml | 35 + test/_files/indentedcomments.yaml | 4 + test/_files/inlinecomments.yaml | 3 + test/_files/listbooleans.yaml | 51 ++ test/_files/listconstants.yaml | 4 + test/_files/translations-de_DE.php | 5 + test/bootstrap.php | 34 + 83 files changed, 5615 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Config.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Factory.php create mode 100644 src/Processor/Constant.php create mode 100644 src/Processor/Filter.php create mode 100644 src/Processor/ProcessorInterface.php create mode 100644 src/Processor/Queue.php create mode 100644 src/Processor/Token.php create mode 100644 src/Processor/Translator.php create mode 100644 src/Reader/Ini.php create mode 100644 src/Reader/Json.php create mode 100644 src/Reader/ReaderInterface.php create mode 100644 src/Reader/Xml.php create mode 100644 src/Reader/Yaml.php create mode 100644 src/ReaderPluginManager.php create mode 100644 src/Writer/AbstractWriter.php create mode 100644 src/Writer/Ini.php create mode 100644 src/Writer/Json.php create mode 100644 src/Writer/PhpArray.php create mode 100644 src/Writer/WriterInterface.php create mode 100644 src/Writer/Xml.php create mode 100644 src/Writer/Yaml.php create mode 100644 test/ConfigTest.php create mode 100644 test/FactoryTest.php create mode 100644 test/ProcessorTest.php create mode 100644 test/Reader/AbstractReaderTestCase.php create mode 100644 test/Reader/IniTest.php create mode 100644 test/Reader/JsonTest.php create mode 100644 test/Reader/TestAssets/DummyReader.php create mode 100644 test/Reader/TestAssets/Ini/include-base.ini create mode 100644 test/Reader/TestAssets/Ini/include-target.ini create mode 100644 test/Reader/TestAssets/Ini/invalid.ini create mode 100644 test/Reader/TestAssets/Json/include-base.json create mode 100644 test/Reader/TestAssets/Json/include-base_nested.json create mode 100644 test/Reader/TestAssets/Json/include-target.json create mode 100644 test/Reader/TestAssets/Json/invalid.json create mode 100644 test/Reader/TestAssets/Xml/array.xml create mode 100644 test/Reader/TestAssets/Xml/include-base.xml create mode 100644 test/Reader/TestAssets/Xml/include-target.xml create mode 100644 test/Reader/TestAssets/Xml/invalid.xml create mode 100644 test/Reader/TestAssets/Yaml/include-base.yaml create mode 100644 test/Reader/TestAssets/Yaml/include-target.yaml create mode 100644 test/Reader/XmlTest.php create mode 100644 test/Reader/YamlTest.php create mode 100644 test/TestAssets/Ini/include-base.ini create mode 100644 test/TestAssets/Ini/include-base2.ini create mode 100644 test/TestAssets/Php/include-base.php create mode 100644 test/TestAssets/Php/include-base2.php create mode 100644 test/TestAssets/Php/include-base3.php create mode 100644 test/TestAssets/Xml/include-base.xml create mode 100644 test/TestAssets/Xml/include-base2.xml create mode 100644 test/TestAssets/bad.ext create mode 100644 test/TestAssets/dummy.dum create mode 100644 test/Writer/AbstractWriterTestCase.php create mode 100644 test/Writer/IniTest.php create mode 100644 test/Writer/JsonTest.php create mode 100644 test/Writer/PhpArrayTest.php create mode 100644 test/Writer/TestAssets/PhpReader.php create mode 100644 test/Writer/XmlTest.php create mode 100644 test/Writer/YamlTest.php create mode 100644 test/Writer/_files/allsections.ini create mode 100644 test/Writer/_files/allsections.json create mode 100644 test/Writer/_files/allsections.xml create mode 100644 test/Writer/_files/allsections.yaml create mode 100644 test/_files/indentedcomments.yaml create mode 100644 test/_files/inlinecomments.yaml create mode 100644 test/_files/listbooleans.yaml create mode 100644 test/_files/listconstants.yaml create mode 100644 test/_files/translations-de_DE.php create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..53bda82 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..85dc9a8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cac0a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..bf4b799 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe909ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3682dfe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-config/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-config.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-config) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-config.git + $ cd zend-config + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-config.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-config.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6eab5aa --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3ff252 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# zend-config + +`Zend\Config` is designed to simplify access to configuration data within +applications. It provides a nested object property-based user interface for +accessing this configuration data within application code. The configuration +data may come from a variety of media supporting hierarchical data storage. + + +- File issues at https://github.com/zendframework/zend-code/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-code diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..29d2be1 --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "zendframework/zend-config", + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "config" + ], + "homepage": "https://github.com/zendframework/zend-config", + "autoload": { + "psr-4": { + "Zend\\Config": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-filter": "self.version", + "zendframework/zend-i18n": "self.version", + "zendframework/zend-json": "self.version", + "zendframework/zend-servicemanager": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-json": "Zend\\Json to use the Json reader or writer classes", + "zendframework/zend-servicemanager": "Zend\\ServiceManager for use with the Config Factory to retrieve reader and writer instances" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Config\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d6be5e8 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,42 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 0000000..cdfc92d --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,45 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + + diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..11ec759 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,404 @@ +allowModifications = (boolean) $allowModifications; + + foreach ($array as $key => $value) { + if (is_array($value)) { + $this->data[$key] = new self($value, $this->allowModifications); + } else { + $this->data[$key] = $value; + } + + $this->count++; + } + } + + /** + * Retrieve a value and return $default if there is no element set. + * + * @param string $name + * @param mixed $default + * @return mixed + */ + public function get($name, $default = null) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + return $default; + } + + /** + * Magic function so that $obj->value will work. + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Set a value in the config. + * + * Only allow setting of a property if $allowModifications was set to true + * on construction. Otherwise, throw an exception. + * + * @param string $name + * @param mixed $value + * @return void + * @throws Exception\RuntimeException + */ + public function __set($name, $value) + { + if ($this->allowModifications) { + + if (is_array($value)) { + $value = new self($value, true); + } + + if (null === $name) { + $this->data[] = $value; + } else { + $this->data[$name] = $value; + } + + $this->count++; + } else { + throw new Exception\RuntimeException('Config is read only'); + } + } + + /** + * Deep clone of this instance to ensure that nested Zend\Configs are also + * cloned. + * + * @return void + */ + public function __clone() + { + $array = array(); + + foreach ($this->data as $key => $value) { + if ($value instanceof self) { + $array[$key] = clone $value; + } else { + $array[$key] = $value; + } + } + + $this->data = $array; + } + + /** + * Return an associative array of the stored data. + * + * @return array + */ + public function toArray() + { + $array = array(); + $data = $this->data; + + /** @var self $value */ + foreach ($data as $key => $value) { + if ($value instanceof self) { + $array[$key] = $value->toArray(); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * isset() overloading + * + * @param string $name + * @return boolean + */ + public function __isset($name) + { + return isset($this->data[$name]); + } + + /** + * unset() overloading + * + * @param string $name + * @return void + * @throws Exception\InvalidArgumentException + */ + public function __unset($name) + { + if (!$this->allowModifications) { + throw new Exception\InvalidArgumentException('Config is read only'); + } elseif (isset($this->data[$name])) { + unset($this->data[$name]); + $this->count--; + $this->skipNextIteration = true; + } + } + + /** + * count(): defined by Countable interface. + * + * @see Countable::count() + * @return integer + */ + public function count() + { + return $this->count; + } + + /** + * current(): defined by Iterator interface. + * + * @see Iterator::current() + * @return mixed + */ + public function current() + { + $this->skipNextIteration = false; + return current($this->data); + } + + /** + * key(): defined by Iterator interface. + * + * @see Iterator::key() + * @return mixed + */ + public function key() + { + return key($this->data); + } + + /** + * next(): defined by Iterator interface. + * + * @see Iterator::next() + * @return void + */ + public function next() + { + if ($this->skipNextIteration) { + $this->skipNextIteration = false; + return; + } + + next($this->data); + } + + /** + * rewind(): defined by Iterator interface. + * + * @see Iterator::rewind() + * @return void + */ + public function rewind() + { + $this->skipNextIteration = false; + reset($this->data); + } + + /** + * valid(): defined by Iterator interface. + * + * @see Iterator::valid() + * @return boolean + */ + public function valid() + { + return ($this->key() !== null); + } + + /** + * offsetExists(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetExists() + * @param mixed $offset + * @return boolean + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + /** + * offsetGet(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetGet() + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->__get($offset); + } + + /** + * offsetSet(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetSet() + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset, $value) + { + $this->__set($offset, $value); + } + + /** + * offsetUnset(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetUnset() + * @param mixed $offset + * @return void + */ + public function offsetUnset($offset) + { + $this->__unset($offset); + } + + /** + * Merge another Config with this one. + * + * For duplicate keys, the following will be performed: + * - Nested Configs will be recursively merged. + * - Items in $merge with INTEGER keys will be appended. + * - Items in $merge with STRING keys will overwrite current values. + * + * @param Config $merge + * @return Config + */ + public function merge(self $merge) + { + /** @var Config $value */ + foreach ($merge as $key => $value) { + if (array_key_exists($key, $this->data)) { + if (is_int($key)) { + $this->data[] = $value; + } elseif ($value instanceof self && $this->data[$key] instanceof self) { + $this->data[$key]->merge($value); + } else { + if ($value instanceof self) { + $this->data[$key] = new self($value->toArray(), $this->allowModifications); + } else { + $this->data[$key] = $value; + } + } + } else { + if ($value instanceof self) { + $this->data[$key] = new self($value->toArray(), $this->allowModifications); + } else { + $this->data[$key] = $value; + } + } + } + + return $this; + } + + /** + * Prevent any more modifications being made to this instance. + * + * Useful after merge() has been used to merge multiple Config objects + * into one object which should then not be modified again. + * + * @return void + */ + public function setReadOnly() + { + $this->allowModifications = false; + + /** @var Config $value */ + foreach ($this->data as $value) { + if ($value instanceof self) { + $value->setReadOnly(); + } + } + } + + /** + * Returns whether this Config object is read only or not. + * + * @return boolean + */ + public function isReadOnly() + { + return !$this->allowModifications; + } +} diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..81b0132 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ + 'ini', + 'json' => 'json', + 'xml' => 'xml', + 'yaml' => 'yaml', + ); + + + /** + * Read a config from a file. + * + * @param string $filename + * @param boolean $returnConfigObject + * @return array|Config + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public static function fromFile($filename, $returnConfigObject = false) + { + $pathinfo = pathinfo($filename); + + if (!isset($pathinfo['extension'])) { + throw new Exception\RuntimeException(sprintf( + 'Filename "%s" is missing an extension and cannot be auto-detected', + $filename + )); + } + + $extension = strtolower($pathinfo['extension']); + + if ($extension === 'php') { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + $config = include $filename; + } elseif (isset(self::$extensions[$extension])) { + $reader = self::$extensions[$extension]; + if (!$reader instanceof Reader\ReaderInterface) { + $reader = self::getReaderPluginManager()->get($reader); + self::$extensions[$extension] = $reader; + } + + /** @var Reader\ReaderInterface $reader */ + $config = $reader->fromFile($filename); + } else { + throw new Exception\RuntimeException(sprintf( + 'Unsupported config file extension: .%s', + $pathinfo['extension'] + )); + } + + return ($returnConfigObject) ? new Config($config) : $config; + } + + /** + * Read configuration from multiple files and merge them. + * + * @param array $files + * @param boolean $returnConfigObject + * @return array|Config + */ + public static function fromFiles(array $files, $returnConfigObject = false) + { + $config = array(); + + foreach ($files as $file) { + $config = ArrayUtils::merge($config, self::fromFile($file)); + } + + return ($returnConfigObject) ? new Config($config) : $config; + } + + /** + * Set reader plugin manager + * + * @param ReaderPluginManager $readers + */ + public static function setReaderPluginManager(ReaderPluginManager $readers) + { + self::$readers = $readers; + } + + /** + * Get the reader plugin manager + * + * @return ReaderPluginManager + */ + public static function getReaderPluginManager() + { + if (static::$readers === null) { + static::$readers = new ReaderPluginManager(); + } + return static::$readers; + } + + /** + * Set config reader for file extension + * + * @param string $extension + * @param string|Reader\ReaderInterface $reader + * @throws Exception\InvalidArgumentException + */ + public static function registerReader($extension, $reader) + { + $extension = strtolower($extension); + + if (!is_string($reader) && !$reader instanceof Reader\ReaderInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Reader should be plugin name, class name or ' . + 'instance of %s\Reader\ReaderInterface; received "%s"', + __NAMESPACE__, + (is_object($reader) ? get_class($reader) : gettype($reader)) + )); + } + + self::$extensions[$extension] = $reader; + } +} diff --git a/src/Processor/Constant.php b/src/Processor/Constant.php new file mode 100644 index 0000000..9fbae8d --- /dev/null +++ b/src/Processor/Constant.php @@ -0,0 +1,89 @@ +setUserOnly($userOnly); + $this->setPrefix($prefix); + $this->setSuffix($suffix); + + $this->loadConstants(); + } + + /** + * @return bool + */ + public function getUserOnly() + { + return $this->userOnly; + } + + /** + * Should we use only user-defined constants? + * + * @param bool $userOnly + * @return Constant + */ + public function setUserOnly($userOnly) + { + $this->userOnly = (bool) $userOnly; + return $this; + } + + /** + * Load all currently defined constants into parser. + * + * @return void + */ + public function loadConstants() + { + if ($this->userOnly) { + $constants = get_defined_constants(true); + $constants = isset($constants['user']) ? $constants['user'] : array(); + $this->setTokens($constants); + } else { + $this->setTokens(get_defined_constants()); + } + } + + /** + * Get current token registry. + * @return array + */ + public function getTokens() + { + return $this->tokens; + } +} diff --git a/src/Processor/Filter.php b/src/Processor/Filter.php new file mode 100644 index 0000000..1ed0f31 --- /dev/null +++ b/src/Processor/Filter.php @@ -0,0 +1,94 @@ +setFilter($filter); + } + + /** + * @param ZendFilter $filter + * @return Filter + */ + public function setFilter(ZendFilter $filter) + { + $this->filter = $filter; + return $this; + } + + /** + * @return ZendFilter + */ + public function getFilter() + { + return $this->filter; + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws Exception\InvalidArgumentException + */ + public function process(Config $config) + { + if ($config->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + /** + * Walk through config and replace values + */ + foreach ($config as $key => $val) { + if ($val instanceof Config) { + $this->process($val); + } else { + $config->$key = $this->filter->filter($val); + } + } + + return $config; + } + + /** + * Process a single value + * + * @param mixed $value + * @return mixed + */ + public function processValue($value) + { + return $this->filter->filter($value); + } +} diff --git a/src/Processor/ProcessorInterface.php b/src/Processor/ProcessorInterface.php new file mode 100644 index 0000000..520a18b --- /dev/null +++ b/src/Processor/ProcessorInterface.php @@ -0,0 +1,37 @@ +isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + foreach ($this as $parser) { + /** @var $parser ProcessorInterface */ + $parser->process($config); + } + } + + /** + * Process a single value + * + * @param mixed $value + * @return mixed + */ + public function processValue($value) + { + foreach ($this as $parser) { + /** @var $parser ProcessorInterface */ + $value = $parser->processValue($value); + } + + return $value; + } +} diff --git a/src/Processor/Token.php b/src/Processor/Token.php new file mode 100644 index 0000000..0ba34a7 --- /dev/null +++ b/src/Processor/Token.php @@ -0,0 +1,247 @@ + value + * to replace it with + * @param string $prefix + * @param string $suffix + * @internal param array $options + * @return Token + */ + public function __construct($tokens = array(), $prefix = '', $suffix = '') + { + $this->setTokens($tokens); + $this->setPrefix($prefix); + $this->setSuffix($suffix); + } + + /** + * @param string $prefix + * @return Token + */ + public function setPrefix($prefix) + { + // reset map + $this->map = null; + $this->prefix = $prefix; + return $this; + } + + /** + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * @param string $suffix + * @return Token + */ + public function setSuffix($suffix) + { + // reset map + $this->map = null; + $this->suffix = $suffix; + + return $this; + } + + /** + * @return string + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * Set token registry. + * + * @param array|Config|Traversable $tokens Associative array of TOKEN => value + * to replace it with + * @return Token + * @throws Exception\InvalidArgumentException + */ + public function setTokens($tokens) + { + if (is_array($tokens)) { + $this->tokens = $tokens; + } elseif ($tokens instanceof Config) { + $this->tokens = $tokens->toArray(); + } elseif ($tokens instanceof Traversable) { + $this->tokens = array(); + foreach ($tokens as $key => $val) { + $this->tokens[$key] = $val; + } + } else { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($tokens) . ' as token registry.'); + } + + // reset map + $this->map = null; + + return $this; + } + + /** + * Get current token registry. + * + * @return array + */ + public function getTokens() + { + return $this->tokens; + } + + /** + * Add new token. + * + * @param string $token + * @param mixed $value + * @return Token + * @throws Exception\InvalidArgumentException + */ + public function addToken($token, $value) + { + if (!is_scalar($token)) { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($token) . ' as token name.'); + } + $this->tokens[$token] = $value; + + // reset map + $this->map = null; + + return $this; + } + + /** + * Add new token. + * + * @param string $token + * @param mixed $value + * @return Token + */ + public function setToken($token, $value) + { + return $this->addToken($token, $value); + } + + /** + * Build replacement map + */ + protected function buildMap() + { + if (!$this->suffix && !$this->prefix) { + $this->map = $this->tokens; + } else { + $this->map = array(); + foreach ($this->tokens as $token => $value) { + $this->map[$this->prefix . $token . $this->suffix] = $value; + } + } + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws InvalidArgumentException + */ + public function process(Config $config) + { + if ($config->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + if ($this->map === null) { + $this->buildMap(); + } + + /** + * Walk through config and replace values + */ + $keys = array_keys($this->map); + $values = array_values($this->map); + foreach ($config as $key => $val) { + if ($val instanceof Config) { + $this->process($val); + } else { + $config->$key = str_replace($keys, $values, $val); + } + } + + return $config; + } + + /** + * Process a single value + * + * @param $value + * @return mixed + */ + public function processValue($value) + { + if ($this->map === null) { + $this->buildMap(); + } + $keys = array_keys($this->map); + $values = array_values($this->map); + + return str_replace($keys, $values, $value); + } +} diff --git a/src/Processor/Translator.php b/src/Processor/Translator.php new file mode 100644 index 0000000..bb2c623 --- /dev/null +++ b/src/Processor/Translator.php @@ -0,0 +1,146 @@ +setTranslator($translator); + $this->setTextDomain($textDomain); + $this->setLocale($locale); + } + + /** + * @param ZendTranslator $translator + * @return Translator + */ + public function setTranslator(ZendTranslator $translator) + { + $this->translator = $translator; + return $this; + } + + /** + * @return ZendTranslator + */ + public function getTranslator() + { + return $this->translator; + } + + /** + * @param string|null $locale + * @return Translator + */ + public function setLocale($locale) + { + $this->locale = $locale; + return $this; + } + + /** + * @return string|null + */ + public function getLocale() + { + return $this->locale; + } + + /** + * @param string $textDomain + * @return Translator + */ + public function setTextDomain($textDomain) + { + $this->textDomain = $textDomain; + return $this; + } + + /** + * @return string + */ + public function getTextDomain() + { + return $this->textDomain; + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws Exception\InvalidArgumentException + */ + public function process(Config $config) + { + if ($config->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + /** + * Walk through config and replace values + */ + foreach ($config as $key => $val) { + if ($val instanceof Config) { + $this->process($val); + } else { + $config->{$key} = $this->translator->translate($val, $this->textDomain, $this->locale); + } + } + + return $config; + } + + /** + * Process a single value + * + * @param $value + * @return mixed + */ + public function processValue($value) + { + return $this->translator->translate($value, $this->textDomain, $this->locale); + } + +} diff --git a/src/Reader/Ini.php b/src/Reader/Ini.php new file mode 100644 index 0000000..fa431f6 --- /dev/null +++ b/src/Reader/Ini.php @@ -0,0 +1,207 @@ +nestSeparator = $separator; + return $this; + } + + /** + * Get nest separator. + * + * @return string + */ + public function getNestSeparator() + { + return $this->nestSeparator; + } + + /** + * fromFile(): defined by Reader interface. + * + * @see ReaderInterface::fromFile() + * @param string $filename + * @return array + * @throws Exception\InvalidArgumentException + */ + public function fromFile($filename) + { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + $this->directory = dirname($filename); + + set_error_handler( + function($error, $message = '', $file = '', $line = 0) use ($filename) { + throw new Exception\RuntimeException(sprintf( + 'Error reading INI file "%s": %s', + $filename, $message + ), $error); + }, E_WARNING + ); + $ini = parse_ini_file($filename, true); + restore_error_handler(); + + return $this->process($ini); + } + + /** + * fromString(): defined by Reader interface. + * + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + $this->directory = null; + + set_error_handler( + function($error, $message = '', $file = '', $line = 0) { + throw new Exception\RuntimeException(sprintf( + 'Error reading INI string: %s', + $message + ), $error); + }, E_WARNING + ); + $ini = parse_ini_string($string, true); + restore_error_handler(); + + return $this->process($ini); + } + + /** + * Process data from the parsed ini file. + * + * @param array $data + * @return array + */ + protected function process(array $data) + { + $config = array(); + + foreach ($data as $section => $value) { + if (is_array($value)) { + if (strpos($section, $this->nestSeparator) !== false) { + $section = explode($this->nestSeparator, $section, 2); + $config[$section[0]][$section[1]] = $this->processSection($value); + } else { + $config[$section] = $this->processSection($value); + } + } else { + $this->processKey($section, $value, $config); + } + } + + return $config; + } + + /** + * Process a section. + * + * @param array $section + * @return array + */ + protected function processSection(array $section) + { + $config = array(); + + foreach ($section as $key => $value) { + $this->processKey($key, $value, $config); + } + + return $config; + } + + /** + * Process a key. + * + * @param string $key + * @param string $value + * @param array $config + * @return array + * @throws Exception\RuntimeException + */ + protected function processKey($key, $value, array &$config) + { + if (strpos($key, $this->nestSeparator) !== false) { + $pieces = explode($this->nestSeparator, $key, 2); + + if (!strlen($pieces[0]) || !strlen($pieces[1])) { + throw new Exception\RuntimeException(sprintf('Invalid key "%s"', $key)); + } elseif (!isset($config[$pieces[0]])) { + if ($pieces[0] === '0' && !empty($config)) { + $config = array($pieces[0] => $config); + } else { + $config[$pieces[0]] = array(); + } + } elseif (!is_array($config[$pieces[0]])) { + throw new Exception\RuntimeException(sprintf( + 'Cannot create sub-key for "%s", as key already exists', $pieces[0] + )); + } + + $this->processKey($pieces[1], $value, $config[$pieces[0]]); + } else { + if ($key === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a string config'); + } + + $reader = clone $this; + $include = $reader->fromFile($this->directory . '/' . $value); + $config = array_replace_recursive($config, $include); + } else { + $config[$key] = $value; + } + } + } +} diff --git a/src/Reader/Json.php b/src/Reader/Json.php new file mode 100644 index 0000000..5d6b7bd --- /dev/null +++ b/src/Reader/Json.php @@ -0,0 +1,110 @@ +directory = dirname($filename); + + try { + $config = JsonFormat::decode(file_get_contents($filename), JsonFormat::TYPE_ARRAY); + } catch (JsonException\RuntimeException $e) { + throw new Exception\RuntimeException($e->getMessage()); + } + + return $this->process($config); + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + + $this->directory = null; + + try { + $config = JsonFormat::decode($string, JsonFormat::TYPE_ARRAY); + } catch (JsonException\RuntimeException $e) { + throw new Exception\RuntimeException($e->getMessage()); + } + + return $this->process($config); + } + + /** + * Process the array for @include + * + * @param array $data + * @return array + * @throws Exception\RuntimeException + */ + protected function process(array $data) + { + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->process($value); + } + if (trim($key) === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a JSON string'); + } + $reader = clone $this; + unset($data[$key]); + $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); + } + } + return $data; + } +} diff --git a/src/Reader/ReaderInterface.php b/src/Reader/ReaderInterface.php new file mode 100644 index 0000000..4ecaf8d --- /dev/null +++ b/src/Reader/ReaderInterface.php @@ -0,0 +1,35 @@ +reader = new XMLReader(); + $this->reader->open($filename, null, LIBXML_XINCLUDE); + + $this->directory = dirname($filename); + + set_error_handler( + function($error, $message = '', $file = '', $line = 0) use ($filename) { + throw new Exception\RuntimeException(sprintf( + 'Error reading XML file "%s": %s', + $filename, $message + ), $error); + }, E_WARNING + ); + $return = $this->process(); + restore_error_handler(); + + return $return; + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + $this->reader = new XMLReader(); + + $this->reader->xml($string, null, LIBXML_XINCLUDE); + + $this->directory = null; + + set_error_handler( + function($error, $message = '', $file = '', $line = 0) { + throw new Exception\RuntimeException(sprintf( + 'Error reading XML string: %s', + $message + ), $error); + }, E_WARNING + ); + $return = $this->process(); + restore_error_handler(); + + return $return; + } + + /** + * Process data from the created XMLReader. + * + * @return array + */ + protected function process() + { + return $this->processNextElement(); + } + + /** + * Process the next inner element. + * + * @return mixed + */ + protected function processNextElement() + { + $children = array(); + $text = ''; + + while ($this->reader->read()) { + if ($this->reader->nodeType === XMLReader::ELEMENT) { + if ($this->reader->depth === 0) { + return $this->processNextElement(); + } + + $attributes = $this->getAttributes(); + $name = $this->reader->name; + + if ($this->reader->isEmptyElement) { + $child = array(); + } else { + $child = $this->processNextElement(); + } + + if ($attributes) { + if (!is_array($child)) { + $child = array(); + } + + $child = array_merge($child, $attributes); + } + + if (isset($children[$name])) { + if (!is_array($children[$name]) || !array_key_exists(0, $children[$name])) { + $children[$name] = array($children[$name]); + } + + $children[$name][] = $child; + } else { + $children[$name] = $child; + } + } elseif ($this->reader->nodeType === XMLReader::END_ELEMENT) { + break; + } elseif (in_array($this->reader->nodeType, $this->textNodes)) { + $text .= $this->reader->value; + } + } + + return $children ?: $text; + } + + /** + * Get all attributes on the current node. + * + * @return array + */ + protected function getAttributes() + { + $attributes = array(); + + if ($this->reader->hasAttributes) { + while ($this->reader->moveToNextAttribute()) { + $attributes[$this->reader->localName] = $this->reader->value; + } + + $this->reader->moveToElement(); + } + + return $attributes; + } +} diff --git a/src/Reader/Yaml.php b/src/Reader/Yaml.php new file mode 100644 index 0000000..c33ef29 --- /dev/null +++ b/src/Reader/Yaml.php @@ -0,0 +1,164 @@ +setYamlDecoder($yamlDecoder); + } else { + if (function_exists('yaml_parse')) { + $this->setYamlDecoder('yaml_parse'); + } + } + } + + /** + * Set callback for decoding YAML + * + * @param string|callable $yamlDecoder the decoder to set + * @return Yaml + * @throws Exception\InvalidArgumentException + */ + public function setYamlDecoder($yamlDecoder) + { + if (!is_callable($yamlDecoder)) { + throw new Exception\RuntimeException( + 'Invalid parameter to setYamlDecoder() - must be callable' + ); + } + $this->yamlDecoder = $yamlDecoder; + return $this; + } + + /** + * Get callback for decoding YAML + * + * @return callable + */ + public function getYamlDecoder() + { + return $this->yamlDecoder; + } + + /** + * fromFile(): defined by Reader interface. + * + * @see ReaderInterface::fromFile() + * @param string $filename + * @return array + * @throws Exception\RuntimeException + */ + public function fromFile($filename) + { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + if (null === $this->getYamlDecoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); + } + + $this->directory = dirname($filename); + + $config = call_user_func($this->getYamlDecoder(), file_get_contents($filename)); + if (null === $config) { + throw new Exception\RuntimeException("Error parsing YAML data"); + } + + return $this->process($config); + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (null === $this->getYamlDecoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); + } + if (empty($string)) { + return array(); + } + + $this->directory = null; + + $config = call_user_func($this->getYamlDecoder(), $string); + if (null === $config) { + throw new Exception\RuntimeException("Error parsing YAML data"); + } + + return $this->process($config); + } + + /** + * Process the array for @include + * + * @param array $data + * @return array + * @throws Exception\RuntimeException + */ + protected function process(array $data) + { + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->process($value); + } + if (trim($key) === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a json string'); + } + $reader = clone $this; + unset($data[$key]); + $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); + } + } + return $data; + } +} diff --git a/src/ReaderPluginManager.php b/src/ReaderPluginManager.php new file mode 100644 index 0000000..2614fd0 --- /dev/null +++ b/src/ReaderPluginManager.php @@ -0,0 +1,54 @@ + 'Zend\Config\Reader\Ini', + 'json' => 'Zend\Config\Reader\Json', + 'xml' => 'Zend\Config\Reader\Xml', + 'yaml' => 'Zend\Config\Reader\Yaml', + ); + + /** + * Validate the plugin + * Checks that the reader loaded is an instance of Reader\ReaderInterface. + * + * @param Reader\ReaderInterface $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Reader\ReaderInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Reader\ReaderInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Writer/AbstractWriter.php b/src/Writer/AbstractWriter.php new file mode 100644 index 0000000..4726075 --- /dev/null +++ b/src/Writer/AbstractWriter.php @@ -0,0 +1,83 @@ +toString($config), $flags); + restore_error_handler(); + } + + /** + * toString(): defined by Writer interface. + * + * @see WriterInterface::toString() + * @param mixed $config + * @return string + * @throws Exception\InvalidArgumentException + */ + public function toString($config) + { + if ($config instanceof Traversable) { + $config = ArrayUtils::iteratorToArray($config); + } elseif (!is_array($config)) { + throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable config'); + } + + return $this->processConfig($config); + } + + /** + * @param array $config + * @return string + */ + abstract protected function processConfig(array $config); +} diff --git a/src/Writer/Ini.php b/src/Writer/Ini.php new file mode 100644 index 0000000..b641d62 --- /dev/null +++ b/src/Writer/Ini.php @@ -0,0 +1,189 @@ +nestSeparator = $separator; + return $this; + } + + /** + * Get nest separator. + * + * @return string + */ + public function getNestSeparator() + { + return $this->nestSeparator; + } + + /** + * Set if rendering should occur without sections or not. + * + * If set to true, the INI file is rendered without sections completely + * into the global namespace of the INI file. + * + * @param bool $withoutSections + * @return Ini + */ + public function setRenderWithoutSectionsFlags($withoutSections) + { + $this->renderWithoutSections = (bool) $withoutSections; + return $this; + } + + /** + * Return whether the writer should render without sections. + * + * @return boolean + */ + public function shouldRenderWithoutSections() + { + return $this->renderWithoutSections; + } + + /** + * processConfig(): defined by AbstractWriter. + * + * @param array $config + * @return string + */ + public function processConfig(array $config) + { + $iniString = ''; + + if ($this->shouldRenderWithoutSections()) { + $iniString .= $this->addBranch($config); + } else { + $config = $this->sortRootElements($config); + + foreach ($config as $sectionName => $data) { + if (!is_array($data)) { + $iniString .= $sectionName + . ' = ' + . $this->prepareValue($data) + . "\n"; + } else { + $iniString .= '[' . $sectionName . ']' . "\n" + . $this->addBranch($data) + . "\n"; + } + } + } + + return $iniString; + } + + /** + * Add a branch to an INI string recursively. + * + * @param array $config + * @param array $parents + * @return string + */ + protected function addBranch(array $config, $parents = array()) + { + $iniString = ''; + + foreach ($config as $key => $value) { + $group = array_merge($parents, array($key)); + + if (is_array($value)) { + $iniString .= $this->addBranch($value, $group); + } else { + $iniString .= implode($this->nestSeparator, $group) + . ' = ' + . $this->prepareValue($value) + . "\n"; + } + } + + return $iniString; + } + + /** + * Prepare a value for INI. + * + * @param mixed $value + * @return string + * @throws Exception\RuntimeException + */ + protected function prepareValue($value) + { + if (is_integer($value) || is_float($value)) { + return $value; + } elseif (is_bool($value)) { + return ($value ? 'true' : 'false'); + } elseif (false === strpos($value, '"')) { + return '"' . $value . '"'; + } else { + throw new Exception\RuntimeException('Value can not contain double quotes'); + } + } + + /** + * Root elements that are not assigned to any section needs to be on the + * top of config. + * + * @param array $config + * @return array + */ + protected function sortRootElements(array $config) + { + $sections = array(); + + // Remove sections from config array. + foreach ($config as $key => $value) { + if (is_array($value)) { + $sections[$key] = $value; + unset($config[$key]); + } + } + + // Read sections to the end. + foreach ($sections as $key => $value) { + $config[$key] = $value; + } + + return $config; + } +} diff --git a/src/Writer/Json.php b/src/Writer/Json.php new file mode 100644 index 0000000..3d544ab --- /dev/null +++ b/src/Writer/Json.php @@ -0,0 +1,32 @@ +openMemory(); + $writer->setIndent(true); + $writer->setIndentString(str_repeat(' ', 4)); + + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('zend-config'); + + foreach ($config as $sectionName => $data) { + if (!is_array($data)) { + $writer->writeElement($sectionName, (string) $data); + } else { + $this->addBranch($sectionName, $data, $writer); + } + } + + $writer->endElement(); + $writer->endDocument(); + + return $writer->outputMemory(); + } + + /** + * Add a branch to an XML object recursively. + * + * @param string $branchName + * @param array $config + * @param XMLWriter $writer + * @return void + * @throws Exception\RuntimeException + */ + protected function addBranch($branchName, array $config, XMLWriter $writer) + { + $branchType = null; + + foreach ($config as $key => $value) { + if ($branchType === null) { + if (is_numeric($key)) { + $branchType = 'numeric'; + } else { + $writer->startElement($branchName); + $branchType = 'string'; + } + } elseif ($branchType !== (is_numeric($key) ? 'numeric' : 'string')) { + throw new Exception\RuntimeException('Mixing of string and numeric keys is not allowed'); + } + + if ($branchType === 'numeric') { + if (is_array($value)) { + $this->addBranch($value, $value, $writer); + } else { + $writer->writeElement($branchName, (string) $value); + } + } else { + if (is_array($value)) { + $this->addBranch($key, $value, $writer); + } else { + $writer->writeElement($key, (string) $value); + } + } + } + + if ($branchType === 'string') { + $writer->endElement(); + } + } +} diff --git a/src/Writer/Yaml.php b/src/Writer/Yaml.php new file mode 100644 index 0000000..9d4c309 --- /dev/null +++ b/src/Writer/Yaml.php @@ -0,0 +1,91 @@ +setYamlEncoder($yamlEncoder); + } else { + if (function_exists('yaml_emit')) { + $this->setYamlEncoder('yaml_emit'); + } + } + } + + /** + * Get callback for decoding YAML + * + * @return callable + */ + public function getYamlEncoder() + { + return $this->yamlEncoder; + } + + /** + * Set callback for decoding YAML + * + * @param callable $yamlEncoder the decoder to set + * @return Yaml + * @throws Exception\InvalidArgumentException + */ + public function setYamlEncoder($yamlEncoder) + { + if (!is_callable($yamlEncoder)) { + throw new Exception\InvalidArgumentException('Invalid parameter to setYamlEncoder() - must be callable'); + } + $this->yamlEncoder = $yamlEncoder; + return $this; + } + + /** + * processConfig(): defined by AbstractWriter. + * + * @param array $config + * @return string + * @throws Exception\RuntimeException + */ + public function processConfig(array $config) + { + if (null === $this->getYamlEncoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback encoder"); + } + + $config = call_user_func($this->getYamlEncoder(), $config); + if (null === $config) { + throw new Exception\RuntimeException("Error generating YAML data"); + } + + return $config; + } +} diff --git a/test/ConfigTest.php b/test/ConfigTest.php new file mode 100644 index 0000000..cdb2dfa --- /dev/null +++ b/test/ConfigTest.php @@ -0,0 +1,635 @@ +all = array( + 'hostname' => 'all', + 'name' => 'thisname', + 'db' => array( + 'host' => '127.0.0.1', + 'user' => 'username', + 'pass' => 'password', + 'name' => 'live' + ), + 'one' => array( + 'two' => array( + 'three' => 'multi' + ) + ) + ); + + $this->numericData = array( + 0 => 34, + 1 => 'test', + ); + + $this->menuData1 = array( + 'button' => array( + 'b0' => array( + 'L1' => 'button0-1', + 'L2' => 'button0-2', + 'L3' => 'button0-3' + ), + 'b1' => array( + 'L1' => 'button1-1', + 'L2' => 'button1-2' + ), + 'b2' => array( + 'L1' => 'button2-1' + ) + ) + ); + + $this->toCombineA = array( + 'foo' => 1, + 'bar' => 2, + 'text' => 'foo', + 'numerical' => array( + 'first', + 'second', + array( + 'third' + ) + ), + 'misaligned' => array( + 2 => 'foo', + 3 => 'bar' + ), + 'mixed' => array( + 'foo' => 'bar' + ), + 'replaceAssoc' => array( + 'foo' => 'bar' + ), + 'replaceNumerical' => array( + 'foo' + ) + ); + + $this->toCombineB = array( + 'foo' => 3, + 'text' => 'bar', + 'numerical' => array( + 'fourth', + 'fifth', + array( + 'sixth' + ) + ), + 'misaligned' => array( + 3 => 'baz' + ), + 'mixed' => array( + false + ), + 'replaceAssoc' => null, + 'replaceNumerical' => true + ); + + $this->leadingdot = array('.test' => 'dot-test'); + $this->invalidkey = array(' ' => 'test', ''=>'test2'); + + } + + public function testLoadSingleSection() + { + $config = new Config($this->all, false); + + $this->assertEquals('all', $config->hostname); + $this->assertEquals('live', $config->db->name); + $this->assertEquals('multi', $config->one->two->three); + $this->assertNull($config->nonexistent); // property doesn't exist + } + + public function testIsset() + { + $config = new Config($this->all, false); + + $this->assertFalse(isset($config->notarealkey)); + $this->assertTrue(isset($config->hostname)); // top level + $this->assertTrue(isset($config->db->name)); // one level down + } + + public function testModification() + { + $config = new Config($this->all, true); + + // overwrite an existing key + $this->assertEquals('thisname', $config->name); + $config->name = 'anothername'; + $this->assertEquals('anothername', $config->name); + + // overwrite an existing multi-level key + $this->assertEquals('multi', $config->one->two->three); + $config->one->two->three = 'anothername'; + $this->assertEquals('anothername', $config->one->two->three); + + // create a new multi-level key + $config->does = array('not'=> array('exist' => 'yet')); + $this->assertEquals('yet', $config->does->not->exist); + + } + + public function testNoModifications() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException', 'Config is read only'); + $config = new Config($this->all); + $config->hostname = 'test'; + } + + public function testNoNestedModifications() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException', 'Config is read only'); + $config = new Config($this->all); + $config->db->host = 'test'; + } + + public function testNumericKeys() + { + $data = new Config($this->numericData); + $this->assertEquals('test', $data->{1}); + $this->assertEquals(34, $data->{0}); + } + + public function testCount() + { + $data = new Config($this->menuData1); + $this->assertEquals(3, count($data->button)); + } + + public function testIterator() + { + // top level + $config = new Config($this->all); + $var = ''; + foreach ($config as $key=>$value) { + if (is_string($value)) { + $var .= "\nkey = $key, value = $value"; + } + } + $this->assertContains('key = name, value = thisname', $var); + + // 1 nest + $var = ''; + foreach ($config->db as $key=>$value) { + $var .= "\nkey = $key, value = $value"; + } + $this->assertContains('key = host, value = 127.0.0.1', $var); + + // 2 nests + $config = new Config($this->menuData1); + $var = ''; + foreach ($config->button->b1 as $key=>$value) { + $var .= "\nkey = $key, value = $value"; + } + $this->assertContains('key = L1, value = button1-1', $var); + } + + public function testArray() + { + $config = new Config($this->all); + + ob_start(); + print_r($config->toArray()); + $contents = ob_get_contents(); + ob_end_clean(); + + $this->assertContains('Array', $contents); + $this->assertContains('[hostname] => all', $contents); + $this->assertContains('[user] => username', $contents); + } + + public function testErrorWriteToReadOnly() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException', 'Config is read only'); + $config = new Config($this->all); + $config->test = '32'; + } + + public function testZF343() + { + $config_array = array( + 'controls' => array( + 'visible' => array( + 'name' => 'visible', + 'type' => 'checkbox', + 'attribs' => array(), // empty array + ), + ), + ); + $form_config = new Config($config_array, true); + $this->assertSame(array(), $form_config->controls->visible->attribs->toArray()); + } + + public function testZF402() + { + $configArray = array( + 'data1' => 'someValue', + 'data2' => 'someValue', + 'false1' => false, + 'data3' => 'someValue' + ); + $config = new Config($configArray); + $this->assertTrue(count($config) === count($configArray)); + $count = 0; + foreach ($config as $key => $value) { + if ($key === 'false1') { + $this->assertTrue($value === false); + } else { + $this->assertTrue($value === 'someValue'); + } + $count++; + } + $this->assertTrue($count === 4); + } + + public function testZf1019_HandlingInvalidKeyNames() + { + $config = new Config($this->leadingdot); + $array = $config->toArray(); + $this->assertContains('dot-test', $array['.test']); + } + + public function testZF1019_EmptyKeys() + { + $config = new Config($this->invalidkey); + $array = $config->toArray(); + $this->assertContains('test', $array[' ']); + $this->assertContains('test', $array['']); + } + + public function testZF1417_DefaultValues() + { + $config = new Config($this->all); + $value = $config->get('notthere', 'default'); + $this->assertTrue($value === 'default'); + $this->assertTrue($config->notThere === null); + + } + + public function testUnsetException() + { + // allow modifications is off - expect an exception + $config = new Config($this->all, false); + + $this->assertTrue(isset($config->hostname)); // top level + + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', 'is read only'); + unset($config->hostname); + } + + public function testUnset() + { + // allow modifications is on + $config = new Config($this->all, true); + + $this->assertTrue(isset($config->hostname)); + $this->assertTrue(isset($config->db->name)); + + unset($config->hostname); + unset($config->db->name); + + $this->assertFalse(isset($config->hostname)); + $this->assertFalse(isset($config->db->name)); + + } + + public function testMerge() + { + $configA = new Config($this->toCombineA); + $configB = new Config($this->toCombineB); + $configA->merge($configB); + + // config-> + $this->assertEquals(3, $configA->foo); + $this->assertEquals(2, $configA->bar); + $this->assertEquals('bar', $configA->text); + + // config->numerical-> ... + $this->assertInstanceOf('\Zend\Config\Config',$configA->numerical); + $this->assertEquals('first',$configA->numerical->{0}); + $this->assertEquals('second',$configA->numerical->{1}); + + // config->numerical->{2}-> ... + $this->assertInstanceOf('\Zend\Config\Config',$configA->numerical->{2}); + $this->assertEquals('third',$configA->numerical->{2}->{0}); + $this->assertEquals(null,$configA->numerical->{2}->{1}); + + // config->numerical-> ... + $this->assertEquals('fourth',$configA->numerical->{3}); + $this->assertEquals('fifth',$configA->numerical->{4}); + + // config->numerical->{5} + $this->assertInstanceOf('\Zend\Config\Config',$configA->numerical->{5}); + $this->assertEquals('sixth',$configA->numerical->{5}->{0}); + $this->assertEquals(null,$configA->numerical->{5}->{1}); + + // config->misaligned + $this->assertInstanceOf('\Zend\Config\Config',$configA->misaligned); + $this->assertEquals('foo',$configA->misaligned->{2}); + $this->assertEquals('bar',$configA->misaligned->{3}); + $this->assertEquals('baz',$configA->misaligned->{4}); + $this->assertEquals(null,$configA->misaligned->{0}); + + // config->mixed + $this->assertInstanceOf('\Zend\Config\Config',$configA->mixed); + $this->assertEquals('bar',$configA->mixed->foo); + $this->assertSame(false,$configA->mixed->{0}); + $this->assertSame(null,$configA->mixed->{1}); + + // config->replaceAssoc + $this->assertSame(null,$configA->replaceAssoc); + + // config->replaceNumerical + $this->assertSame(true,$configA->replaceNumerical); + + } + + public function testArrayAccess() + { + $config = new Config($this->all, true); + + $this->assertEquals('thisname', $config['name']); + $config['name'] = 'anothername'; + $this->assertEquals('anothername', $config['name']); + $this->assertEquals('multi', $config['one']['two']['three']); + + $this->assertTrue(isset($config['hostname'])); + $this->assertTrue(isset($config['db']['name'])); + + unset($config['hostname']); + unset($config['db']['name']); + + $this->assertFalse(isset($config['hostname'])); + $this->assertFalse(isset($config['db']['name'])); + } + + public function testArrayAccessModification() + { + $config = new Config($this->numericData, true); + + // Define some values we'll be using + $poem = array( + 'poem' => array ( + 'line 1' => 'Roses are red, bacon is also red,', + 'line 2' => 'Poems are hard,', + 'line 3' => 'Bacon.', + ), + ); + + $bacon = 'Bacon'; + + // Add a value + $config[] = $bacon; + + // Check if bacon now has a key that equals to 2 + $this->assertEquals($bacon, $config[2]); + + // Now let's try setting an array with no key supplied + $config[] = $poem; + + // This should now be set with key 3 + $this->assertEquals($poem, $config[3]->toArray()); + } + + /** + * Ensures that toArray() supports objects of types other than Zend_Config + * + * @return void + */ + public function testToArraySupportsObjects() + { + $configData = array( + 'a' => new \stdClass(), + 'b' => array( + 'c' => new \stdClass(), + 'd' => new \stdClass() + ) + ); + $config = new Config($configData); + $this->assertEquals($config->toArray(), $configData); + $this->assertInstanceOf('stdClass', $config->a); + $this->assertInstanceOf('stdClass', $config->b->c); + $this->assertInstanceOf('stdClass', $config->b->d); + } + + /** + * ensure that modification is not allowed after calling setReadOnly() + * + */ + public function testSetReadOnly() + { + $configData = array( + 'a' => 'a' + ); + $config = new Config($configData, true); + $config->b = 'b'; + + $config->setReadOnly(); + $this->setExpectedException('Zend\Config\Exception\RuntimeException', 'Config is read only'); + $config->c = 'c'; + } + + public function testZF3408_countNotDecreasingOnUnset() + { + $configData = array( + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + ); + $config = new Config($configData, true); + $this->assertEquals(count($config), 3); + unset($config->b); + $this->assertEquals(count($config), 2); + } + + public function testZF4107_ensureCloneDoesNotKeepNestedReferences() + { + $parent = new Config(array('key' => array('nested' => 'parent')), true); + $newConfig = clone $parent; + $newConfig->merge(new Config(array('key' => array('nested' => 'override')), true)); + + $this->assertEquals('override', $newConfig->key->nested, '$newConfig is not overridden'); + $this->assertEquals('parent', $parent->key->nested, '$parent has been overridden'); + + } + + /** + * @group ZF-3575 + * + */ + public function testMergeHonoursAllowModificationsFlagAtAllLevels() + { + $config = new Config(array('key' => array('nested' => 'yes'), 'key2'=>'yes'), false); + $config2 = new Config(array(), true); + + $config2->merge($config); + + $config2->key2 = 'no'; + + $this->assertEquals('no', $config2->key2); + + $config2->key->nested = 'no'; + + $this->assertEquals('no', $config2->key->nested); + } + + /** + * @group ZF-5771a + * + */ + public function testUnsettingFirstElementDuringForeachDoesNotSkipAnElement() + { + $config = new Config(array( + 'first' => array(1), + 'second' => array(2), + 'third' => array(3) + ), true); + + $keyList = array(); + foreach ($config as $key => $value) { + $keyList[] = $key; + if ($key == 'first') { + unset($config->$key); // uses magic Zend\Config\Config::__unset() method + } + } + + $this->assertEquals('first', $keyList[0]); + $this->assertEquals('second', $keyList[1]); + $this->assertEquals('third', $keyList[2]); + } + + /** + * @group ZF-5771 + * + */ + public function testUnsettingAMiddleElementDuringForeachDoesNotSkipAnElement() + { + $config = new Config(array( + 'first' => array(1), + 'second' => array(2), + 'third' => array(3) + ), true); + + $keyList = array(); + foreach ($config as $key => $value) { + $keyList[] = $key; + if ($key == 'second') { + unset($config->$key); // uses magic Zend\Config\Config::__unset() method + } + } + + $this->assertEquals('first', $keyList[0]); + $this->assertEquals('second', $keyList[1]); + $this->assertEquals('third', $keyList[2]); + } + + /** + * @group ZF-5771 + * + */ + public function testUnsettingLastElementDuringForeachDoesNotSkipAnElement() + { + $config = new Config(array( + 'first' => array(1), + 'second' => array(2), + 'third' => array(3) + ), true); + + $keyList = array(); + foreach ($config as $key => $value) { + $keyList[] = $key; + if ($key == 'third') { + unset($config->$key); // uses magic Zend\Config\Config::__unset() method + } + } + + $this->assertEquals('first', $keyList[0]); + $this->assertEquals('second', $keyList[1]); + $this->assertEquals('third', $keyList[2]); + } + + /** + * @group ZF-4728 + * + */ + public function testSetReadOnlyAppliesToChildren() + { + $config = new Config($this->all, true); + + $config->setReadOnly(); + $this->assertTrue($config->isReadOnly()); + $this->assertTrue($config->one->isReadOnly(), 'First level children are writable'); + $this->assertTrue($config->one->two->isReadOnly(), 'Second level children are writable'); + } + + public function testZF6995_toArrayDoesNotDisturbInternalIterator() + { + $config = new Config(range(1,10)); + $config->rewind(); + $this->assertEquals(1, $config->current()); + + $config->toArray(); + $this->assertEquals(1, $config->current()); + } + + /** + * @depends testMerge + * @link http://framework.zend.com/issues/browse/ZF2-186 + */ + public function testZF2_186_mergeReplacingUnnamedConfigSettings() + { + $arrayA = array( + 'flag' => true, + 'text' => 'foo', + 'list' => array( 'a', 'b', 'c' ), + 'aSpecific' => 12 + ); + + $arrayB = array( + 'flag' => false, + 'text' => 'bar', + 'list' => array( 'd', 'e' ), + 'bSpecific' => 100 + ); + + $mergeResult = array( + 'flag' => false, + 'text' => 'bar', + 'list' => array( 'a', 'b', 'c', 'd', 'e' ), + 'aSpecific' => 12, + 'bSpecific' => 100 + ); + + $configA = new Config($arrayA); + $configB = new Config($arrayB); + + $configA->merge($configB); // merge B onto A + $this->assertEquals($mergeResult, $configA->toArray()); + } +} + diff --git a/test/FactoryTest.php b/test/FactoryTest.php new file mode 100644 index 0000000..ffd4836 --- /dev/null +++ b/test/FactoryTest.php @@ -0,0 +1,143 @@ +assertEquals('bar', $config['base']['foo']); + } + + public function testFromXml() + { + $config = Factory::fromFile(__DIR__ . '/TestAssets/Xml/include-base.xml'); + + $this->assertEquals('bar', $config['base']['foo']); + } + + public function testFromIniFiles() + { + $files = array ( + __DIR__ . '/TestAssets/Ini/include-base.ini', + __DIR__ . '/TestAssets/Ini/include-base2.ini' + ); + $config = Factory::fromFiles($files); + + $this->assertEquals('bar', $config['base']['foo']); + $this->assertEquals('baz', $config['test']['bar']); + } + + public function testFromXmlFiles() + { + $files = array ( + __DIR__ . '/TestAssets/Xml/include-base.xml', + __DIR__ . '/TestAssets/Xml/include-base2.xml' + ); + $config = Factory::fromFiles($files); + + $this->assertEquals('bar', $config['base']['foo']); + $this->assertEquals('baz', $config['test']['bar']); + } + + public function testFromPhpFiles() + { + $files = array ( + __DIR__ . '/TestAssets/Php/include-base.php', + __DIR__ . '/TestAssets/Php/include-base2.php' + ); + $config = Factory::fromFiles($files); + + $this->assertEquals('bar', $config['base']['foo']); + $this->assertEquals('baz', $config['test']['bar']); + } + + public function testFromIniAndXmlAndPhpFiles() + { + $files = array ( + __DIR__ . '/TestAssets/Ini/include-base.ini', + __DIR__ . '/TestAssets/Xml/include-base2.xml', + __DIR__ . '/TestAssets/Php/include-base3.php', + ); + $config = Factory::fromFiles($files); + + $this->assertEquals('bar', $config['base']['foo']); + $this->assertEquals('baz', $config['test']['bar']); + $this->assertEquals('baz', $config['last']['bar']); + } + + public function testReturnsConfigObjectIfRequestedAndArrayOtherwise() + { + $files = array ( + __DIR__ . '/TestAssets/Ini/include-base.ini', + ); + + $configArray = Factory::fromFile($files[0]); + $this->assertTrue(is_array($configArray)); + + $configArray = Factory::fromFiles($files); + $this->assertTrue(is_array($configArray)); + + $configObject = Factory::fromFile($files[0], true); + $this->assertInstanceOf('Zend\Config\Config', $configObject); + + $configObject = Factory::fromFiles($files, true); + $this->assertInstanceOf('Zend\Config\Config', $configObject); + } + + public function testNonExistentFileThrowsRuntimeException() + { + $this->setExpectedException('RuntimeException'); + $config = Factory::fromFile('foo.bar'); + } + + public function testUnsupportedFileExtensionThrowsRuntimeException() + { + $this->setExpectedException('RuntimeException'); + $config = Factory::fromFile(__DIR__ . '/TestAssets/bad.ext'); + } + + public function testFactoryCanRegisterCustomReaderInstance() + { + Factory::registerReader('dum', new Reader\TestAssets\DummyReader()); + + $configObject = Factory::fromFile(__DIR__ . '/TestAssets/dummy.dum', true); + $this->assertInstanceOf('Zend\Config\Config', $configObject); + + $this->assertEquals($configObject['one'], 1); + } + + public function testFactoryCanRegisterCustomReaderPlugn() + { + $dummyReader = new Reader\TestAssets\DummyReader(); + Factory::getReaderPluginManager()->setService('DummyReader',$dummyReader); + + Factory::registerReader('dum', 'DummyReader'); + + $configObject = Factory::fromFile(__DIR__ . '/TestAssets/dummy.dum', true); + $this->assertInstanceOf('Zend\Config\Config', $configObject); + + $this->assertEquals($configObject['one'], 1); + } + + +} + diff --git a/test/ProcessorTest.php b/test/ProcessorTest.php new file mode 100644 index 0000000..aaeb368 --- /dev/null +++ b/test/ProcessorTest.php @@ -0,0 +1,495 @@ +nested = array( + 'a' => 1, + 'b' => 2, + 'c' => array( + 'ca' => 3, + 'cb' => 4, + 'cc' => 5, + 'cd' => array( + 'cda' => 6, + 'cdb' => 7 + ), + ), + 'd' => array( + 'da' => 8, + 'db' => 9 + ), + 'e' => 10 + ); + + $this->tokenBare = array( + 'simple' => 'BARETOKEN', + 'inside' => 'some text with BARETOKEN inside', + 'nested' => array( + 'simple' => 'BARETOKEN', + 'inside' => 'some text with BARETOKEN inside', + ), + ); + + $this->tokenPrefix = array( + 'simple' => '::TOKEN', + 'inside' => ':: some text with ::TOKEN inside ::', + 'nested' => array( + 'simple' => '::TOKEN', + 'inside' => ':: some text with ::TOKEN inside ::', + ), + ); + + $this->tokenSuffix = array( + 'simple' => 'TOKEN::', + 'inside' => ':: some text with TOKEN:: inside ::', + 'nested' => array( + 'simple' => 'TOKEN::', + 'inside' => ':: some text with TOKEN:: inside ::', + ), + ); + + $this->tokenSurround = array( + 'simple' => '##TOKEN##', + 'inside' => '## some text with ##TOKEN## inside ##', + 'nested' => array( + 'simple' => '##TOKEN##', + 'inside' => '## some text with ##TOKEN## inside ##', + ), + ); + + $this->tokenSurroundMixed = array( + 'simple' => '##TOKEN##', + 'inside' => '## some text with ##TOKEN## inside ##', + 'nested' => array( + 'simple' => '@@TOKEN@@', + 'inside' => '@@ some text with @@TOKEN@@ inside @@', + ), + ); + + $this->translatorData = array( + 'pages' => array( + array( + 'id' => 'oneDog', + 'label' => 'one dog', + 'route' => 'app-one-dog' + ), + array( + 'id' => 'twoDogs', + 'label' => 'two dogs', + 'route' => 'app-two-dogs' + ), + ) + ); + + $this->translatorFile = realpath(__DIR__ . '/_files/translations-de_DE.php'); + + $this->filter = array( + 'simple' => 'some MixedCase VALue', + 'nested' => array( + 'simple' => 'OTHER mixed Case Value', + ), + ); + + $this->userConstants = array( + 'simple' => 'SOME_USERLAND_CONSTANT', + 'inside' => 'some text with SOME_USERLAND_CONSTANT inside', + 'nested' => array( + 'simple' => 'SOME_USERLAND_CONSTANT', + 'inside' => 'some text with SOME_USERLAND_CONSTANT inside', + ), + ); + + $this->phpConstants = array( + 'phpVersion' => 'PHP_VERSION', + 'phpVersionInside' => 'Current PHP version is: PHP_VERSION', + 'nested' => array( + 'phpVersion' => 'PHP_VERSION', + 'phpVersionInside' => 'Current PHP version is: PHP_VERSION', + ), + ); + } + + public function testProcessorsQueue() + { + $processor1 = new TokenProcessor(); + $processor2 = new TokenProcessor(); + $queue = new Queue(); + $queue->insert($processor1); + $queue->insert($processor2); + + $this->assertInstanceOf('\Zend\Config\Processor\Queue', $queue); + $this->assertEquals(2, $queue->count()); + $this->assertTrue($queue->contains($processor1)); + $this->assertTrue($queue->contains($processor2)); + } + + public function testBareTokenPost() + { + $config = new Config($this->tokenBare, true); + $processor = new TokenProcessor(); + $processor->addToken('BARETOKEN', 'some replaced value'); + $processor->process($config); + + $this->assertEquals(array('BARETOKEN' => 'some replaced value'), $processor->getTokens()); + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals('some text with some replaced value inside', $config->inside); + $this->assertEquals('some replaced value', $config->nested->simple); + $this->assertEquals('some text with some replaced value inside', $config->nested->inside); + } + + public function testAddInvalidToken() + { + $processor = new TokenProcessor(); + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', + 'Cannot use ' . gettype(array()) . ' as token name.'); + $processor->addToken(array(), 'bar'); + } + + public function testSingleValueToken() + { + $processor = new TokenProcessor(); + $processor->addToken('BARETOKEN', 'test'); + $data = 'BARETOKEN'; + $out = $processor->processValue($data); + $this->assertEquals($out, 'test'); + } + + public function testTokenReadOnly() + { + $config = new Config($this->tokenBare, false); + $processor = new TokenProcessor(); + $processor->addToken('BARETOKEN', 'some replaced value'); + + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', + 'Cannot process config because it is read-only'); + $processor->process($config); + } + + public function testTokenPrefix() + { + $config = new Config($this->tokenPrefix, true); + $processor = new TokenProcessor(array('TOKEN' => 'some replaced value'), '::'); + $processor->process($config); + + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals(':: some text with some replaced value inside ::', $config->inside); + $this->assertEquals('some replaced value', $config->nested->simple); + $this->assertEquals(':: some text with some replaced value inside ::', $config->nested->inside); + } + + public function testTokenSuffix() + { + $config = new Config($this->tokenSuffix, true); + $processor = new TokenProcessor(array('TOKEN' => 'some replaced value'), '', '::'); + $processor->process($config); + + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals(':: some text with some replaced value inside ::', $config->inside); + $this->assertEquals('some replaced value', $config->nested->simple); + $this->assertEquals(':: some text with some replaced value inside ::', $config->nested->inside); + } + + /** + * @depends testTokenSuffix + * @depends testTokenPrefix + */ + public function testTokenSurround() + { + $config = new Config($this->tokenSurround, true); + $processor = new TokenProcessor(array('TOKEN' => 'some replaced value'), '##', '##'); + $processor->process($config); + + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals('## some text with some replaced value inside ##', $config->inside); + $this->assertEquals('some replaced value', $config->nested->simple); + $this->assertEquals('## some text with some replaced value inside ##', $config->nested->inside); + } + + /** + * @depends testTokenSurround + */ + public function testTokenChangeParams() + { + $config = new Config($this->tokenSurroundMixed, true); + $processor = new TokenProcessor(array('TOKEN' => 'some replaced value'), '##', '##'); + $processor->process($config); + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals('## some text with some replaced value inside ##', $config->inside); + $this->assertEquals('@@TOKEN@@', $config->nested->simple); + $this->assertEquals('@@ some text with @@TOKEN@@ inside @@', $config->nested->inside); + + /** + * Now change prefix and suffix on the processor + */ + $processor->setPrefix('@@'); + $processor->setSuffix('@@'); + + /** + * Parse the config again + */ + $processor->process($config); + + $this->assertEquals('some replaced value', $config->simple); + $this->assertEquals('## some text with some replaced value inside ##', $config->inside); + $this->assertEquals('some replaced value', $config->nested->simple); + $this->assertEquals('@@ some text with some replaced value inside @@', $config->nested->inside); + } + + /** + * @depends testTokenSurround + */ + public function testUserConstants() + { + define('SOME_USERLAND_CONSTANT', 'some constant value'); + + $config = new Config($this->userConstants, true); + $processor = new ConstantProcessor(false); + $processor->process($config); + + $tokens = $processor->getTokens(); + $this->assertTrue(is_array($tokens)); + $this->assertTrue(in_array('SOME_USERLAND_CONSTANT', $tokens)); + $this->assertTrue(!$processor->getUserOnly()); + + $this->assertEquals('some constant value', $config->simple); + $this->assertEquals('some text with some constant value inside', $config->inside); + $this->assertEquals('some constant value', $config->nested->simple); + $this->assertEquals('some text with some constant value inside', $config->nested->inside); + } + + /** + * @depends testUserConstants + */ + public function testUserOnlyConstants() + { + + $config = new Config($this->userConstants, true); + $processor = new ConstantProcessor(); + $processor->process($config); + + $tokens = $processor->getTokens(); + + $this->assertTrue(is_array($tokens)); + $this->assertTrue(in_array('SOME_USERLAND_CONSTANT', $tokens)); + $this->assertTrue($processor->getUserOnly()); + + $this->assertEquals('some constant value', $config->simple); + $this->assertEquals('some text with some constant value inside', $config->inside); + $this->assertEquals('some constant value', $config->nested->simple); + $this->assertEquals('some text with some constant value inside', $config->nested->inside); + } + + /** + * @depends testTokenSurround + */ + public function testPHPConstants() + { + $config = new Config($this->phpConstants, true); + $processor = new ConstantProcessor(false); + $processor->process($config); + + $this->assertEquals(PHP_VERSION, $config->phpVersion); + $this->assertEquals('Current PHP version is: ' . PHP_VERSION, $config->phpVersionInside); + $this->assertEquals(PHP_VERSION, $config->nested->phpVersion); + $this->assertEquals('Current PHP version is: ' . PHP_VERSION, $config->nested->phpVersionInside); + } + + public function testTranslator() + { + $config = new Config($this->translatorData, true); + $translator = new Translator(); + $translator->addTranslationFile('phparray', $this->translatorFile); + $processor = new TranslatorProcessor($translator); + + $processor->process($config); + + $this->assertEquals('oneDog', $config->pages[0]->id); + $this->assertEquals('ein Hund', $config->pages[0]->label); + $this->assertEquals('twoDogs', $config->pages[1]->id); + $this->assertEquals('zwei Hunde', $config->pages[1]->label); + } + + public function testTranslatorReadOnly() + { + $config = new Config($this->translatorData, false); + $translator = new Translator(); + $processor = new TranslatorProcessor($translator); + + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', + 'Cannot process config because it is read-only'); + $processor->process($config); + } + + public function testTranslatorSingleValue() + { + $translator = new Translator(); + $translator->addTranslationFile('phparray', $this->translatorFile); + $processor = new TranslatorProcessor($translator); + + $this->assertEquals('ein Hund', $processor->processValue('one dog')); + } + + public function testFilter() + { + $config = new Config($this->filter, true); + $filter = new StringToLower(); + $processor = new FilterProcessor($filter); + + $this->assertTrue($processor->getFilter() instanceof StringToLower); + $processor->process($config); + + $this->assertEquals('some mixedcase value', $config->simple); + $this->assertEquals('other mixed case value', $config->nested->simple); + } + + public function testFilterReadOnly() + { + $config = new Config($this->filter, false); + $filter = new StringToLower(); + $processor = new FilterProcessor($filter); + + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', + 'Cannot process config because it is read-only'); + $processor->process($config); + } + + public function testFilterValue() + { + $filter = new StringToLower(); + $processor = new FilterProcessor($filter); + + $value = 'TEST'; + $this->assertEquals('test', $processor->processValue($value)); + } + + /** + * @depends testFilter + */ + public function testQueueFIFO() + { + $config = new Config($this->filter, true); + $lower = new StringToLower(); + $upper = new StringToUpper(); + $lowerProcessor = new FilterProcessor($lower); + $upperProcessor = new FilterProcessor($upper); + + /** + * Default queue order (FIFO) + */ + $queue = new Queue(); + $queue->insert($upperProcessor); + $queue->insert($lowerProcessor); + $queue->process($config); + + $this->assertEquals('some mixedcase value', $config->simple); + $this->assertEquals('other mixed case value', $config->nested->simple); + } + + public function testQueueReadOnly() + { + $config = new Config($this->filter, false); + $lower = new StringToLower(); + $lowerProcessor = new FilterProcessor($lower); + + /** + * Default queue order (FIFO) + */ + $queue = new Queue(); + $queue->insert($lowerProcessor); + + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', + 'Cannot process config because it is read-only'); + $queue->process($config); + } + + public function testQueueSingleValue() + { + $lower = new StringToLower(); + $upper = new StringToUpper(); + $lowerProcessor = new FilterProcessor($lower); + $upperProcessor = new FilterProcessor($upper); + + /** + * Default queue order (FIFO) + */ + $queue = new Queue(); + $queue->insert($upperProcessor); + $queue->insert($lowerProcessor); + + $data ='TeSt'; + $this->assertEquals('test', $queue->processValue($data)); + + } + + /** + * @depends testQueueFIFO + */ + public function testQueuePriorities() + { + $config = new Config($this->filter, 1); + $lower = new StringToLower(); + $upper = new StringToUpper(); + $replace = new PregReplace('/[a-z]/', ''); + $lowerProcessor = new FilterProcessor($lower); + $upperProcessor = new FilterProcessor($upper); + $replaceProcessor = new FilterProcessor($replace); + $queue = new Queue(); + + /** + * Insert lower case filter with higher priority + */ + $queue->insert($upperProcessor, 10); + $queue->insert($lowerProcessor, 1000); + + $config->simple = 'some MixedCase VALue'; + $queue->process($config); + $this->assertEquals('SOME MIXEDCASE VALUE', $config->simple); + + /** + * Add even higher priority replace processor that will remove all lowercase letters + */ + $queue->insert($replaceProcessor, 10000); + $config->newValue = 'THIRD mixed CASE value'; + $queue->process($config); + $this->assertEquals('THIRD CASE ', $config->newValue); + } + +} + diff --git a/test/Reader/AbstractReaderTestCase.php b/test/Reader/AbstractReaderTestCase.php new file mode 100644 index 0000000..1e45a5c --- /dev/null +++ b/test/Reader/AbstractReaderTestCase.php @@ -0,0 +1,56 @@ +getTestAssetPath('no-file'); + $this->setExpectedException('Zend\Config\Exception\RuntimeException', "doesn't exist or not readable"); + $config = $this->reader->fromFile($filename); + } + + public function testFromFile() + { + $config = $this->reader->fromFile($this->getTestAssetPath('include-base')); + $this->assertEquals('foo', $config['foo']); + } + + public function testFromEmptyString() + { + $config = $this->reader->fromString(''); + $this->assertTrue(!$config); + } +} diff --git a/test/Reader/IniTest.php b/test/Reader/IniTest.php new file mode 100644 index 0000000..66484b9 --- /dev/null +++ b/test/Reader/IniTest.php @@ -0,0 +1,103 @@ +reader = new Ini(); + } + + /** + * getTestAssetPath(): defined by AbstractReaderTestCase. + * + * @see AbstractReaderTestCase::getTestAssetPath() + * @return string + */ + protected function getTestAssetPath($name) + { + return __DIR__ . '/TestAssets/Ini/' . $name . '.ini'; + } + + public function testInvalidIniFile() + { + $this->reader = new Ini(); + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayIni = $this->reader->fromFile($this->getTestAssetPath('invalid')); + } + + public function testFromString() + { + $ini = <<reader->fromString($ini); + $this->assertEquals($arrayIni['test'], 'foo'); + $this->assertEquals($arrayIni['bar'][0], 'baz'); + $this->assertEquals($arrayIni['bar'][1], 'foo'); + } + + public function testInvalidString() + { + $ini = <<setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayIni = $this->reader->fromString($ini); + } + + public function testFromStringWithSection() + { + $ini = <<reader->fromString($ini); + $this->assertEquals($arrayIni['all']['test'], 'foo'); + $this->assertEquals($arrayIni['all']['bar'][0], 'baz'); + $this->assertEquals($arrayIni['all']['bar'][1], 'foo'); + } + + public function testFromStringNested() + { + $ini = <<reader->fromString($ini); + $this->assertEquals($arrayIni['foo']['bar'], 'foobar'); + $this->assertEquals($arrayIni['foobar'][0], 'foobarArray'); + $this->assertEquals($arrayIni['foo']['baz'][0], 'foobaz1'); + $this->assertEquals($arrayIni['foo']['baz'][1], 'foobaz2'); + } +} diff --git a/test/Reader/JsonTest.php b/test/Reader/JsonTest.php new file mode 100644 index 0000000..20cc681 --- /dev/null +++ b/test/Reader/JsonTest.php @@ -0,0 +1,70 @@ +reader = new Json(); + } + + /** + * getTestAssetPath(): defined by AbstractReaderTestCase. + * + * @see AbstractReaderTestCase::getTestAssetPath() + * @return string + */ + protected function getTestAssetPath($name) + { + return __DIR__ . '/TestAssets/Json/' . $name . '.json'; + } + + public function testInvalidJsonFile() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayJson = $this->reader->fromFile($this->getTestAssetPath('invalid')); + } + + public function testIncludeAsElement() + { + $arrayJson = $this->reader->fromFile($this->getTestAssetPath('include-base_nested')); + $this->assertEquals($arrayJson['bar']['foo'], 'foo'); + } + + public function testFromString() + { + $json = '{ "test" : "foo", "bar" : [ "baz", "foo" ] }'; + + $arrayJson = $this->reader->fromString($json); + + $this->assertEquals($arrayJson['test'], 'foo'); + $this->assertEquals($arrayJson['bar'][0], 'baz'); + $this->assertEquals($arrayJson['bar'][1], 'foo'); + } + + public function testInvalidString() + { + $json = '{"foo":"bar"'; + + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayIni = $this->reader->fromString($json); + } + +} diff --git a/test/Reader/TestAssets/DummyReader.php b/test/Reader/TestAssets/DummyReader.php new file mode 100644 index 0000000..95d24d5 --- /dev/null +++ b/test/Reader/TestAssets/DummyReader.php @@ -0,0 +1,35 @@ + + + + 2a + 2b + + + + 5 + + 4 + + 5 + + + + + 1 + 1 + + + 2 + 2 + + + 3 + 3 + + + + diff --git a/test/Reader/TestAssets/Xml/include-base.xml b/test/Reader/TestAssets/Xml/include-base.xml new file mode 100644 index 0000000..3c52f34 --- /dev/null +++ b/test/Reader/TestAssets/Xml/include-base.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/Reader/TestAssets/Xml/include-target.xml b/test/Reader/TestAssets/Xml/include-target.xml new file mode 100644 index 0000000..866fe2a --- /dev/null +++ b/test/Reader/TestAssets/Xml/include-target.xml @@ -0,0 +1,2 @@ + +foo \ No newline at end of file diff --git a/test/Reader/TestAssets/Xml/invalid.xml b/test/Reader/TestAssets/Xml/invalid.xml new file mode 100644 index 0000000..edac7d5 --- /dev/null +++ b/test/Reader/TestAssets/Xml/invalid.xml @@ -0,0 +1,4 @@ + + + value + \ No newline at end of file diff --git a/test/Reader/TestAssets/Yaml/include-base.yaml b/test/Reader/TestAssets/Yaml/include-base.yaml new file mode 100644 index 0000000..4f891f2 --- /dev/null +++ b/test/Reader/TestAssets/Yaml/include-base.yaml @@ -0,0 +1 @@ +@include: include-target.yaml \ No newline at end of file diff --git a/test/Reader/TestAssets/Yaml/include-target.yaml b/test/Reader/TestAssets/Yaml/include-target.yaml new file mode 100644 index 0000000..d867783 --- /dev/null +++ b/test/Reader/TestAssets/Yaml/include-target.yaml @@ -0,0 +1 @@ +foo: foo \ No newline at end of file diff --git a/test/Reader/XmlTest.php b/test/Reader/XmlTest.php new file mode 100644 index 0000000..9258a74 --- /dev/null +++ b/test/Reader/XmlTest.php @@ -0,0 +1,98 @@ +reader = new Xml(); + } + + /** + * getTestAssetPath(): defined by AbstractReaderTestCase. + * + * @see AbstractReaderTestCase::getTestAssetPath() + * @return string + */ + protected function getTestAssetPath($name) + { + return __DIR__ . '/TestAssets/Xml/' . $name . '.xml'; + } + + public function testInvalidXmlFile() + { + $this->reader = new Xml(); + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayXml = $this->reader->fromFile($this->getTestAssetPath('invalid')); + } + + public function testFromString() + { + $xml = << + + foo + baz + foo + + +ECS; + + $arrayXml= $this->reader->fromString($xml); + $this->assertEquals($arrayXml['test'], 'foo'); + $this->assertEquals($arrayXml['bar'][0], 'baz'); + $this->assertEquals($arrayXml['bar'][1], 'foo'); + } + + public function testInvalidString() + { + $xml = << + + baz + + +ECS; + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayXml = $this->reader->fromString($xml); + } + + public function testZF300_MultipleKeysOfTheSameName() + { + $config = $this->reader->fromFile($this->getTestAssetPath('array')); + + $this->assertEquals('2a', $config['one']['two'][0]); + $this->assertEquals('2b', $config['one']['two'][1]); + $this->assertEquals('4', $config['three']['four'][1]); + $this->assertEquals('5', $config['three']['four'][0]['five']); + } + + public function testZF300_ArraysWithMultipleChildren() + { + $config = $this->reader->fromFile($this->getTestAssetPath('array')); + + $this->assertEquals('1', $config['six']['seven'][0]['eight']); + $this->assertEquals('2', $config['six']['seven'][1]['eight']); + $this->assertEquals('3', $config['six']['seven'][2]['eight']); + $this->assertEquals('1', $config['six']['seven'][0]['nine']); + $this->assertEquals('2', $config['six']['seven'][1]['nine']); + $this->assertEquals('3', $config['six']['seven'][2]['nine']); + } +} diff --git a/test/Reader/YamlTest.php b/test/Reader/YamlTest.php new file mode 100644 index 0000000..cf9be60 --- /dev/null +++ b/test/Reader/YamlTest.php @@ -0,0 +1,91 @@ +markTestSkipped('Yaml test for Zend\Config skipped'); + } + + if (constant('TESTS_ZEND_CONFIG_YAML_LIB_INCLUDE')) { + require_once constant('TESTS_ZEND_CONFIG_YAML_LIB_INCLUDE'); + } + + $yamlReader = explode('::', constant('TESTS_ZEND_CONFIG_READER_YAML_CALLBACK')); + if (isset($yamlReader[1])) { + $this->reader = new YamlReader(array($yamlReader[0], $yamlReader[1])); + } else { + $this->reader = new YamlReader(array($yamlReader[0])); + } + } + + /** + * getTestAssetPath(): defined by AbstractReaderTestCase. + * + * @see AbstractReaderTestCase::getTestAssetPath() + * @return string + */ + protected function getTestAssetPath($name) + { + return __DIR__ . '/TestAssets/Yaml/' . $name . '.yaml'; + } + + public function testInvalidIniFile() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $arrayIni = $this->reader->fromFile($this->getTestAssetPath('invalid')); + } + + public function testFromString() + { + $yaml = <<reader->fromString($yaml); + $this->assertEquals($arrayYaml['test'], 'foo'); + $this->assertEquals($arrayYaml['bar'][0], 'baz'); + $this->assertEquals($arrayYaml['bar'][1], 'foo'); + } + + public function testFromStringWithSection() + { + $yaml = <<reader->fromString($yaml); + $this->assertEquals($arrayYaml['all']['test'], 'foo'); + $this->assertEquals($arrayYaml['all']['bar'][0], 'baz'); + $this->assertEquals($arrayYaml['all']['bar'][1], 'foo'); + } +} diff --git a/test/TestAssets/Ini/include-base.ini b/test/TestAssets/Ini/include-base.ini new file mode 100644 index 0000000..54d0b56 --- /dev/null +++ b/test/TestAssets/Ini/include-base.ini @@ -0,0 +1,2 @@ +[base] +foo = "bar" \ No newline at end of file diff --git a/test/TestAssets/Ini/include-base2.ini b/test/TestAssets/Ini/include-base2.ini new file mode 100644 index 0000000..c2a372b --- /dev/null +++ b/test/TestAssets/Ini/include-base2.ini @@ -0,0 +1,2 @@ +[test] +bar = "baz" diff --git a/test/TestAssets/Php/include-base.php b/test/TestAssets/Php/include-base.php new file mode 100644 index 0000000..f773522 --- /dev/null +++ b/test/TestAssets/Php/include-base.php @@ -0,0 +1,6 @@ + array( + 'bar' => 'baz', + ), +); diff --git a/test/TestAssets/Php/include-base2.php b/test/TestAssets/Php/include-base2.php new file mode 100644 index 0000000..a870592 --- /dev/null +++ b/test/TestAssets/Php/include-base2.php @@ -0,0 +1,6 @@ + array( + 'foo' => 'bar', + ), +); diff --git a/test/TestAssets/Php/include-base3.php b/test/TestAssets/Php/include-base3.php new file mode 100644 index 0000000..04fa19b --- /dev/null +++ b/test/TestAssets/Php/include-base3.php @@ -0,0 +1,6 @@ + array( + 'bar' => 'baz', + ), +); diff --git a/test/TestAssets/Xml/include-base.xml b/test/TestAssets/Xml/include-base.xml new file mode 100644 index 0000000..82b9111 --- /dev/null +++ b/test/TestAssets/Xml/include-base.xml @@ -0,0 +1,6 @@ + + + + bar + + \ No newline at end of file diff --git a/test/TestAssets/Xml/include-base2.xml b/test/TestAssets/Xml/include-base2.xml new file mode 100644 index 0000000..e7cc59a --- /dev/null +++ b/test/TestAssets/Xml/include-base2.xml @@ -0,0 +1,6 @@ + + + + baz + + \ No newline at end of file diff --git a/test/TestAssets/bad.ext b/test/TestAssets/bad.ext new file mode 100644 index 0000000..e69de29 diff --git a/test/TestAssets/dummy.dum b/test/TestAssets/dummy.dum new file mode 100644 index 0000000..3379326 --- /dev/null +++ b/test/TestAssets/dummy.dum @@ -0,0 +1 @@ +a:3:{s:3:"one";i:1;s:3:"two";i:2;s:5:"three";i:3;} \ No newline at end of file diff --git a/test/Writer/AbstractWriterTestCase.php b/test/Writer/AbstractWriterTestCase.php new file mode 100644 index 0000000..921a89b --- /dev/null +++ b/test/Writer/AbstractWriterTestCase.php @@ -0,0 +1,93 @@ +tmpfile)) { + $this->tmpfile = tempnam(sys_get_temp_dir(), 'zend-config-writer'); + } + return $this->tmpfile; + } + + public function tearDown() + { + if (file_exists($this->getTestAssetFileName())) { + if (!is_writable($this->getTestAssetFileName())) { + chmod($this->getTestAssetFileName(), 0777); + } + @unlink($this->getTestAssetFileName()); + } + } + + public function testNoFilenameSet() + { + $this->setExpectedException('Zend\Config\Exception\InvalidArgumentException', 'No file name specified'); + $this->writer->toFile('', ''); + } + + public function testFileNotValid() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + $this->writer->toFile('.', new Config(array())); + } + + public function testFileNotWritable() + { + $this->setExpectedException('Zend\Config\Exception\RuntimeException'); + chmod($this->getTestAssetFileName(), 0444); + $this->writer->toFile($this->getTestAssetFileName(), new Config(array())); + } + + public function testWriteAndRead() + { + $config = new Config(array('default' => array('test' => 'foo'))); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('foo', $config['default']['test']); + } +} diff --git a/test/Writer/IniTest.php b/test/Writer/IniTest.php new file mode 100644 index 0000000..287ad4d --- /dev/null +++ b/test/Writer/IniTest.php @@ -0,0 +1,53 @@ +reader = new IniReader(); + $this->writer = new IniWriter(); + } + + public function testNoSection() + { + $config = new Config(array('test' => 'foo', 'test2' => array('test3' => 'bar'))); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('foo', $config['test']); + $this->assertEquals('bar', $config['test2']['test3']); + } + + public function testWriteAndReadOriginalFile() + { + $config = $this->reader->fromFile(__DIR__ . '/_files/allsections.ini'); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('multi', $config['all']['one']['two']['three']); + } +} diff --git a/test/Writer/JsonTest.php b/test/Writer/JsonTest.php new file mode 100644 index 0000000..a37ba6a --- /dev/null +++ b/test/Writer/JsonTest.php @@ -0,0 +1,54 @@ +reader = new JsonReader(); + $this->writer = new JsonWriter(); + } + + public function testNoSection() + { + $config = new Config(array('test' => 'foo', 'test2' => array('test3' => 'bar'))); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('foo', $config['test']); + $this->assertEquals('bar', $config['test2']['test3']); + } + + public function testWriteAndReadOriginalFile() + { + $config = $this->reader->fromFile(__DIR__ . '/_files/allsections.json'); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('multi', $config['all']['one']['two']['three']); + + } +} diff --git a/test/Writer/PhpArrayTest.php b/test/Writer/PhpArrayTest.php new file mode 100644 index 0000000..2dae349 --- /dev/null +++ b/test/Writer/PhpArrayTest.php @@ -0,0 +1,55 @@ +writer = new PhpArray(); + $this->reader = new PhpReader(); + } + + /** + * @group ZF-8234 + */ + public function testRender() + { + $config = new Config(array('test' => 'foo', 'bar' => array(0 => 'baz', 1 => 'foo'))); + + $configString = $this->writer->toString($config); + + // build string line by line as we are trailing-whitespace sensitive. + $expected = " 'foo',\n"; + $expected .= " 'bar' => \n"; + $expected .= " array (\n"; + $expected .= " 0 => 'baz',\n"; + $expected .= " 1 => 'foo',\n"; + $expected .= " ),\n"; + $expected .= ");\n"; + + $this->assertEquals($expected, $configString); + } +} diff --git a/test/Writer/TestAssets/PhpReader.php b/test/Writer/TestAssets/PhpReader.php new file mode 100644 index 0000000..909ff1e --- /dev/null +++ b/test/Writer/TestAssets/PhpReader.php @@ -0,0 +1,19 @@ +writer = new XmlWriter(); + $this->reader = new XmlReader(); + } + + public function testToString() + { + $config = new Config(array('test' => 'foo', 'bar' => array(0 => 'baz', 1 => 'foo'))); + + $configString = $this->writer->toString($config); + + $expected = << + + foo + baz + foo + + +ECS; + + $this->assertEquals($expected, $configString); + } + + public function testSectionsToString() + { + $config = new Config(array(), true); + $config->production = array(); + + $config->production->webhost = 'www.example.com'; + $config->production->database = array(); + $config->production->database->params = array(); + $config->production->database->params->host = 'localhost'; + $config->production->database->params->username = 'production'; + $config->production->database->params->password = 'secret'; + $config->production->database->params->dbname = 'dbproduction'; + + $configString = $this->writer->toString($config); + + $expected = << + + + www.example.com + + + localhost + production + secret + dbproduction + + + + + +ECS; + + $this->assertEquals($expected, $configString); + } +} diff --git a/test/Writer/YamlTest.php b/test/Writer/YamlTest.php new file mode 100644 index 0000000..d0d9db1 --- /dev/null +++ b/test/Writer/YamlTest.php @@ -0,0 +1,73 @@ +markTestSkipped('Yaml test for Zend\Config skipped'); + } + + if (constant('TESTS_ZEND_CONFIG_YAML_LIB_INCLUDE')) { + require_once constant('TESTS_ZEND_CONFIG_YAML_LIB_INCLUDE'); + } + + $yamlReader = explode('::', constant('TESTS_ZEND_CONFIG_READER_YAML_CALLBACK')); + if (isset($yamlReader[1])) { + $this->reader = new YamlReader(array($yamlReader[0], $yamlReader[1])); + } else { + $this->reader = new YamlReader(array($yamlReader[0])); + } + + $yamlWriter = explode('::', constant('TESTS_ZEND_CONFIG_WRITER_YAML_CALLBACK')); + if (isset($yamlWriter[1])) { + $this->writer = new YamlWriter(array($yamlWriter[0], $yamlWriter[1])); + } else { + $this->writer = new YamlWriter(array($yamlWriter[0])); + } + } + + public function testNoSection() + { + $config = new Config(array('test' => 'foo', 'test2' => array('test3' => 'bar'))); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('foo', $config['test']); + $this->assertEquals('bar', $config['test2']['test3']); + } + + public function testWriteAndReadOriginalFile() + { + $config = $this->reader->fromFile(__DIR__ . '/_files/allsections.yaml'); + + $this->writer->toFile($this->getTestAssetFileName(), $config); + + $config = $this->reader->fromFile($this->getTestAssetFileName()); + + $this->assertEquals('multi', $config['all']['one']['two']['three']); + + } +} diff --git a/test/Writer/_files/allsections.ini b/test/Writer/_files/allsections.ini new file mode 100644 index 0000000..4eaa75b --- /dev/null +++ b/test/Writer/_files/allsections.ini @@ -0,0 +1,26 @@ +[all] +hostname = all +name = thisname +db.host = 127.0.0.1 +db.user = username +db.pass = password +db.name = live +one.two.three = multi + +[staging] +hostname = staging +db.name = dbstaging +debug = false + +[debug] +hostname = debug +debug = true +values.changed = yes +db.name = dbdebug +special.no = no +special.null = null +special.false = false + +[other_staging] +only_in = otherStaging +db.pass = anotherpwd diff --git a/test/Writer/_files/allsections.json b/test/Writer/_files/allsections.json new file mode 100644 index 0000000..d47bd4e --- /dev/null +++ b/test/Writer/_files/allsections.json @@ -0,0 +1,45 @@ +{ +"all" : { + "hostname" : "all", + "name" : "thisname", + "db" : { + "host" : "127.0.0.1", + "user" : "username", + "pass" : "password", + "name" : "live" + }, + "one" : { + "two" : { + "three" : "multi" + } + } +}, +"staging" : { + "hostname" : "staging", + "db" : { + "name" : "dbstaging" + }, + "debug" : "false" +}, +"debug" : { + "hostname" : "debug", + "debug" : "true", + "values" : { + "changed" : "yes" + }, + "db" : { + "name" : "dbdebug" + }, + "special" : { + "no" : "no", + "null" : "null", + "false" : "false" + } +}, +"other_staging" : { + "only_in" : "otherStaging", + "db" : { + "pass" : "anotherpwd" + } +} +} \ No newline at end of file diff --git a/test/Writer/_files/allsections.xml b/test/Writer/_files/allsections.xml new file mode 100644 index 0000000..1abb816 --- /dev/null +++ b/test/Writer/_files/allsections.xml @@ -0,0 +1,35 @@ + + + + all + thisname + + 127.0.0.1 + username + password + live + + + + multi + + + + + + staging + + dbstaging + + false + + + + otherStaging + + anotherpwd + + + + + \ No newline at end of file diff --git a/test/Writer/_files/allsections.yaml b/test/Writer/_files/allsections.yaml new file mode 100644 index 0000000..1dafe98 --- /dev/null +++ b/test/Writer/_files/allsections.yaml @@ -0,0 +1,35 @@ +all: +# this is a comment + hostname: all + name: thisname + db: + host: 127.0.0.1 + user: username + pass: password + name: live + one: + two: + three: multi + +staging: + hostname: staging + db: + name: dbstaging + debug: + _extends: all +debug: + hostname: debug + debug: 1 + values: + changed: 1 + db: + name: dbdebug + special: + no: + null: + false: + _extends: all +other_staging: + only_in: otherStaging + db: + pass: anotherpwd diff --git a/test/_files/indentedcomments.yaml b/test/_files/indentedcomments.yaml new file mode 100644 index 0000000..4b7b767 --- /dev/null +++ b/test/_files/indentedcomments.yaml @@ -0,0 +1,4 @@ +resources: + #frontcontroller! + frontController: + controllerDirectory: APPLICATION_PATH/controllers \ No newline at end of file diff --git a/test/_files/inlinecomments.yaml b/test/_files/inlinecomments.yaml new file mode 100644 index 0000000..c3b75e6 --- /dev/null +++ b/test/_files/inlinecomments.yaml @@ -0,0 +1,3 @@ +resources: + frontController: + controllerDirectory: APPLICATION_PATH/controllers #heynow! \ No newline at end of file diff --git a/test/_files/listbooleans.yaml b/test/_files/listbooleans.yaml new file mode 100644 index 0000000..21fc789 --- /dev/null +++ b/test/_files/listbooleans.yaml @@ -0,0 +1,51 @@ +production: + usingLowerCasedYes: + - yes + usingTitleCasedYes: + - Yes + usingCapitalYes: + - YES + usingLowerY: + - y + usingUpperY: + - Y + + usingLowerCasedNo: + - no + usingTitleCasedNo: + - No + usingCapitalNo: + - NO + usingLowerN: + - n + usingUpperN: + - N + + usingLowerCasedTrue: + - true + usingTitleCasedTrue: + - True + usingCapitalTrue: + - TRUE + + usingLowerCasedFalse: + - false + usingTitleCasedFalse: + - False + usingCapitalFalse: + - FALSE + + usingLowerCasedOn: + - on + usingTitleCasedOn: + - On + usingCapitalOn: + - ON + + usingLowerCasedOff: + - off + usingTitleCasedOff: + - Off + usingCapitalOff: + - OFF + diff --git a/test/_files/listconstants.yaml b/test/_files/listconstants.yaml new file mode 100644 index 0000000..86610ca --- /dev/null +++ b/test/_files/listconstants.yaml @@ -0,0 +1,4 @@ +production: + paths: + - ZEND_CONFIG_YAML_TEST_PATH + - ZEND_CONFIG_YAML_TEST_PATH/library/test diff --git a/test/_files/translations-de_DE.php b/test/_files/translations-de_DE.php new file mode 100644 index 0000000..5f42e0b --- /dev/null +++ b/test/_files/translations-de_DE.php @@ -0,0 +1,5 @@ + 'ein Hund', + 'two dogs' => 'zwei Hunde' +); diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..057f291 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +