PageRenderTime 109ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/Config/Definition/ArrayNode.php

https://github.com/Exercise/symfony
PHP | 363 lines | 189 code | 48 blank | 126 comment | 22 complexity | ff5e968c8e9730d04a33a8378edcb5a3 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Config\Definition;
  11. use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
  12. use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
  13. use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
  14. /**
  15. * Represents an Array node in the config tree.
  16. *
  17. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  18. */
  19. class ArrayNode extends BaseNode implements PrototypeNodeInterface
  20. {
  21. protected $xmlRemappings;
  22. protected $children;
  23. protected $allowFalse;
  24. protected $allowNewKeys;
  25. protected $addIfNotSet;
  26. protected $performDeepMerging;
  27. protected $ignoreExtraKeys;
  28. /**
  29. * Constructor.
  30. *
  31. * @param string $name The Node's name
  32. * @param NodeInterface $parent The node parent
  33. */
  34. public function __construct($name, NodeInterface $parent = null)
  35. {
  36. parent::__construct($name, $parent);
  37. $this->children = array();
  38. $this->xmlRemappings = array();
  39. $this->removeKeyAttribute = true;
  40. $this->allowFalse = false;
  41. $this->addIfNotSet = false;
  42. $this->allowNewKeys = true;
  43. $this->performDeepMerging = true;
  44. }
  45. /**
  46. * Retrieves the children of this node.
  47. *
  48. * @return array The children
  49. */
  50. public function getChildren()
  51. {
  52. return $this->children;
  53. }
  54. /**
  55. * Sets the xml remappings that should be performed.
  56. *
  57. * @param array $remappings an array of the form array(array(string, string))
  58. */
  59. public function setXmlRemappings(array $remappings)
  60. {
  61. $this->xmlRemappings = $remappings;
  62. }
  63. /**
  64. * Sets whether to add default values for this array if it has not been
  65. * defined in any of the configuration files.
  66. *
  67. * @param Boolean $boolean
  68. */
  69. public function setAddIfNotSet($boolean)
  70. {
  71. $this->addIfNotSet = (Boolean) $boolean;
  72. }
  73. /**
  74. * Sets whether false is allowed as value indicating that the array should be unset.
  75. *
  76. * @param Boolean $allow
  77. */
  78. public function setAllowFalse($allow)
  79. {
  80. $this->allowFalse = (Boolean) $allow;
  81. }
  82. /**
  83. * Sets whether new keys can be defined in subsequent configurations.
  84. *
  85. * @param Boolean $allow
  86. */
  87. public function setAllowNewKeys($allow)
  88. {
  89. $this->allowNewKeys = (Boolean) $allow;
  90. }
  91. /**
  92. * Sets if deep merging should occur.
  93. *
  94. * @param Boolean $boolean
  95. */
  96. public function setPerformDeepMerging($boolean)
  97. {
  98. $this->performDeepMerging = (Boolean) $boolean;
  99. }
  100. /**
  101. * Whether extra keys should just be ignore without an exception.
  102. *
  103. * @param Boolean $boolean To allow extra keys
  104. */
  105. public function setIgnoreExtraKeys($boolean)
  106. {
  107. $this->ignoreExtraKeys = (Boolean) $boolean;
  108. }
  109. /**
  110. * Sets the node Name.
  111. *
  112. * @param string $name The node's name
  113. */
  114. public function setName($name)
  115. {
  116. $this->name = $name;
  117. }
  118. /**
  119. * Checks if the node has a default value.
  120. *
  121. * @return Boolean
  122. */
  123. public function hasDefaultValue()
  124. {
  125. return $this->addIfNotSet;
  126. }
  127. /**
  128. * Retrieves the default value.
  129. *
  130. * @return array The default value
  131. *
  132. * @throws \RuntimeException if the node has no default value
  133. */
  134. public function getDefaultValue()
  135. {
  136. if (!$this->hasDefaultValue()) {
  137. throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
  138. }
  139. $defaults = array();
  140. foreach ($this->children as $name => $child) {
  141. if ($child->hasDefaultValue()) {
  142. $defaults[$name] = $child->getDefaultValue();
  143. }
  144. }
  145. return $defaults;
  146. }
  147. /**
  148. * Adds a child node.
  149. *
  150. * @param NodeInterface $node The child node to add
  151. *
  152. * @throws \InvalidArgumentException when the child node has no name
  153. * @throws \InvalidArgumentException when the child node's name is not unique
  154. */
  155. public function addChild(NodeInterface $node)
  156. {
  157. $name = $node->getName();
  158. if (empty($name)) {
  159. throw new \InvalidArgumentException('Child nodes must be named.');
  160. }
  161. if (isset($this->children[$name])) {
  162. throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name));
  163. }
  164. $this->children[$name] = $node;
  165. }
  166. /**
  167. * Finalizes the value of this node.
  168. *
  169. * @param mixed $value
  170. *
  171. * @return mixed The finalised value
  172. *
  173. * @throws UnsetKeyException
  174. * @throws InvalidConfigurationException if the node doesn't have enough children
  175. */
  176. protected function finalizeValue($value)
  177. {
  178. if (false === $value) {
  179. $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
  180. throw new UnsetKeyException($msg);
  181. }
  182. foreach ($this->children as $name => $child) {
  183. if (!array_key_exists($name, $value)) {
  184. if ($child->isRequired()) {
  185. $msg = sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath());
  186. $ex = new InvalidConfigurationException($msg);
  187. $ex->setPath($this->getPath());
  188. throw $ex;
  189. }
  190. if ($child->hasDefaultValue()) {
  191. $value[$name] = $child->getDefaultValue();
  192. }
  193. continue;
  194. }
  195. try {
  196. $value[$name] = $child->finalize($value[$name]);
  197. } catch (UnsetKeyException $unset) {
  198. unset($value[$name]);
  199. }
  200. }
  201. return $value;
  202. }
  203. /**
  204. * Validates the type of the value.
  205. *
  206. * @param mixed $value
  207. *
  208. * @throws InvalidTypeException
  209. */
  210. protected function validateType($value)
  211. {
  212. if (!is_array($value) && (!$this->allowFalse || false !== $value)) {
  213. $ex = new InvalidTypeException(sprintf(
  214. 'Invalid type for path "%s". Expected array, but got %s',
  215. $this->getPath(),
  216. gettype($value)
  217. ));
  218. $ex->setPath($this->getPath());
  219. throw $ex;
  220. }
  221. }
  222. /**
  223. * Normalizes the value.
  224. *
  225. * @param mixed $value The value to normalize
  226. *
  227. * @return mixed The normalized value
  228. */
  229. protected function normalizeValue($value)
  230. {
  231. if (false === $value) {
  232. return $value;
  233. }
  234. $value = $this->remapXml($value);
  235. $normalized = array();
  236. foreach ($this->children as $name => $child) {
  237. if (array_key_exists($name, $value)) {
  238. $normalized[$name] = $child->normalize($value[$name]);
  239. unset($value[$name]);
  240. }
  241. }
  242. // if extra fields are present, throw exception
  243. if (count($value) && !$this->ignoreExtraKeys) {
  244. $msg = sprintf('Unrecognized options "%s" under "%s"', implode(', ', array_keys($value)), $this->getPath());
  245. $ex = new InvalidConfigurationException($msg);
  246. $ex->setPath($this->getPath());
  247. throw $ex;
  248. }
  249. return $normalized;
  250. }
  251. /**
  252. * Remaps multiple singular values to a single plural value.
  253. *
  254. * @param array $value The source values
  255. *
  256. * @return array The remapped values
  257. */
  258. protected function remapXml($value)
  259. {
  260. foreach ($this->xmlRemappings as $transformation) {
  261. list($singular, $plural) = $transformation;
  262. if (!isset($value[$singular])) {
  263. continue;
  264. }
  265. $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
  266. unset($value[$singular]);
  267. }
  268. return $value;
  269. }
  270. /**
  271. * Merges values together.
  272. *
  273. * @param mixed $leftSide The left side to merge.
  274. * @param mixed $rightSide The right side to merge.
  275. *
  276. * @return mixed The merged values
  277. *
  278. * @throws InvalidConfigurationException
  279. * @throws \RuntimeException
  280. */
  281. protected function mergeValues($leftSide, $rightSide)
  282. {
  283. if (false === $rightSide) {
  284. // if this is still false after the last config has been merged the
  285. // finalization pass will take care of removing this key entirely
  286. return false;
  287. }
  288. if (false === $leftSide || !$this->performDeepMerging) {
  289. return $rightSide;
  290. }
  291. foreach ($rightSide as $k => $v) {
  292. // no conflict
  293. if (!array_key_exists($k, $leftSide)) {
  294. if (!$this->allowNewKeys) {
  295. $ex = new InvalidConfigurationException(sprintf(
  296. 'You are not allowed to define new elements for path "%s". '
  297. .'Please define all elements for this path in one config file. '
  298. .'If you are trying to overwrite an element, make sure you redefine it '
  299. .'with the same name.',
  300. $this->getPath()
  301. ));
  302. $ex->setPath($this->getPath());
  303. throw $ex;
  304. }
  305. $leftSide[$k] = $v;
  306. continue;
  307. }
  308. if (!isset($this->children[$k])) {
  309. throw new \RuntimeException('merge() expects a normalized config array.');
  310. }
  311. $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
  312. }
  313. return $leftSide;
  314. }
  315. }