├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractConfigFactory.php ├── Config.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── PluginNotFoundException.php ├── RuntimeException.php └── UnprocessableConfigException.php ├── Factory.php ├── Processor ├── Constant.php ├── Filter.php ├── ProcessorInterface.php ├── Queue.php ├── Token.php └── Translator.php ├── Reader ├── Ini.php ├── JavaProperties.php ├── Json.php ├── ReaderInterface.php ├── Xml.php └── Yaml.php ├── ReaderPluginManager.php ├── StandaloneReaderPluginManager.php ├── StandaloneWriterPluginManager.php ├── Writer ├── AbstractWriter.php ├── Ini.php ├── JavaProperties.php ├── Json.php ├── PhpArray.php ├── WriterInterface.php ├── Xml.php └── Yaml.php └── WriterPluginManager.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 3.3.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 3.3.0 - 2019-06-08 28 | 29 | ### Added 30 | 31 | - [#54](https://github.com/zendframework/zend-config/pull/54) adds support for PHP 7.3. 32 | - [#58](https://github.com/zendframework/zend-config/pull/58) adds 33 | `$processSections` to INI reader, allowing control over whether sections 34 | should be parsed or not 35 | - [#63](https://github.com/zendframework/zend-config/pull/63) adds .yml to 36 | Zend\Config\Factory as an alternative extension for yaml 37 | 38 | ### Changed 39 | 40 | - Nothing. 41 | 42 | ### Deprecated 43 | 44 | - Nothing. 45 | 46 | ### Removed 47 | 48 | - Nothing. 49 | 50 | ### Fixed 51 | 52 | - Nothing. 53 | 54 | ## 3.2.0 - 2018-04-24 55 | 56 | ### Added 57 | 58 | - [#47](https://github.com/zendframework/zend-config/pull/47) adds `Zend\Config\Writer\JavaProperties`, a complement to 59 | `Zend\Config\Reader\JavaProperties`, for writing JavaProperties files from configuration. The writer supports 60 | specifying an alternate key/value delimiter (the default is ":") via the constructor. 61 | 62 | - [#46](https://github.com/zendframework/zend-config/pull/46) adds a constructor option to the JavaProperties reader to allow 63 | users to indicate keys and values from the configuration should be trimmed of whitespace: 64 | 65 | ```php 66 | $reader = new JavaProperties( 67 | JavaProperties::DELIMITER_DEFAULT, // or ":" 68 | JavaProperties::WHITESPACE_TRIM, // or true; default is false 69 | ); 70 | ``` 71 | 72 | - [#45](https://github.com/zendframework/zend-config/pull/45) adds the ability to specify an alternate key/value delimiter to 73 | the JavaProperties config reader via the constructor: `$reader = new JavaProperties("=");`. 74 | 75 | - [#42](https://github.com/zendframework/zend-config/pull/42) adds support for PHP 7.1 and 7.2. 76 | 77 | ### Changed 78 | 79 | - Nothing. 80 | 81 | ### Deprecated 82 | 83 | - Nothing. 84 | 85 | ### Removed 86 | 87 | - [#42](https://github.com/zendframework/zend-config/pull/42) removes support for HHVM. 88 | 89 | ### Fixed 90 | 91 | - Nothing. 92 | 93 | ## 3.1.0 - 2017-02-22 94 | 95 | ### Added 96 | 97 | - [#37](https://github.com/zendframework/zend-config/pull/37) adds a new method, 98 | `enableKeyProcessing()`, and constructor argument, `$enableKeyProcessing = 99 | false`, to each of the `Token` and `Constant` processors. These allow enabling 100 | processing of tokens and/or constants encountered in configuration key values. 101 | 102 | - [#37](https://github.com/zendframework/zend-config/pull/37) adds the ability 103 | for the `Constant` processor to process class constants, including the 104 | `::class` pseudo-constant. 105 | 106 | ### Deprecated 107 | 108 | - Nothing. 109 | 110 | ### Removed 111 | 112 | - Nothing. 113 | 114 | ### Fixed 115 | 116 | - Nothing. 117 | 118 | ## 3.0.0 - 2017-02-16 119 | 120 | ### Added 121 | 122 | - [#36](https://github.com/zendframework/zend-config/pull/36) adds support for 123 | [PSR-11](http://www.php-fig.org/psr/psr-11/). 124 | 125 | - [#36](https://github.com/zendframework/zend-config/pull/36) adds the class 126 | `Zend\Config\StandaloneReaderPluginManager` for managing config reader plugins. 127 | This implementation implements the PSR-11 `ContainerInterface`, and uses a 128 | hard-coded list of reader plugins. 129 | 130 | - [#36](https://github.com/zendframework/zend-config/pull/36) adds the class 131 | `Zend\Config\StandaloneWriterPluginManager` for managing config writer plugins. 132 | This implementation implements the PSR-11 `ContainerInterface`, and uses a 133 | hard-coded list of writer plugins. 134 | 135 | ### Changes 136 | 137 | - [#36](https://github.com/zendframework/zend-config/pull/36) updates the 138 | `Zend\Config\Factory::getReaderPluginManager()` method to lazy-load a 139 | `StandaloneReaderPluginManager` by default, instead of a 140 | `ReaderPluginManager`, allowing usage out-of-the-box without requiring 141 | zend-servicemanager. 142 | 143 | - [#36](https://github.com/zendframework/zend-config/pull/36) updates the 144 | `Zend\Config\Factory::setReaderPluginManager()` method to typehint against 145 | `Psr\Container\ContainerInterface` instead of `ReaderPluginManager`. If you 146 | were extending and overriding that method, you will need to update your 147 | signature. 148 | 149 | - [#36](https://github.com/zendframework/zend-config/pull/36) updates the 150 | `Zend\Config\Factory::getWriterPluginManager()` method to lazy-load a 151 | `StandaloneWriterPluginManager` by default, instead of a 152 | `WriterPluginManager`, allowing usage out-of-the-box without requiring 153 | zend-servicemanager. 154 | 155 | - [#36](https://github.com/zendframework/zend-config/pull/36) updates the 156 | `Zend\Config\Factory::setWriterPluginManager()` method to typehint against 157 | `Psr\Container\ContainerInterface` instead of `WriterPluginManager`. If you 158 | were extending and overriding that method, you will need to update your 159 | signature. 160 | 161 | ### Deprecated 162 | 163 | - Nothing. 164 | 165 | ### Removed 166 | 167 | - [#36](https://github.com/zendframework/zend-config/pull/36) removes usage of 168 | zend-json as a JSON de/serializer in the JSON writer and reader; the 169 | component now requires ext/json is installed to use these features. 170 | 171 | ### Fixed 172 | 173 | - Nothing. 174 | 175 | ## 2.6.0 - 2016-02-04 176 | 177 | ### Added 178 | 179 | - [#6](https://github.com/zendframework/zend-config/pull/6) adds the ability for 180 | the `PhpArray` writer to optionally translate strings that evaluate to known 181 | classes to `ClassName::class` syntax; the feature works for both keys and 182 | values. 183 | - [#21](https://github.com/zendframework/zend-config/pull/21) adds revised 184 | documentation, and publishes it to https://zendframework.github.io/zend-config/ 185 | 186 | ### Deprecated 187 | 188 | - Nothing. 189 | 190 | ### Removed 191 | 192 | - Nothing. 193 | 194 | ### Fixed 195 | 196 | - [#8](https://github.com/zendframework/zend-config/pull/8), 197 | [#18](https://github.com/zendframework/zend-config/pull/18), and 198 | [#20](https://github.com/zendframework/zend-config/pull/20) update the 199 | code base to make it forwards-compatible with the v3.0 versions of 200 | zend-stdlib and zend-servicemanager. Primarily, this involved: 201 | - Updating the `AbstractConfigFactory` to implement the new methods in the 202 | v3 `AbstractFactoryInterface` definition, and updating the v2 methods to 203 | proxy to those. 204 | - Updating `ReaderPluginManager` and `WriterPluginManager` to follow the 205 | changes to `AbstractPluginManager`. In particular, instead of defining 206 | invokables, they now define a combination of aliases and factories (using 207 | the new `InvokableFactory`); additionally, they each now implement both 208 | `validatePlugin()` from v2 and `validate()` from v3. 209 | - Pinning to stable versions of already updated components. 210 | - Selectively omitting zend-i18n-reliant tests when testing against 211 | zend-servicemanager v3. 212 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2018, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-config 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-config](https://github.com/laminas/laminas-config). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-config.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-config) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-config/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-config?branch=master) 9 | 10 | zend-config is designed to simplify access to configuration data within 11 | applications. It provides a nested object property-based user interface for 12 | accessing this configuration data within application code. The configuration 13 | data may come from a variety of media supporting hierarchical data storage. 14 | 15 | - File issues at https://github.com/zendframework/zend-config/issues 16 | - Documentation is at https://docs.zendframework.com/zend-config/ 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-config", 3 | "description": "provides a nested object property based user interface for accessing this configuration data within application code", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "config" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-config/", 12 | "issues": "https://github.com/zendframework/zend-config/issues", 13 | "source": "https://github.com/zendframework/zend-config", 14 | "rss": "https://github.com/zendframework/zend-config/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "ext-json": "*", 21 | "zendframework/zend-stdlib": "^2.7.7 || ^3.1", 22 | "psr/container": "^1.0" 23 | }, 24 | "require-dev": { 25 | "malukenho/docheader": "^0.1.6", 26 | "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", 27 | "zendframework/zend-coding-standard": "~1.0.0", 28 | "zendframework/zend-filter": "^2.7.2", 29 | "zendframework/zend-i18n": "^2.7.4", 30 | "zendframework/zend-servicemanager": "^2.7.8 || ^3.3" 31 | }, 32 | "conflict": { 33 | "container-interop/container-interop": "<1.2.0" 34 | }, 35 | "suggest": { 36 | "zendframework/zend-filter": "^2.7.2; install if you want to use the Filter processor", 37 | "zendframework/zend-i18n": "^2.7.4; install if you want to use the Translator processor", 38 | "zendframework/zend-servicemanager": "^2.7.8 || ^3.3; if you need an extensible plugin manager for use with the Config Factory" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Zend\\Config\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "ZendTest\\Config\\": "test/" 48 | } 49 | }, 50 | "config": { 51 | "sort-packages": true 52 | }, 53 | "extra": { 54 | "branch-alias": { 55 | "dev-master": "3.3.x-dev", 56 | "dev-develop": "3.4.x-dev" 57 | } 58 | }, 59 | "scripts": { 60 | "check": [ 61 | "@license-check", 62 | "@cs-check", 63 | "@test" 64 | ], 65 | "cs-check": "phpcs", 66 | "cs-fix": "phpcbf", 67 | "test": "phpunit --colors=always", 68 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 69 | "license-check": "docheader check src/" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AbstractConfigFactory.php: -------------------------------------------------------------------------------- 1 | canCreate($serviceLocator, $requestedName); 49 | } 50 | 51 | /** 52 | * Determine if we can create a service (SM v3) 53 | * 54 | * @param ContainerInterface $container 55 | * @param string $requestedName 56 | * @return bool 57 | */ 58 | public function canCreate(ContainerInterface $container, $requestedName) 59 | { 60 | if (isset($this->configs[$requestedName])) { 61 | return true; 62 | } 63 | 64 | if (! $container->has('Config')) { 65 | return false; 66 | } 67 | 68 | $key = $this->match($requestedName); 69 | if (null === $key) { 70 | return false; 71 | } 72 | 73 | $config = $container->get('Config'); 74 | return isset($config[$key]); 75 | } 76 | 77 | /** 78 | * Create service with name (SM v2) 79 | * 80 | * @param ServiceLocatorInterface $serviceLocator 81 | * @param string $name 82 | * @param string $requestedName 83 | * @return string|mixed|array 84 | */ 85 | public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName) 86 | { 87 | return $this($serviceLocator, $requestedName); 88 | } 89 | 90 | /** 91 | * Create service with name (SM v3) 92 | * 93 | * @param ContainerInterface $container 94 | * @param string $requestedName 95 | * @param array $options 96 | * @return string|mixed|array 97 | */ 98 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 99 | { 100 | if (isset($this->configs[$requestedName])) { 101 | return $this->configs[$requestedName]; 102 | } 103 | 104 | $key = $this->match($requestedName); 105 | if (isset($this->configs[$key])) { 106 | $this->configs[$requestedName] = $this->configs[$key]; 107 | return $this->configs[$key]; 108 | } 109 | 110 | $config = $container->get('Config'); 111 | $this->configs[$requestedName] = $this->configs[$key] = $config[$key]; 112 | return $config[$key]; 113 | } 114 | 115 | /** 116 | * @param string $pattern 117 | * @return self 118 | * @throws Exception\InvalidArgumentException 119 | */ 120 | public function addPattern($pattern) 121 | { 122 | if (! is_string($pattern)) { 123 | throw new Exception\InvalidArgumentException('pattern must be string'); 124 | } 125 | 126 | $patterns = $this->getPatterns(); 127 | array_unshift($patterns, $pattern); 128 | $this->setPatterns($patterns); 129 | return $this; 130 | } 131 | 132 | /** 133 | * @param array|Traversable $patterns 134 | * @return self 135 | * @throws Exception\InvalidArgumentException 136 | */ 137 | public function addPatterns($patterns) 138 | { 139 | if ($patterns instanceof Traversable) { 140 | $patterns = iterator_to_array($patterns); 141 | } 142 | 143 | if (! is_array($patterns)) { 144 | throw new Exception\InvalidArgumentException("patterns must be array or Traversable"); 145 | } 146 | 147 | foreach ($patterns as $pattern) { 148 | $this->addPattern($pattern); 149 | } 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @param array|Traversable $patterns 156 | * @return self 157 | * @throws \InvalidArgumentException 158 | */ 159 | public function setPatterns($patterns) 160 | { 161 | if ($patterns instanceof Traversable) { 162 | $patterns = iterator_to_array($patterns); 163 | } 164 | 165 | if (! is_array($patterns)) { 166 | throw new \InvalidArgumentException("patterns must be array or Traversable"); 167 | } 168 | 169 | $this->patterns = $patterns; 170 | return $this; 171 | } 172 | 173 | /** 174 | * @return array 175 | */ 176 | public function getPatterns() 177 | { 178 | if (null === $this->patterns) { 179 | $this->setPatterns($this->defaultPatterns); 180 | } 181 | return $this->patterns; 182 | } 183 | 184 | /** 185 | * @param string $requestedName 186 | * @return null|string 187 | */ 188 | protected function match($requestedName) 189 | { 190 | foreach ($this->getPatterns() as $pattern) { 191 | if (preg_match($pattern, $requestedName, $matches)) { 192 | return $matches[1]; 193 | } 194 | } 195 | return null; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | allowModifications = (bool) $allowModifications; 58 | 59 | foreach ($array as $key => $value) { 60 | if (is_array($value)) { 61 | $this->data[$key] = new static($value, $this->allowModifications); 62 | } else { 63 | $this->data[$key] = $value; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Retrieve a value and return $default if there is no element set. 70 | * 71 | * @param string $name 72 | * @param mixed $default 73 | * @return mixed 74 | */ 75 | public function get($name, $default = null) 76 | { 77 | if (array_key_exists($name, $this->data)) { 78 | return $this->data[$name]; 79 | } 80 | 81 | return $default; 82 | } 83 | 84 | /** 85 | * Magic function so that $obj->value will work. 86 | * 87 | * @param string $name 88 | * @return mixed 89 | */ 90 | public function __get($name) 91 | { 92 | return $this->get($name); 93 | } 94 | 95 | /** 96 | * Set a value in the config. 97 | * 98 | * Only allow setting of a property if $allowModifications was set to true 99 | * on construction. Otherwise, throw an exception. 100 | * 101 | * @param string $name 102 | * @param mixed $value 103 | * @return void 104 | * @throws Exception\RuntimeException 105 | */ 106 | public function __set($name, $value) 107 | { 108 | if ($this->allowModifications) { 109 | if (is_array($value)) { 110 | $value = new static($value, true); 111 | } 112 | 113 | if (null === $name) { 114 | $this->data[] = $value; 115 | } else { 116 | $this->data[$name] = $value; 117 | } 118 | } else { 119 | throw new Exception\RuntimeException('Config is read only'); 120 | } 121 | } 122 | 123 | /** 124 | * Deep clone of this instance to ensure that nested Zend\Configs are also 125 | * cloned. 126 | * 127 | * @return void 128 | */ 129 | public function __clone() 130 | { 131 | $array = []; 132 | 133 | foreach ($this->data as $key => $value) { 134 | if ($value instanceof self) { 135 | $array[$key] = clone $value; 136 | } else { 137 | $array[$key] = $value; 138 | } 139 | } 140 | 141 | $this->data = $array; 142 | } 143 | 144 | /** 145 | * Return an associative array of the stored data. 146 | * 147 | * @return array 148 | */ 149 | public function toArray() 150 | { 151 | $array = []; 152 | $data = $this->data; 153 | 154 | /** @var self $value */ 155 | foreach ($data as $key => $value) { 156 | if ($value instanceof self) { 157 | $array[$key] = $value->toArray(); 158 | } else { 159 | $array[$key] = $value; 160 | } 161 | } 162 | 163 | return $array; 164 | } 165 | 166 | /** 167 | * isset() overloading 168 | * 169 | * @param string $name 170 | * @return bool 171 | */ 172 | public function __isset($name) 173 | { 174 | return isset($this->data[$name]); 175 | } 176 | 177 | /** 178 | * unset() overloading 179 | * 180 | * @param string $name 181 | * @return void 182 | * @throws Exception\InvalidArgumentException 183 | */ 184 | public function __unset($name) 185 | { 186 | if (! $this->allowModifications) { 187 | throw new Exception\InvalidArgumentException('Config is read only'); 188 | } elseif (isset($this->data[$name])) { 189 | unset($this->data[$name]); 190 | $this->skipNextIteration = true; 191 | } 192 | } 193 | 194 | /** 195 | * count(): defined by Countable interface. 196 | * 197 | * @see Countable::count() 198 | * @return int 199 | */ 200 | public function count() 201 | { 202 | return count($this->data); 203 | } 204 | 205 | /** 206 | * current(): defined by Iterator interface. 207 | * 208 | * @see Iterator::current() 209 | * @return mixed 210 | */ 211 | public function current() 212 | { 213 | $this->skipNextIteration = false; 214 | return current($this->data); 215 | } 216 | 217 | /** 218 | * key(): defined by Iterator interface. 219 | * 220 | * @see Iterator::key() 221 | * @return mixed 222 | */ 223 | public function key() 224 | { 225 | return key($this->data); 226 | } 227 | 228 | /** 229 | * next(): defined by Iterator interface. 230 | * 231 | * @see Iterator::next() 232 | * @return void 233 | */ 234 | public function next() 235 | { 236 | if ($this->skipNextIteration) { 237 | $this->skipNextIteration = false; 238 | return; 239 | } 240 | 241 | next($this->data); 242 | } 243 | 244 | /** 245 | * rewind(): defined by Iterator interface. 246 | * 247 | * @see Iterator::rewind() 248 | * @return void 249 | */ 250 | public function rewind() 251 | { 252 | $this->skipNextIteration = false; 253 | reset($this->data); 254 | } 255 | 256 | /** 257 | * valid(): defined by Iterator interface. 258 | * 259 | * @see Iterator::valid() 260 | * @return bool 261 | */ 262 | public function valid() 263 | { 264 | return ($this->key() !== null); 265 | } 266 | 267 | /** 268 | * offsetExists(): defined by ArrayAccess interface. 269 | * 270 | * @see ArrayAccess::offsetExists() 271 | * @param mixed $offset 272 | * @return bool 273 | */ 274 | public function offsetExists($offset) 275 | { 276 | return $this->__isset($offset); 277 | } 278 | 279 | /** 280 | * offsetGet(): defined by ArrayAccess interface. 281 | * 282 | * @see ArrayAccess::offsetGet() 283 | * @param mixed $offset 284 | * @return mixed 285 | */ 286 | public function offsetGet($offset) 287 | { 288 | return $this->__get($offset); 289 | } 290 | 291 | /** 292 | * offsetSet(): defined by ArrayAccess interface. 293 | * 294 | * @see ArrayAccess::offsetSet() 295 | * @param mixed $offset 296 | * @param mixed $value 297 | * @return void 298 | */ 299 | public function offsetSet($offset, $value) 300 | { 301 | $this->__set($offset, $value); 302 | } 303 | 304 | /** 305 | * offsetUnset(): defined by ArrayAccess interface. 306 | * 307 | * @see ArrayAccess::offsetUnset() 308 | * @param mixed $offset 309 | * @return void 310 | */ 311 | public function offsetUnset($offset) 312 | { 313 | $this->__unset($offset); 314 | } 315 | 316 | /** 317 | * Merge another Config with this one. 318 | * 319 | * For duplicate keys, the following will be performed: 320 | * - Nested Configs will be recursively merged. 321 | * - Items in $merge with INTEGER keys will be appended. 322 | * - Items in $merge with STRING keys will overwrite current values. 323 | * 324 | * @param Config $merge 325 | * @return self 326 | */ 327 | public function merge(Config $merge) 328 | { 329 | /** @var Config $value */ 330 | foreach ($merge as $key => $value) { 331 | if (array_key_exists($key, $this->data)) { 332 | if (is_int($key)) { 333 | $this->data[] = $value; 334 | } elseif ($value instanceof self && $this->data[$key] instanceof self) { 335 | $this->data[$key]->merge($value); 336 | } else { 337 | if ($value instanceof self) { 338 | $this->data[$key] = new static($value->toArray(), $this->allowModifications); 339 | } else { 340 | $this->data[$key] = $value; 341 | } 342 | } 343 | } else { 344 | if ($value instanceof self) { 345 | $this->data[$key] = new static($value->toArray(), $this->allowModifications); 346 | } else { 347 | $this->data[$key] = $value; 348 | } 349 | } 350 | } 351 | 352 | return $this; 353 | } 354 | 355 | /** 356 | * Prevent any more modifications being made to this instance. 357 | * 358 | * Useful after merge() has been used to merge multiple Config objects 359 | * into one object which should then not be modified again. 360 | * 361 | * @return void 362 | */ 363 | public function setReadOnly() 364 | { 365 | $this->allowModifications = false; 366 | 367 | /** @var Config $value */ 368 | foreach ($this->data as $value) { 369 | if ($value instanceof self) { 370 | $value->setReadOnly(); 371 | } 372 | } 373 | } 374 | 375 | /** 376 | * Returns whether this Config object is read only or not. 377 | * 378 | * @return bool 379 | */ 380 | public function isReadOnly() 381 | { 382 | return ! $this->allowModifications; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 'ini', 37 | 'json' => 'json', 38 | 'xml' => 'xml', 39 | 'yaml' => 'yaml', 40 | 'yml' => 'yaml', 41 | 'properties' => 'javaproperties', 42 | ]; 43 | 44 | /** 45 | * Register config file extensions for writing 46 | * key is extension, value is writer instance or plugin name 47 | * 48 | * @var array 49 | */ 50 | protected static $writerExtensions = [ 51 | 'php' => 'php', 52 | 'ini' => 'ini', 53 | 'json' => 'json', 54 | 'xml' => 'xml', 55 | 'yaml' => 'yaml', 56 | 'yml' => 'yaml', 57 | ]; 58 | 59 | /** 60 | * Read a config from a file. 61 | * 62 | * @param string $filename 63 | * @param bool $returnConfigObject 64 | * @param bool $useIncludePath 65 | * @return array|Config 66 | * @throws Exception\InvalidArgumentException 67 | * @throws Exception\RuntimeException 68 | */ 69 | public static function fromFile($filename, $returnConfigObject = false, $useIncludePath = false) 70 | { 71 | $filepath = $filename; 72 | if (! file_exists($filename)) { 73 | if (! $useIncludePath) { 74 | throw new Exception\RuntimeException(sprintf( 75 | 'Filename "%s" cannot be found relative to the working directory', 76 | $filename 77 | )); 78 | } 79 | 80 | $fromIncludePath = stream_resolve_include_path($filename); 81 | if (! $fromIncludePath) { 82 | throw new Exception\RuntimeException(sprintf( 83 | 'Filename "%s" cannot be found relative to the working directory or the include_path ("%s")', 84 | $filename, 85 | get_include_path() 86 | )); 87 | } 88 | $filepath = $fromIncludePath; 89 | } 90 | 91 | $pathinfo = pathinfo($filepath); 92 | 93 | if (! isset($pathinfo['extension'])) { 94 | throw new Exception\RuntimeException(sprintf( 95 | 'Filename "%s" is missing an extension and cannot be auto-detected', 96 | $filename 97 | )); 98 | } 99 | 100 | $extension = strtolower($pathinfo['extension']); 101 | 102 | if ($extension === 'php') { 103 | if (! is_file($filepath) || ! is_readable($filepath)) { 104 | throw new Exception\RuntimeException(sprintf( 105 | "File '%s' doesn't exist or not readable", 106 | $filename 107 | )); 108 | } 109 | 110 | $config = include $filepath; 111 | } elseif (isset(static::$extensions[$extension])) { 112 | $reader = static::$extensions[$extension]; 113 | if (! $reader instanceof Reader\ReaderInterface) { 114 | $reader = static::getReaderPluginManager()->get($reader); 115 | static::$extensions[$extension] = $reader; 116 | } 117 | 118 | /* @var Reader\ReaderInterface $reader */ 119 | $config = $reader->fromFile($filepath); 120 | } else { 121 | throw new Exception\RuntimeException(sprintf( 122 | 'Unsupported config file extension: .%s', 123 | $pathinfo['extension'] 124 | )); 125 | } 126 | 127 | return ($returnConfigObject) ? new Config($config) : $config; 128 | } 129 | 130 | /** 131 | * Read configuration from multiple files and merge them. 132 | * 133 | * @param array $files 134 | * @param bool $returnConfigObject 135 | * @param bool $useIncludePath 136 | * @return array|Config 137 | */ 138 | public static function fromFiles(array $files, $returnConfigObject = false, $useIncludePath = false) 139 | { 140 | $config = []; 141 | 142 | foreach ($files as $file) { 143 | $config = ArrayUtils::merge($config, static::fromFile($file, false, $useIncludePath)); 144 | } 145 | 146 | return ($returnConfigObject) ? new Config($config) : $config; 147 | } 148 | 149 | /** 150 | * Writes a config to a file 151 | * 152 | * @param string $filename 153 | * @param array|Config $config 154 | * @return bool TRUE on success | FALSE on failure 155 | * @throws Exception\RuntimeException 156 | * @throws Exception\InvalidArgumentException 157 | */ 158 | public static function toFile($filename, $config) 159 | { 160 | if ((is_object($config) && ! ($config instanceof Config)) 161 | || (! is_object($config) && ! is_array($config)) 162 | ) { 163 | throw new Exception\InvalidArgumentException( 164 | __METHOD__." \$config should be an array or instance of Zend\\Config\\Config" 165 | ); 166 | } 167 | 168 | $extension = substr(strrchr($filename, '.'), 1); 169 | $directory = dirname($filename); 170 | 171 | if (! is_dir($directory)) { 172 | throw new Exception\RuntimeException( 173 | "Directory '{$directory}' does not exists!" 174 | ); 175 | } 176 | 177 | if (! is_writable($directory)) { 178 | throw new Exception\RuntimeException( 179 | "Cannot write in directory '{$directory}'" 180 | ); 181 | } 182 | 183 | if (! isset(static::$writerExtensions[$extension])) { 184 | throw new Exception\RuntimeException( 185 | "Unsupported config file extension: '.{$extension}' for writing." 186 | ); 187 | } 188 | 189 | $writer = static::$writerExtensions[$extension]; 190 | if (($writer instanceof Writer\AbstractWriter) === false) { 191 | $writer = self::getWriterPluginManager()->get($writer); 192 | static::$writerExtensions[$extension] = $writer; 193 | } 194 | 195 | if (is_object($config)) { 196 | $config = $config->toArray(); 197 | } 198 | 199 | $content = $writer->processConfig($config); 200 | 201 | return (bool) (file_put_contents($filename, $content) !== false); 202 | } 203 | 204 | /** 205 | * Set reader plugin manager 206 | * 207 | * @param ContainerInterface $readers 208 | * @return void 209 | */ 210 | public static function setReaderPluginManager(ContainerInterface $readers) 211 | { 212 | static::$readers = $readers; 213 | } 214 | 215 | /** 216 | * Get the reader plugin manager. 217 | * 218 | * If none is available, registers and returns a 219 | * StandaloneReaderPluginManager instance by default. 220 | * 221 | * @return ContainerInterface 222 | */ 223 | public static function getReaderPluginManager() 224 | { 225 | if (static::$readers === null) { 226 | static::$readers = new StandaloneReaderPluginManager(); 227 | } 228 | return static::$readers; 229 | } 230 | 231 | /** 232 | * Set writer plugin manager 233 | * 234 | * @param ContainerInterface $writers 235 | * @return void 236 | */ 237 | public static function setWriterPluginManager(ContainerInterface $writers) 238 | { 239 | static::$writers = $writers; 240 | } 241 | 242 | /** 243 | * Get the writer plugin manager. 244 | * 245 | * If none is available, registers and returns a 246 | * StandaloneWriterPluginManager instance by default. 247 | * 248 | * @return ContainerInterface 249 | */ 250 | public static function getWriterPluginManager() 251 | { 252 | if (static::$writers === null) { 253 | static::$writers = new StandaloneWriterPluginManager(); 254 | } 255 | 256 | return static::$writers; 257 | } 258 | 259 | /** 260 | * Set config reader for file extension 261 | * 262 | * @param string $extension 263 | * @param string|Reader\ReaderInterface $reader 264 | * @throws Exception\InvalidArgumentException 265 | * @return void 266 | */ 267 | public static function registerReader($extension, $reader) 268 | { 269 | $extension = strtolower($extension); 270 | 271 | if (! is_string($reader) && ! $reader instanceof Reader\ReaderInterface) { 272 | throw new Exception\InvalidArgumentException(sprintf( 273 | 'Reader should be plugin name, class name or ' . 274 | 'instance of %s\Reader\ReaderInterface; received "%s"', 275 | __NAMESPACE__, 276 | (is_object($reader) ? get_class($reader) : gettype($reader)) 277 | )); 278 | } 279 | 280 | static::$extensions[$extension] = $reader; 281 | } 282 | 283 | /** 284 | * Set config writer for file extension 285 | * 286 | * @param string $extension 287 | * @param string|Writer\AbstractWriter $writer 288 | * @throws Exception\InvalidArgumentException 289 | * @return void 290 | */ 291 | public static function registerWriter($extension, $writer) 292 | { 293 | $extension = strtolower($extension); 294 | 295 | if (! is_string($writer) && ! $writer instanceof Writer\AbstractWriter) { 296 | throw new Exception\InvalidArgumentException(sprintf( 297 | 'Writer should be plugin name, class name or ' . 298 | 'instance of %s\Writer\AbstractWriter; received "%s"', 299 | __NAMESPACE__, 300 | (is_object($writer) ? get_class($writer) : gettype($writer)) 301 | )); 302 | } 303 | 304 | static::$writerExtensions[$extension] = $writer; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Processor/Constant.php: -------------------------------------------------------------------------------- 1 | setUserOnly((bool) $userOnly); 34 | $this->setPrefix((string) $prefix); 35 | $this->setSuffix((string) $suffix); 36 | 37 | if (true === $enableKeyProcessing) { 38 | $this->enableKeyProcessing(); 39 | } 40 | 41 | $this->loadConstants(); 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function getUserOnly() 48 | { 49 | return $this->userOnly; 50 | } 51 | 52 | /** 53 | * Should we use only user-defined constants? 54 | * 55 | * @param bool $userOnly 56 | * @return self 57 | */ 58 | public function setUserOnly($userOnly) 59 | { 60 | $this->userOnly = (bool) $userOnly; 61 | return $this; 62 | } 63 | 64 | /** 65 | * Load all currently defined constants into parser. 66 | * 67 | * @return void 68 | */ 69 | public function loadConstants() 70 | { 71 | if ($this->userOnly) { 72 | $constants = get_defined_constants(true); 73 | $constants = isset($constants['user']) ? $constants['user'] : []; 74 | $this->setTokens($constants); 75 | } else { 76 | $this->setTokens(get_defined_constants()); 77 | } 78 | } 79 | 80 | /** 81 | * Get current token registry. 82 | * @return array 83 | */ 84 | public function getTokens() 85 | { 86 | return $this->tokens; 87 | } 88 | 89 | /** 90 | * Override processing of individual value. 91 | * 92 | * If the value is a string and evaluates to a class constant, returns 93 | * the class constant value; otherwise, delegates to the parent. 94 | * 95 | * @param mixed $value 96 | * @param array $replacements 97 | * @return mixed 98 | */ 99 | protected function doProcess($value, array $replacements) 100 | { 101 | if (! is_string($value)) { 102 | return parent::doProcess($value, $replacements); 103 | } 104 | 105 | if (false === strpos($value, '::')) { 106 | return parent::doProcess($value, $replacements); 107 | } 108 | 109 | // Handle class constants 110 | if (defined($value)) { 111 | return constant($value); 112 | } 113 | 114 | // Handle ::class notation 115 | if (! preg_match('/::class$/i', $value)) { 116 | return parent::doProcess($value, $replacements); 117 | } 118 | 119 | $class = substr($value, 0, -7); 120 | if (class_exists($class)) { 121 | return $class; 122 | } 123 | 124 | // While we've matched ::class, the class does not exist, and PHP will 125 | // raise an error if you try to define a constant using that notation. 126 | // As such, we have something that cannot possibly be a constant, so we 127 | // can safely return the value verbatim. 128 | return $value; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Processor/Filter.php: -------------------------------------------------------------------------------- 1 | setFilter($filter); 29 | } 30 | 31 | /** 32 | * @param ZendFilter $filter 33 | * @return self 34 | */ 35 | public function setFilter(ZendFilter $filter) 36 | { 37 | $this->filter = $filter; 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return ZendFilter 43 | */ 44 | public function getFilter() 45 | { 46 | return $this->filter; 47 | } 48 | 49 | /** 50 | * Process 51 | * 52 | * @param Config $config 53 | * @return Config 54 | * @throws Exception\InvalidArgumentException 55 | */ 56 | public function process(Config $config) 57 | { 58 | if ($config->isReadOnly()) { 59 | throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); 60 | } 61 | 62 | /** 63 | * Walk through config and replace values 64 | */ 65 | foreach ($config as $key => $val) { 66 | if ($val instanceof Config) { 67 | $this->process($val); 68 | } else { 69 | $config->$key = $this->filter->filter($val); 70 | } 71 | } 72 | 73 | return $config; 74 | } 75 | 76 | /** 77 | * Process a single value 78 | * 79 | * @param mixed $value 80 | * @return mixed 81 | */ 82 | public function processValue($value) 83 | { 84 | return $this->filter->filter($value); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Processor/ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | isReadOnly()) { 26 | throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); 27 | } 28 | 29 | foreach ($this as $parser) { 30 | /** @var $parser ProcessorInterface */ 31 | $parser->process($config); 32 | } 33 | } 34 | 35 | /** 36 | * Process a single value 37 | * 38 | * @param mixed $value 39 | * @return mixed 40 | */ 41 | public function processValue($value) 42 | { 43 | foreach ($this as $parser) { 44 | /** @var $parser ProcessorInterface */ 45 | $value = $parser->processValue($value); 46 | } 47 | 48 | return $value; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Processor/Token.php: -------------------------------------------------------------------------------- 1 | 56 | * value to replace it with 57 | * @param string $prefix 58 | * @param string $suffix 59 | * @param bool $enableKeyProcessing Whether or not to enable processing of 60 | * token values in configuration keys; defaults to false. 61 | */ 62 | public function __construct($tokens = [], $prefix = '', $suffix = '', $enableKeyProcessing = false) 63 | { 64 | $this->setTokens($tokens); 65 | $this->setPrefix((string) $prefix); 66 | $this->setSuffix((string) $suffix); 67 | if (true === $enableKeyProcessing) { 68 | $this->enableKeyProcessing(); 69 | } 70 | } 71 | 72 | /** 73 | * @param string $prefix 74 | * @return self 75 | */ 76 | public function setPrefix($prefix) 77 | { 78 | // reset map 79 | $this->map = null; 80 | $this->prefix = $prefix; 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getPrefix() 88 | { 89 | return $this->prefix; 90 | } 91 | 92 | /** 93 | * @param string $suffix 94 | * @return self 95 | */ 96 | public function setSuffix($suffix) 97 | { 98 | // reset map 99 | $this->map = null; 100 | $this->suffix = $suffix; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function getSuffix() 109 | { 110 | return $this->suffix; 111 | } 112 | 113 | /** 114 | * Set token registry. 115 | * 116 | * @param array|Config|Traversable $tokens Associative array of TOKEN => 117 | * value to replace it with 118 | * @return self 119 | * @throws Exception\InvalidArgumentException 120 | */ 121 | public function setTokens($tokens) 122 | { 123 | if (is_array($tokens)) { 124 | $this->tokens = $tokens; 125 | } elseif ($tokens instanceof Config) { 126 | $this->tokens = $tokens->toArray(); 127 | } elseif ($tokens instanceof Traversable) { 128 | $this->tokens = []; 129 | foreach ($tokens as $key => $val) { 130 | $this->tokens[$key] = $val; 131 | } 132 | } else { 133 | throw new Exception\InvalidArgumentException('Cannot use ' . gettype($tokens) . ' as token registry.'); 134 | } 135 | 136 | // reset map 137 | $this->map = null; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Get current token registry. 144 | * 145 | * @return array 146 | */ 147 | public function getTokens() 148 | { 149 | return $this->tokens; 150 | } 151 | 152 | /** 153 | * Add new token. 154 | * 155 | * @param string $token 156 | * @param mixed $value 157 | * @return self 158 | * @throws Exception\InvalidArgumentException 159 | */ 160 | public function addToken($token, $value) 161 | { 162 | if (! is_scalar($token)) { 163 | throw new Exception\InvalidArgumentException('Cannot use ' . gettype($token) . ' as token name.'); 164 | } 165 | $this->tokens[$token] = $value; 166 | 167 | // reset map 168 | $this->map = null; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Add new token. 175 | * 176 | * @param string $token 177 | * @param mixed $value 178 | * @return self 179 | */ 180 | public function setToken($token, $value) 181 | { 182 | return $this->addToken($token, $value); 183 | } 184 | 185 | /** 186 | * Enable processing keys as well as values. 187 | * 188 | * @return void 189 | */ 190 | public function enableKeyProcessing() 191 | { 192 | $this->processKeys = true; 193 | } 194 | 195 | /** 196 | * Build replacement map 197 | * 198 | * @return array 199 | */ 200 | protected function buildMap() 201 | { 202 | if (null === $this->map) { 203 | if (! $this->suffix && ! $this->prefix) { 204 | $this->map = $this->tokens; 205 | } else { 206 | $this->map = []; 207 | 208 | foreach ($this->tokens as $token => $value) { 209 | $this->map[$this->prefix . $token . $this->suffix] = $value; 210 | } 211 | } 212 | 213 | foreach (array_keys($this->map) as $key) { 214 | if (empty($key)) { 215 | unset($this->map[$key]); 216 | } 217 | } 218 | } 219 | 220 | return $this->map; 221 | } 222 | 223 | /** 224 | * Process 225 | * 226 | * @param Config $config 227 | * @return Config 228 | * @throws Exception\InvalidArgumentException 229 | */ 230 | public function process(Config $config) 231 | { 232 | return $this->doProcess($config, $this->buildMap()); 233 | } 234 | 235 | /** 236 | * Process a single value 237 | * 238 | * @param $value 239 | * @return mixed 240 | */ 241 | public function processValue($value) 242 | { 243 | return $this->doProcess($value, $this->buildMap()); 244 | } 245 | 246 | /** 247 | * Applies replacement map to the given value by modifying the value itself 248 | * 249 | * @param mixed $value 250 | * @param array $replacements 251 | * 252 | * @return mixed 253 | * 254 | * @throws Exception\InvalidArgumentException if the provided value is a read-only {@see Config} 255 | */ 256 | protected function doProcess($value, array $replacements) 257 | { 258 | if ($value instanceof Config) { 259 | if ($value->isReadOnly()) { 260 | throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); 261 | } 262 | 263 | foreach ($value as $key => $val) { 264 | $newKey = $this->processKeys ? $this->doProcess($key, $replacements) : $key; 265 | $value->$newKey = $this->doProcess($val, $replacements); 266 | 267 | // If the processed key differs from the original, remove the original 268 | if ($newKey !== $key) { 269 | unset($value->$key); 270 | } 271 | } 272 | 273 | return $value; 274 | } 275 | 276 | if ($value instanceof Traversable || is_array($value)) { 277 | foreach ($value as & $val) { 278 | $val = $this->doProcess($val, $replacements); 279 | } 280 | 281 | return $value; 282 | } 283 | 284 | if (! is_string($value) && (is_bool($value) || is_numeric($value))) { 285 | $stringVal = (string) $value; 286 | $changedVal = strtr($stringVal, $this->map); 287 | 288 | // replace the value only if a string replacement occurred 289 | if ($changedVal !== $stringVal) { 290 | return $changedVal; 291 | } 292 | 293 | return $value; 294 | } 295 | 296 | return strtr((string) $value, $this->map); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Processor/Translator.php: -------------------------------------------------------------------------------- 1 | setTranslator($translator); 42 | $this->setTextDomain($textDomain); 43 | $this->setLocale($locale); 44 | } 45 | 46 | /** 47 | * @param ZendTranslator $translator 48 | * @return self 49 | */ 50 | public function setTranslator(ZendTranslator $translator) 51 | { 52 | $this->translator = $translator; 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return ZendTranslator 58 | */ 59 | public function getTranslator() 60 | { 61 | return $this->translator; 62 | } 63 | 64 | /** 65 | * @param string|null $locale 66 | * @return self 67 | */ 68 | public function setLocale($locale) 69 | { 70 | $this->locale = $locale; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return string|null 76 | */ 77 | public function getLocale() 78 | { 79 | return $this->locale; 80 | } 81 | 82 | /** 83 | * @param string $textDomain 84 | * @return self 85 | */ 86 | public function setTextDomain($textDomain) 87 | { 88 | $this->textDomain = $textDomain; 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function getTextDomain() 96 | { 97 | return $this->textDomain; 98 | } 99 | 100 | /** 101 | * Process 102 | * 103 | * @param Config $config 104 | * @return Config 105 | * @throws Exception\InvalidArgumentException 106 | */ 107 | public function process(Config $config) 108 | { 109 | if ($config->isReadOnly()) { 110 | throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); 111 | } 112 | 113 | /** 114 | * Walk through config and replace values 115 | */ 116 | foreach ($config as $key => $val) { 117 | if ($val instanceof Config) { 118 | $this->process($val); 119 | } else { 120 | $config->{$key} = $this->translator->translate($val, $this->textDomain, $this->locale); 121 | } 122 | } 123 | 124 | return $config; 125 | } 126 | 127 | /** 128 | * Process a single value 129 | * 130 | * @param $value 131 | * @return string 132 | */ 133 | public function processValue($value) 134 | { 135 | return $this->translator->translate($value, $this->textDomain, $this->locale); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Reader/Ini.php: -------------------------------------------------------------------------------- 1 | nestSeparator = $separator; 48 | return $this; 49 | } 50 | 51 | /** 52 | * Get nest separator. 53 | * 54 | * @return string 55 | */ 56 | public function getNestSeparator() 57 | { 58 | return $this->nestSeparator; 59 | } 60 | 61 | /** 62 | * Marks whether sections should be processed. 63 | * When sections are not processed,section names are stripped and section 64 | * values are merged 65 | * 66 | * @see https://www.php.net/parse_ini_file 67 | * @param bool $processSections 68 | * @return $this 69 | */ 70 | public function setProcessSections($processSections) 71 | { 72 | $this->processSections = (bool) $processSections; 73 | return $this; 74 | } 75 | 76 | /** 77 | * Get if sections should be processed 78 | * When sections are not processed,section names are stripped and section 79 | * values are merged 80 | * 81 | * @see https://www.php.net/parse_ini_file 82 | * @return bool 83 | */ 84 | public function getProcessSections() 85 | { 86 | return $this->processSections; 87 | } 88 | 89 | /** 90 | * fromFile(): defined by Reader interface. 91 | * 92 | * @see ReaderInterface::fromFile() 93 | * @param string $filename 94 | * @return array 95 | * @throws Exception\RuntimeException 96 | */ 97 | public function fromFile($filename) 98 | { 99 | if (! is_file($filename) || ! is_readable($filename)) { 100 | throw new Exception\RuntimeException(sprintf( 101 | "File '%s' doesn't exist or not readable", 102 | $filename 103 | )); 104 | } 105 | 106 | $this->directory = dirname($filename); 107 | 108 | set_error_handler( 109 | function ($error, $message = '') use ($filename) { 110 | throw new Exception\RuntimeException( 111 | sprintf('Error reading INI file "%s": %s', $filename, $message), 112 | $error 113 | ); 114 | }, 115 | E_WARNING 116 | ); 117 | $ini = parse_ini_file($filename, $this->getProcessSections()); 118 | restore_error_handler(); 119 | 120 | return $this->process($ini); 121 | } 122 | 123 | /** 124 | * fromString(): defined by Reader interface. 125 | * 126 | * @param string $string 127 | * @return array|bool 128 | * @throws Exception\RuntimeException 129 | */ 130 | public function fromString($string) 131 | { 132 | if (empty($string)) { 133 | return []; 134 | } 135 | $this->directory = null; 136 | 137 | set_error_handler( 138 | function ($error, $message = '') { 139 | throw new Exception\RuntimeException( 140 | sprintf('Error reading INI string: %s', $message), 141 | $error 142 | ); 143 | }, 144 | E_WARNING 145 | ); 146 | $ini = parse_ini_string($string, $this->getProcessSections()); 147 | restore_error_handler(); 148 | 149 | return $this->process($ini); 150 | } 151 | 152 | /** 153 | * Process data from the parsed ini file. 154 | * 155 | * @param array $data 156 | * @return array 157 | */ 158 | protected function process(array $data) 159 | { 160 | $config = []; 161 | 162 | foreach ($data as $section => $value) { 163 | if (is_array($value)) { 164 | if (strpos($section, $this->nestSeparator) !== false) { 165 | $sections = explode($this->nestSeparator, $section); 166 | $config = array_merge_recursive($config, $this->buildNestedSection($sections, $value)); 167 | } else { 168 | $config[$section] = $this->processSection($value); 169 | } 170 | } else { 171 | $this->processKey($section, $value, $config); 172 | } 173 | } 174 | 175 | return $config; 176 | } 177 | 178 | /** 179 | * Process a nested section 180 | * 181 | * @param array $sections 182 | * @param mixed $value 183 | * @return array 184 | */ 185 | private function buildNestedSection($sections, $value) 186 | { 187 | if (! $sections) { 188 | return $this->processSection($value); 189 | } 190 | 191 | $nestedSection = []; 192 | 193 | $first = array_shift($sections); 194 | $nestedSection[$first] = $this->buildNestedSection($sections, $value); 195 | 196 | return $nestedSection; 197 | } 198 | 199 | /** 200 | * Process a section. 201 | * 202 | * @param array $section 203 | * @return array 204 | */ 205 | protected function processSection(array $section) 206 | { 207 | $config = []; 208 | 209 | foreach ($section as $key => $value) { 210 | $this->processKey($key, $value, $config); 211 | } 212 | 213 | return $config; 214 | } 215 | 216 | /** 217 | * Process a key. 218 | * 219 | * @param string $key 220 | * @param string $value 221 | * @param array $config 222 | * @return array 223 | * @throws Exception\RuntimeException 224 | */ 225 | protected function processKey($key, $value, array &$config) 226 | { 227 | if (strpos($key, $this->nestSeparator) !== false) { 228 | $pieces = explode($this->nestSeparator, $key, 2); 229 | 230 | if ($pieces[0] === '' || $pieces[1] === '') { 231 | throw new Exception\RuntimeException(sprintf('Invalid key "%s"', $key)); 232 | } 233 | 234 | if (! isset($config[$pieces[0]])) { 235 | if ($pieces[0] === '0' && ! empty($config)) { 236 | $config = [$pieces[0] => $config]; 237 | } else { 238 | $config[$pieces[0]] = []; 239 | } 240 | } elseif (! is_array($config[$pieces[0]])) { 241 | throw new Exception\RuntimeException( 242 | sprintf('Cannot create sub-key for "%s", as key already exists', $pieces[0]) 243 | ); 244 | } 245 | 246 | $this->processKey($pieces[1], $value, $config[$pieces[0]]); 247 | } else { 248 | if ($key === '@include') { 249 | if ($this->directory === null) { 250 | throw new Exception\RuntimeException('Cannot process @include statement for a string config'); 251 | } 252 | 253 | $reader = clone $this; 254 | $include = $reader->fromFile($this->directory . '/' . $value); 255 | $config = array_replace_recursive($config, $include); 256 | } else { 257 | $config[$key] = $value; 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Reader/JavaProperties.php: -------------------------------------------------------------------------------- 1 | delimiter = $delimiter; 56 | $this->trimWhitespace = (bool) $trimWhitespace; 57 | } 58 | 59 | /** 60 | * fromFile(): defined by Reader interface. 61 | * 62 | * @see ReaderInterface::fromFile() 63 | * @param string $filename 64 | * @return array 65 | * @throws Exception\RuntimeException if the file cannot be read 66 | */ 67 | public function fromFile($filename) 68 | { 69 | if (! is_file($filename) || ! is_readable($filename)) { 70 | throw new Exception\RuntimeException(sprintf( 71 | "File '%s' doesn't exist or not readable", 72 | $filename 73 | )); 74 | } 75 | 76 | $this->directory = dirname($filename); 77 | 78 | $config = $this->parse(file_get_contents($filename)); 79 | 80 | return $this->process($config); 81 | } 82 | 83 | /** 84 | * fromString(): defined by Reader interface. 85 | * 86 | * @see ReaderInterface::fromString() 87 | * @param string $string 88 | * @return array 89 | * @throws Exception\RuntimeException if an @include key is found 90 | */ 91 | public function fromString($string) 92 | { 93 | if (empty($string)) { 94 | return []; 95 | } 96 | 97 | $this->directory = null; 98 | 99 | $config = $this->parse($string); 100 | 101 | return $this->process($config); 102 | } 103 | 104 | /** 105 | * Process the array for @include 106 | * 107 | * @param array $data 108 | * @return array 109 | * @throws Exception\RuntimeException if an @include key is found 110 | */ 111 | protected function process(array $data) 112 | { 113 | foreach ($data as $key => $value) { 114 | if (trim($key) === '@include') { 115 | if ($this->directory === null) { 116 | throw new Exception\RuntimeException('Cannot process @include statement for a string'); 117 | } 118 | $reader = clone $this; 119 | unset($data[$key]); 120 | $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); 121 | } 122 | } 123 | return $data; 124 | } 125 | 126 | /** 127 | * Parse Java-style properties string 128 | * 129 | * @todo Support use of the equals sign "key=value" as key-value delimiter 130 | * @todo Ignore whitespace that precedes text past the first line of multiline values 131 | * 132 | * @param string $string 133 | * @return array 134 | */ 135 | protected function parse($string) 136 | { 137 | $delimiter = $this->delimiter; 138 | $delimLength = strlen($delimiter); 139 | $result = []; 140 | $lines = explode("\n", $string); 141 | $key = ''; 142 | $isWaitingOtherLine = false; 143 | foreach ($lines as $i => $line) { 144 | // Ignore empty lines and commented lines 145 | if (empty($line) 146 | || (! $isWaitingOtherLine && strpos($line, "#") === 0) 147 | || (! $isWaitingOtherLine && strpos($line, "!") === 0) 148 | ) { 149 | continue; 150 | } 151 | 152 | // Add a new key-value pair or append value to a previous pair 153 | if (! $isWaitingOtherLine) { 154 | $key = substr($line, 0, strpos($line, $delimiter)); 155 | $value = substr($line, strpos($line, $delimiter) + $delimLength, strlen($line)); 156 | } else { 157 | $value .= $line; 158 | } 159 | 160 | // Check if ends with single '\' (indicating another line is expected) 161 | if (strrpos($value, "\\") === strlen($value) - strlen("\\")) { 162 | $value = substr($value, 0, -1); 163 | $isWaitingOtherLine = true; 164 | } else { 165 | $isWaitingOtherLine = false; 166 | } 167 | 168 | $key = $this->trimWhitespace ? trim($key) : $key; 169 | $value = $this->trimWhitespace && ! $isWaitingOtherLine 170 | ? trim($value) 171 | : $value; 172 | 173 | $result[$key] = stripslashes($value); 174 | unset($lines[$i]); 175 | } 176 | 177 | return $result; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Reader/Json.php: -------------------------------------------------------------------------------- 1 | directory = dirname($filename); 42 | 43 | $config = $this->decode(file_get_contents($filename)); 44 | 45 | return $this->process($config); 46 | } 47 | 48 | /** 49 | * fromString(): defined by Reader interface. 50 | * 51 | * @see ReaderInterface::fromString() 52 | * @param string $string 53 | * @return array|bool 54 | * @throws Exception\RuntimeException 55 | */ 56 | public function fromString($string) 57 | { 58 | if (empty($string)) { 59 | return []; 60 | } 61 | 62 | $this->directory = null; 63 | 64 | $config = $this->decode($string); 65 | 66 | return $this->process($config); 67 | } 68 | 69 | /** 70 | * Process the array for @include 71 | * 72 | * @param array $data 73 | * @return array 74 | * @throws Exception\RuntimeException 75 | */ 76 | protected function process(array $data) 77 | { 78 | foreach ($data as $key => $value) { 79 | if (is_array($value)) { 80 | $data[$key] = $this->process($value); 81 | } 82 | if (trim($key) === '@include') { 83 | if ($this->directory === null) { 84 | throw new Exception\RuntimeException('Cannot process @include statement for a JSON string'); 85 | } 86 | $reader = clone $this; 87 | unset($data[$key]); 88 | $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); 89 | } 90 | } 91 | return $data; 92 | } 93 | 94 | /** 95 | * Decode JSON configuration. 96 | * 97 | * Determines if ext/json is present, and, if so, uses that to decode the 98 | * configuration. Otherwise, it uses zend-json, and, if that is missing, 99 | * raises an exception indicating inability to decode. 100 | * 101 | * @param string $data 102 | * @return array 103 | * @throws Exception\RuntimeException for any decoding errors. 104 | */ 105 | private function decode($data) 106 | { 107 | $config = json_decode($data, true); 108 | 109 | if (null !== $config && ! is_array($config)) { 110 | throw new Exception\RuntimeException( 111 | 'Invalid JSON configuration; did not return an array or object' 112 | ); 113 | } 114 | 115 | if (null !== $config) { 116 | return $config; 117 | } 118 | 119 | if (JSON_ERROR_NONE === json_last_error()) { 120 | return $config; 121 | } 122 | 123 | throw new Exception\RuntimeException(json_last_error_msg()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Reader/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | reader = new XMLReader(); 61 | $this->reader->open($filename, null, LIBXML_XINCLUDE); 62 | 63 | $this->directory = dirname($filename); 64 | 65 | set_error_handler( 66 | function ($error, $message = '') use ($filename) { 67 | throw new Exception\RuntimeException( 68 | sprintf('Error reading XML file "%s": %s', $filename, $message), 69 | $error 70 | ); 71 | }, 72 | E_WARNING 73 | ); 74 | $return = $this->process(); 75 | restore_error_handler(); 76 | $this->reader->close(); 77 | 78 | return $return; 79 | } 80 | 81 | /** 82 | * fromString(): defined by Reader interface. 83 | * 84 | * @see ReaderInterface::fromString() 85 | * @param string $string 86 | * @return array|bool 87 | * @throws Exception\RuntimeException 88 | */ 89 | public function fromString($string) 90 | { 91 | if (empty($string)) { 92 | return []; 93 | } 94 | $this->reader = new XMLReader(); 95 | 96 | $this->reader->XML($string, null, LIBXML_XINCLUDE); 97 | 98 | $this->directory = null; 99 | 100 | set_error_handler( 101 | function ($error, $message = '') { 102 | throw new Exception\RuntimeException( 103 | sprintf('Error reading XML string: %s', $message), 104 | $error 105 | ); 106 | }, 107 | E_WARNING 108 | ); 109 | $return = $this->process(); 110 | restore_error_handler(); 111 | $this->reader->close(); 112 | 113 | return $return; 114 | } 115 | 116 | /** 117 | * Process data from the created XMLReader. 118 | * 119 | * @return array 120 | */ 121 | protected function process() 122 | { 123 | return $this->processNextElement(); 124 | } 125 | 126 | /** 127 | * Process the next inner element. 128 | * 129 | * @return mixed 130 | */ 131 | protected function processNextElement() 132 | { 133 | $children = []; 134 | $text = ''; 135 | 136 | while ($this->reader->read()) { 137 | if ($this->reader->nodeType === XMLReader::ELEMENT) { 138 | if ($this->reader->depth === 0) { 139 | return $this->processNextElement(); 140 | } 141 | 142 | $attributes = $this->getAttributes(); 143 | $name = $this->reader->name; 144 | 145 | if ($this->reader->isEmptyElement) { 146 | $child = []; 147 | } else { 148 | $child = $this->processNextElement(); 149 | } 150 | 151 | if ($attributes) { 152 | if (is_string($child)) { 153 | $child = ['_' => $child]; 154 | } 155 | 156 | if (! is_array($child)) { 157 | $child = []; 158 | } 159 | 160 | $child = array_merge($child, $attributes); 161 | } 162 | 163 | if (isset($children[$name])) { 164 | if (! is_array($children[$name]) || ! array_key_exists(0, $children[$name])) { 165 | $children[$name] = [$children[$name]]; 166 | } 167 | 168 | $children[$name][] = $child; 169 | } else { 170 | $children[$name] = $child; 171 | } 172 | } elseif ($this->reader->nodeType === XMLReader::END_ELEMENT) { 173 | break; 174 | } elseif (in_array($this->reader->nodeType, $this->textNodes)) { 175 | $text .= $this->reader->value; 176 | } 177 | } 178 | 179 | return $children ?: $text; 180 | } 181 | 182 | /** 183 | * Get all attributes on the current node. 184 | * 185 | * @return array 186 | */ 187 | protected function getAttributes() 188 | { 189 | $attributes = []; 190 | 191 | if ($this->reader->hasAttributes) { 192 | while ($this->reader->moveToNextAttribute()) { 193 | $attributes[$this->reader->localName] = $this->reader->value; 194 | } 195 | 196 | $this->reader->moveToElement(); 197 | } 198 | 199 | return $attributes; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Reader/Yaml.php: -------------------------------------------------------------------------------- 1 | setYamlDecoder($yamlDecoder); 40 | } else { 41 | if (function_exists('yaml_parse')) { 42 | $this->setYamlDecoder('yaml_parse'); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Set callback for decoding YAML 49 | * 50 | * @param string|callable $yamlDecoder the decoder to set 51 | * @return self 52 | * @throws Exception\RuntimeException 53 | */ 54 | public function setYamlDecoder($yamlDecoder) 55 | { 56 | if (! is_callable($yamlDecoder)) { 57 | throw new Exception\RuntimeException( 58 | 'Invalid parameter to setYamlDecoder() - must be callable' 59 | ); 60 | } 61 | $this->yamlDecoder = $yamlDecoder; 62 | return $this; 63 | } 64 | 65 | /** 66 | * Get callback for decoding YAML 67 | * 68 | * @return callable 69 | */ 70 | public function getYamlDecoder() 71 | { 72 | return $this->yamlDecoder; 73 | } 74 | 75 | /** 76 | * fromFile(): defined by Reader interface. 77 | * 78 | * @see ReaderInterface::fromFile() 79 | * @param string $filename 80 | * @return array 81 | * @throws Exception\RuntimeException 82 | */ 83 | public function fromFile($filename) 84 | { 85 | if (! is_file($filename) || ! is_readable($filename)) { 86 | throw new Exception\RuntimeException(sprintf( 87 | "File '%s' doesn't exist or not readable", 88 | $filename 89 | )); 90 | } 91 | 92 | if (null === $this->getYamlDecoder()) { 93 | throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); 94 | } 95 | 96 | $this->directory = dirname($filename); 97 | 98 | $config = call_user_func($this->getYamlDecoder(), file_get_contents($filename)); 99 | if (null === $config) { 100 | throw new Exception\RuntimeException("Error parsing YAML data"); 101 | } 102 | 103 | return $this->process($config); 104 | } 105 | 106 | /** 107 | * fromString(): defined by Reader interface. 108 | * 109 | * @see ReaderInterface::fromString() 110 | * @param string $string 111 | * @return array|bool 112 | * @throws Exception\RuntimeException 113 | */ 114 | public function fromString($string) 115 | { 116 | if (null === $this->getYamlDecoder()) { 117 | throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); 118 | } 119 | if (empty($string)) { 120 | return []; 121 | } 122 | 123 | $this->directory = null; 124 | 125 | $config = call_user_func($this->getYamlDecoder(), $string); 126 | if (null === $config) { 127 | throw new Exception\RuntimeException("Error parsing YAML data"); 128 | } 129 | 130 | return $this->process($config); 131 | } 132 | 133 | /** 134 | * Process the array for @include 135 | * 136 | * @param array $data 137 | * @return array 138 | * @throws Exception\RuntimeException 139 | */ 140 | protected function process(array $data) 141 | { 142 | foreach ($data as $key => $value) { 143 | if (is_array($value)) { 144 | $data[$key] = $this->process($value); 145 | } 146 | if (trim($key) === '@include') { 147 | if ($this->directory === null) { 148 | throw new Exception\RuntimeException('Cannot process @include statement for a json string'); 149 | } 150 | $reader = clone $this; 151 | unset($data[$key]); 152 | $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); 153 | } 154 | } 155 | return $data; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/ReaderPluginManager.php: -------------------------------------------------------------------------------- 1 | Reader\Ini::class, 21 | 'Ini' => Reader\Ini::class, 22 | 'json' => Reader\Json::class, 23 | 'Json' => Reader\Json::class, 24 | 'xml' => Reader\Xml::class, 25 | 'Xml' => Reader\Xml::class, 26 | 'yaml' => Reader\Yaml::class, 27 | 'Yaml' => Reader\Yaml::class, 28 | 'javaproperties' => Reader\JavaProperties::class, 29 | 'javaProperties' => Reader\JavaProperties::class, 30 | 'JavaProperties' => Reader\JavaProperties::class, 31 | ]; 32 | 33 | protected $factories = [ 34 | Reader\Ini::class => InvokableFactory::class, 35 | Reader\Json::class => InvokableFactory::class, 36 | Reader\Xml::class => InvokableFactory::class, 37 | Reader\Yaml::class => InvokableFactory::class, 38 | Reader\JavaProperties::class => InvokableFactory::class, 39 | // Legacy (v2) due to alias resolution; canonical form of resolved 40 | // alias is used to look up the factory, while the non-normalized 41 | // resolved alias is used as the requested name passed to the factory. 42 | 'zendconfigreaderini' => InvokableFactory::class, 43 | 'zendconfigreaderjson' => InvokableFactory::class, 44 | 'zendconfigreaderxml' => InvokableFactory::class, 45 | 'zendconfigreaderyaml' => InvokableFactory::class, 46 | 'zendconfigreaderjavaproperties' => InvokableFactory::class, 47 | ]; 48 | 49 | /** 50 | * Validate the plugin is of the expected type (v3). 51 | * 52 | * Validates against `$instanceOf`. 53 | * 54 | * @param mixed $instance 55 | * @throws InvalidServiceException 56 | */ 57 | public function validate($instance) 58 | { 59 | if (! $instance instanceof $this->instanceOf) { 60 | throw new InvalidServiceException(sprintf( 61 | '%s can only create instances of %s; %s is invalid', 62 | get_class($this), 63 | $this->instanceOf, 64 | (is_object($instance) ? get_class($instance) : gettype($instance)) 65 | )); 66 | } 67 | } 68 | 69 | /** 70 | * Validate the plugin is of the expected type (v2). 71 | * 72 | * Proxies to `validate()`. 73 | * 74 | * @param mixed $instance 75 | * @throws Exception\InvalidArgumentException 76 | */ 77 | public function validatePlugin($instance) 78 | { 79 | try { 80 | $this->validate($instance); 81 | } catch (InvalidServiceException $e) { 82 | throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 83 | } 84 | } 85 | 86 | public function __construct(ContainerInterface $container, array $config = []) 87 | { 88 | $config = array_merge_recursive(['aliases' => $this->aliases], $config); 89 | parent::__construct($container, $config); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/StandaloneReaderPluginManager.php: -------------------------------------------------------------------------------- 1 | Reader\Ini::class, 16 | 'json' => Reader\Json::class, 17 | 'xml' => Reader\Xml::class, 18 | 'yaml' => Reader\Yaml::class, 19 | 'javaproperties' => Reader\JavaProperties::class, 20 | ]; 21 | 22 | /** 23 | * @param string $plugin 24 | * @return bool 25 | */ 26 | public function has($plugin) 27 | { 28 | if (in_array($plugin, array_values($this->knownPlugins), true)) { 29 | return true; 30 | } 31 | 32 | return in_array(strtolower($plugin), array_keys($this->knownPlugins), true); 33 | } 34 | 35 | /** 36 | * @param string $plugin 37 | * @return Reader\ReaderInterface 38 | * @throws Exception\PluginNotFoundException 39 | */ 40 | public function get($plugin) 41 | { 42 | if (! $this->has($plugin)) { 43 | throw new Exception\PluginNotFoundException(sprintf( 44 | 'Config reader plugin by name %s not found', 45 | $plugin 46 | )); 47 | } 48 | 49 | if (! class_exists($plugin)) { 50 | $plugin = $this->knownPlugins[strtolower($plugin)]; 51 | } 52 | 53 | return new $plugin(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/StandaloneWriterPluginManager.php: -------------------------------------------------------------------------------- 1 | Writer\Ini::class, 16 | 'javaproperties' => Writer\JavaProperties::class, 17 | 'json' => Writer\Json::class, 18 | 'php' => Writer\PhpArray::class, 19 | 'phparray' => Writer\PhpArray::class, 20 | 'xml' => Writer\Xml::class, 21 | 'yaml' => Writer\Yaml::class, 22 | ]; 23 | 24 | /** 25 | * @param string $plugin 26 | * @return bool 27 | */ 28 | public function has($plugin) 29 | { 30 | if (in_array($plugin, array_values($this->knownPlugins), true)) { 31 | return true; 32 | } 33 | 34 | return in_array(strtolower($plugin), array_keys($this->knownPlugins), true); 35 | } 36 | 37 | /** 38 | * @param string $plugin 39 | * @return Reader\ReaderInterface 40 | * @throws Exception\PluginNotFoundException 41 | */ 42 | public function get($plugin) 43 | { 44 | if (! $this->has($plugin)) { 45 | throw new Exception\PluginNotFoundException(sprintf( 46 | 'Config writer plugin by name %s not found', 47 | $plugin 48 | )); 49 | } 50 | 51 | if (! class_exists($plugin)) { 52 | $plugin = $this->knownPlugins[strtolower($plugin)]; 53 | } 54 | 55 | return new $plugin(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Writer/AbstractWriter.php: -------------------------------------------------------------------------------- 1 | toString($config), $flags); 50 | } catch (\Exception $e) { 51 | restore_error_handler(); 52 | throw $e; 53 | } 54 | 55 | restore_error_handler(); 56 | } 57 | 58 | /** 59 | * toString(): defined by Writer interface. 60 | * 61 | * @see WriterInterface::toString() 62 | * @param mixed $config 63 | * @return string 64 | * @throws Exception\InvalidArgumentException 65 | */ 66 | public function toString($config) 67 | { 68 | if ($config instanceof Traversable) { 69 | $config = ArrayUtils::iteratorToArray($config); 70 | } elseif (! is_array($config)) { 71 | throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable config'); 72 | } 73 | 74 | return $this->processConfig($config); 75 | } 76 | 77 | /** 78 | * @param array $config 79 | * @return string 80 | */ 81 | abstract protected function processConfig(array $config); 82 | } 83 | -------------------------------------------------------------------------------- /src/Writer/Ini.php: -------------------------------------------------------------------------------- 1 | nestSeparator = $separator; 38 | return $this; 39 | } 40 | 41 | /** 42 | * Get nest separator. 43 | * 44 | * @return string 45 | */ 46 | public function getNestSeparator() 47 | { 48 | return $this->nestSeparator; 49 | } 50 | 51 | /** 52 | * Set if rendering should occur without sections or not. 53 | * 54 | * If set to true, the INI file is rendered without sections completely 55 | * into the global namespace of the INI file. 56 | * 57 | * @param bool $withoutSections 58 | * @return self 59 | */ 60 | public function setRenderWithoutSectionsFlags($withoutSections) 61 | { 62 | $this->renderWithoutSections = (bool) $withoutSections; 63 | return $this; 64 | } 65 | 66 | /** 67 | * Return whether the writer should render without sections. 68 | * 69 | * @return bool 70 | */ 71 | public function shouldRenderWithoutSections() 72 | { 73 | return $this->renderWithoutSections; 74 | } 75 | 76 | /** 77 | * processConfig(): defined by AbstractWriter. 78 | * 79 | * @param array $config 80 | * @return string 81 | */ 82 | public function processConfig(array $config) 83 | { 84 | $iniString = ''; 85 | 86 | if ($this->shouldRenderWithoutSections()) { 87 | $iniString .= $this->addBranch($config); 88 | } else { 89 | $config = $this->sortRootElements($config); 90 | 91 | foreach ($config as $sectionName => $data) { 92 | if (! is_array($data)) { 93 | $iniString .= $sectionName 94 | . ' = ' 95 | . $this->prepareValue($data) 96 | . "\n"; 97 | } else { 98 | $iniString .= '[' . $sectionName . ']' . "\n" 99 | . $this->addBranch($data) 100 | . "\n"; 101 | } 102 | } 103 | } 104 | 105 | return $iniString; 106 | } 107 | 108 | /** 109 | * Add a branch to an INI string recursively. 110 | * 111 | * @param array $config 112 | * @param array $parents 113 | * @return string 114 | */ 115 | protected function addBranch(array $config, $parents = []) 116 | { 117 | $iniString = ''; 118 | 119 | foreach ($config as $key => $value) { 120 | $group = array_merge($parents, [$key]); 121 | 122 | if (is_array($value)) { 123 | $iniString .= $this->addBranch($value, $group); 124 | } else { 125 | $iniString .= implode($this->nestSeparator, $group) 126 | . ' = ' 127 | . $this->prepareValue($value) 128 | . "\n"; 129 | } 130 | } 131 | 132 | return $iniString; 133 | } 134 | 135 | /** 136 | * Prepare a value for INI. 137 | * 138 | * @param mixed $value 139 | * @return string 140 | * @throws Exception\RuntimeException 141 | */ 142 | protected function prepareValue($value) 143 | { 144 | if (is_int($value) || is_float($value)) { 145 | return $value; 146 | } 147 | 148 | if (is_bool($value)) { 149 | return ($value ? 'true' : 'false'); 150 | } 151 | 152 | if (false === strpos($value, '"')) { 153 | return '"' . $value . '"'; 154 | } 155 | 156 | throw new Exception\RuntimeException('Value can not contain double quotes'); 157 | } 158 | 159 | /** 160 | * Root elements that are not assigned to any section needs to be on the 161 | * top of config. 162 | * 163 | * @param array $config 164 | * @return array 165 | */ 166 | protected function sortRootElements(array $config) 167 | { 168 | $sections = []; 169 | 170 | // Remove sections from config array. 171 | foreach ($config as $key => $value) { 172 | if (is_array($value)) { 173 | $sections[$key] = $value; 174 | unset($config[$key]); 175 | } 176 | } 177 | 178 | // Read sections to the end. 179 | foreach ($sections as $key => $value) { 180 | $config[$key] = $value; 181 | } 182 | 183 | return $config; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Writer/JavaProperties.php: -------------------------------------------------------------------------------- 1 | delimiter = $delimiter; 36 | } 37 | 38 | /** 39 | * processConfig(): defined by AbstractWriter. 40 | * 41 | * @param array $config 42 | * @return string 43 | * @throws Exception\UnprocessableConfigException for non-scalar values in 44 | * the $config array. 45 | */ 46 | public function processConfig(array $config) 47 | { 48 | $string = ''; 49 | 50 | foreach ($config as $key => $value) { 51 | if (! is_scalar($value)) { 52 | throw new Exception\UnprocessableConfigException(sprintf( 53 | '%s configuration writer can only process scalar values; received "%s" for key "%s"', 54 | __CLASS__, 55 | is_object($value) ? get_class($value) : gettype($value), 56 | $key 57 | )); 58 | } 59 | 60 | $value = (null === $value) ? '' : $value; 61 | 62 | $string .= sprintf( 63 | "%s%s%s\n", 64 | $key, 65 | $this->delimiter, 66 | $value 67 | ); 68 | } 69 | 70 | return $string; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Writer/Json.php: -------------------------------------------------------------------------------- 1 | $this->useBracketArraySyntax ? '[' : 'array(', 39 | 'close' => $this->useBracketArraySyntax ? ']' : ')' 40 | ]; 41 | 42 | return "processIndented($config, $arraySyntax) . 44 | $arraySyntax['close'] . ";\n"; 45 | } 46 | 47 | /** 48 | * Sets whether or not to use the PHP 5.4+ "[]" array syntax. 49 | * 50 | * @param bool $value 51 | * @return self 52 | */ 53 | public function setUseBracketArraySyntax($value) 54 | { 55 | $this->useBracketArraySyntax = $value; 56 | return $this; 57 | } 58 | 59 | /** 60 | * Sets whether or not to render resolvable FQN strings as scalars, using PHP 5.5+ class-keyword 61 | * 62 | * @param boolean $value 63 | * @return self 64 | */ 65 | public function setUseClassNameScalars($value) 66 | { 67 | $this->useClassNameScalars = $value; 68 | return $this; 69 | } 70 | 71 | /** 72 | * @return boolean 73 | */ 74 | public function getUseClassNameScalars() 75 | { 76 | return $this->useClassNameScalars; 77 | } 78 | 79 | /** 80 | * toFile(): defined by Writer interface. 81 | * 82 | * @see WriterInterface::toFile() 83 | * @param string $filename 84 | * @param mixed $config 85 | * @param bool $exclusiveLock 86 | * @return void 87 | * @throws Exception\InvalidArgumentException 88 | * @throws Exception\RuntimeException 89 | */ 90 | public function toFile($filename, $config, $exclusiveLock = true) 91 | { 92 | if (empty($filename)) { 93 | throw new Exception\InvalidArgumentException('No file name specified'); 94 | } 95 | 96 | $flags = 0; 97 | if ($exclusiveLock) { 98 | $flags |= LOCK_EX; 99 | } 100 | 101 | set_error_handler( 102 | function ($error, $message = '') use ($filename) { 103 | throw new Exception\RuntimeException( 104 | sprintf('Error writing to "%s": %s', $filename, $message), 105 | $error 106 | ); 107 | }, 108 | E_WARNING 109 | ); 110 | 111 | try { 112 | // for Windows, paths are escaped. 113 | $dirname = str_replace('\\', '\\\\', dirname($filename)); 114 | 115 | $string = $this->toString($config); 116 | $string = str_replace("'" . $dirname, "__DIR__ . '", $string); 117 | 118 | file_put_contents($filename, $string, $flags); 119 | } catch (\Exception $e) { 120 | restore_error_handler(); 121 | throw $e; 122 | } 123 | 124 | restore_error_handler(); 125 | } 126 | 127 | /** 128 | * Recursively processes a PHP config array structure into a readable format. 129 | * 130 | * @param array $config 131 | * @param array $arraySyntax 132 | * @param int $indentLevel 133 | * @return string 134 | */ 135 | protected function processIndented(array $config, array $arraySyntax, &$indentLevel = 1) 136 | { 137 | $arrayString = ""; 138 | 139 | foreach ($config as $key => $value) { 140 | $arrayString .= str_repeat(self::INDENT_STRING, $indentLevel); 141 | $arrayString .= (is_int($key) ? $key : $this->processStringKey($key)) . ' => '; 142 | 143 | if (is_array($value)) { 144 | if ($value === []) { 145 | $arrayString .= $arraySyntax['open'] . $arraySyntax['close'] . ",\n"; 146 | } else { 147 | $indentLevel++; 148 | $arrayString .= $arraySyntax['open'] . "\n" 149 | . $this->processIndented($value, $arraySyntax, $indentLevel) 150 | . str_repeat(self::INDENT_STRING, --$indentLevel) . $arraySyntax['close'] . ",\n"; 151 | } 152 | } elseif (is_object($value)) { 153 | $arrayString .= var_export($value, true) . ",\n"; 154 | } elseif (is_string($value)) { 155 | $arrayString .= $this->processStringValue($value) . ",\n"; 156 | } elseif (is_bool($value)) { 157 | $arrayString .= ($value ? 'true' : 'false') . ",\n"; 158 | } elseif ($value === null) { 159 | $arrayString .= "null,\n"; 160 | } else { 161 | $arrayString .= $value . ",\n"; 162 | } 163 | } 164 | 165 | return $arrayString; 166 | } 167 | 168 | /** 169 | * Process a string configuration value 170 | * 171 | * @param string $value 172 | * @return string 173 | */ 174 | protected function processStringValue($value) 175 | { 176 | if ($this->useClassNameScalars && false !== ($fqnValue = $this->fqnStringToClassNameScalar($value))) { 177 | return $fqnValue; 178 | } 179 | 180 | return var_export($value, true); 181 | } 182 | 183 | /** 184 | * Process a string configuration key 185 | * 186 | * @param string $key 187 | * @return string 188 | */ 189 | protected function processStringKey($key) 190 | { 191 | if ($this->useClassNameScalars && false !== ($fqnKey = $this->fqnStringToClassNameScalar($key))) { 192 | return $fqnKey; 193 | } 194 | 195 | return "'" . addslashes($key) . "'"; 196 | } 197 | 198 | /** 199 | * Attempts to convert a FQN string to class name scalar. 200 | * Returns false if string is not a valid FQN or can not be resolved to an existing name. 201 | * 202 | * @param string $string 203 | * @return bool|string 204 | */ 205 | protected function fqnStringToClassNameScalar($string) 206 | { 207 | if (strlen($string) < 1) { 208 | return false; 209 | } 210 | 211 | if ($string[0] !== '\\') { 212 | $string = '\\' . $string; 213 | } 214 | 215 | if ($this->checkStringIsFqn($string)) { 216 | return $string . '::class'; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | /** 223 | * Check whether a string represents a resolvable FQCN 224 | * 225 | * @param string $string 226 | * @return bool 227 | */ 228 | protected function checkStringIsFqn($string) 229 | { 230 | if (! preg_match('/^(?:\x5c[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $string)) { 231 | return false; 232 | } 233 | 234 | return class_exists($string) || interface_exists($string) || trait_exists($string); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Writer/WriterInterface.php: -------------------------------------------------------------------------------- 1 | openMemory(); 25 | $writer->setIndent(true); 26 | $writer->setIndentString(str_repeat(' ', 4)); 27 | 28 | $writer->startDocument('1.0', 'UTF-8'); 29 | $writer->startElement('zend-config'); 30 | 31 | foreach ($config as $sectionName => $data) { 32 | if (! is_array($data)) { 33 | $writer->writeElement($sectionName, (string) $data); 34 | } else { 35 | $this->addBranch($sectionName, $data, $writer); 36 | } 37 | } 38 | 39 | $writer->endElement(); 40 | $writer->endDocument(); 41 | 42 | return $writer->outputMemory(); 43 | } 44 | 45 | /** 46 | * Add a branch to an XML object recursively. 47 | * 48 | * @param string $branchName 49 | * @param array $config 50 | * @param XMLWriter $writer 51 | * @return void 52 | * @throws Exception\RuntimeException 53 | */ 54 | protected function addBranch($branchName, array $config, XMLWriter $writer) 55 | { 56 | $branchType = null; 57 | 58 | foreach ($config as $key => $value) { 59 | if ($branchType === null) { 60 | if (is_numeric($key)) { 61 | $branchType = 'numeric'; 62 | } else { 63 | $writer->startElement($branchName); 64 | $branchType = 'string'; 65 | } 66 | } elseif ($branchType !== (is_numeric($key) ? 'numeric' : 'string')) { 67 | throw new Exception\RuntimeException('Mixing of string and numeric keys is not allowed'); 68 | } 69 | 70 | if ($branchType === 'numeric') { 71 | if (is_array($value)) { 72 | $this->addBranch($branchName, $value, $writer); 73 | } else { 74 | $writer->writeElement($branchName, (string) $value); 75 | } 76 | } else { 77 | if (is_array($value)) { 78 | $this->addBranch($key, $value, $writer); 79 | } else { 80 | $writer->writeElement($key, (string) $value); 81 | } 82 | } 83 | } 84 | 85 | if ($branchType === 'string') { 86 | $writer->endElement(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Writer/Yaml.php: -------------------------------------------------------------------------------- 1 | setYamlEncoder($yamlEncoder); 30 | } else { 31 | if (function_exists('yaml_emit')) { 32 | $this->setYamlEncoder('yaml_emit'); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Get callback for decoding YAML 39 | * 40 | * @return callable 41 | */ 42 | public function getYamlEncoder() 43 | { 44 | return $this->yamlEncoder; 45 | } 46 | 47 | /** 48 | * Set callback for decoding YAML 49 | * 50 | * @param callable $yamlEncoder the decoder to set 51 | * @return self 52 | * @throws Exception\InvalidArgumentException 53 | */ 54 | public function setYamlEncoder($yamlEncoder) 55 | { 56 | if (! is_callable($yamlEncoder)) { 57 | throw new Exception\InvalidArgumentException('Invalid parameter to setYamlEncoder() - must be callable'); 58 | } 59 | $this->yamlEncoder = $yamlEncoder; 60 | return $this; 61 | } 62 | 63 | /** 64 | * processConfig(): defined by AbstractWriter. 65 | * 66 | * @param array $config 67 | * @return string 68 | * @throws Exception\RuntimeException 69 | */ 70 | public function processConfig(array $config) 71 | { 72 | if (null === $this->getYamlEncoder()) { 73 | throw new Exception\RuntimeException("You didn't specify a Yaml callback encoder"); 74 | } 75 | 76 | $config = call_user_func($this->getYamlEncoder(), $config); 77 | if (null === $config) { 78 | throw new Exception\RuntimeException("Error generating YAML data"); 79 | } 80 | 81 | return $config; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/WriterPluginManager.php: -------------------------------------------------------------------------------- 1 | Writer\Ini::class, 21 | 'Ini' => Writer\Ini::class, 22 | 'json' => Writer\Json::class, 23 | 'Json' => Writer\Json::class, 24 | 'php' => Writer\PhpArray::class, 25 | 'phparray' => Writer\PhpArray::class, 26 | 'phpArray' => Writer\PhpArray::class, 27 | 'PhpArray' => Writer\PhpArray::class, 28 | 'yaml' => Writer\Yaml::class, 29 | 'Yaml' => Writer\Yaml::class, 30 | 'xml' => Writer\Xml::class, 31 | 'Xml' => Writer\Xml::class, 32 | 'javaproperties' => Writer\JavaProperties::class, 33 | 'javaProperties' => Writer\JavaProperties::class, 34 | 'JavaProperties' => Writer\JavaProperties::class, 35 | ]; 36 | 37 | protected $factories = [ 38 | Writer\Ini::class => InvokableFactory::class, 39 | Writer\JavaProperties::class => InvokableFactory::class, 40 | Writer\Json::class => InvokableFactory::class, 41 | Writer\PhpArray::class => InvokableFactory::class, 42 | Writer\Yaml::class => InvokableFactory::class, 43 | Writer\Xml::class => InvokableFactory::class, 44 | // Legacy (v2) due to alias resolution; canonical form of resolved 45 | // alias is used to look up the factory, while the non-normalized 46 | // resolved alias is used as the requested name passed to the factory. 47 | 'zendconfigwriterini' => InvokableFactory::class, 48 | 'zendconfigwriterjavaproperties' => InvokableFactory::class, 49 | 'zendconfigwriterjson' => InvokableFactory::class, 50 | 'zendconfigwriterphparray' => InvokableFactory::class, 51 | 'zendconfigwriteryaml' => InvokableFactory::class, 52 | 'zendconfigwriterxml' => InvokableFactory::class, 53 | ]; 54 | 55 | /** 56 | * Validate the plugin is of the expected type (v3). 57 | * 58 | * Validates against `$instanceOf`. 59 | * 60 | * @param mixed $instance 61 | * @throws InvalidServiceException 62 | */ 63 | public function validate($instance) 64 | { 65 | if (! $instance instanceof $this->instanceOf) { 66 | throw new InvalidServiceException(sprintf( 67 | '%s can only create instances of %s; %s is invalid', 68 | get_class($this), 69 | $this->instanceOf, 70 | (is_object($instance) ? get_class($instance) : gettype($instance)) 71 | )); 72 | } 73 | } 74 | 75 | /** 76 | * Validate the plugin is of the expected type (v2). 77 | * 78 | * Proxies to `validate()`. 79 | * 80 | * @param mixed $instance 81 | * @throws Exception\InvalidArgumentException 82 | */ 83 | public function validatePlugin($instance) 84 | { 85 | try { 86 | $this->validate($instance); 87 | } catch (InvalidServiceException $e) { 88 | throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 89 | } 90 | } 91 | 92 | public function __construct(ContainerInterface $container, array $config = []) 93 | { 94 | $config = array_merge_recursive(['aliases' => $this->aliases], $config); 95 | parent::__construct($container, $config); 96 | } 97 | } 98 | --------------------------------------------------------------------------------