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

https://github.com/meze/symfony · PHP · 280 lines · 142 code · 37 blank · 101 comment · 23 complexity · 478b7ea874e6a47252faea254a7adcbd 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\DuplicateKeyException;
  13. use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
  14. /**
  15. * Represents a prototyped Array node in the config tree.
  16. *
  17. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  18. */
  19. class PrototypedArrayNode extends ArrayNode
  20. {
  21. protected $prototype;
  22. protected $keyAttribute;
  23. protected $removeKeyAttribute;
  24. protected $minNumberOfElements;
  25. protected $defaultValue;
  26. /**
  27. * Constructor.
  28. *
  29. * @param string $name The Node's name
  30. * @param NodeInterface $parent The node parent
  31. */
  32. public function __construct($name, NodeInterface $parent = null)
  33. {
  34. parent::__construct($name, $parent);
  35. $this->minNumberOfElements = 0;
  36. }
  37. /**
  38. * Sets the minimum number of elements that a prototype based node must
  39. * contain. By default this is zero, meaning no elements.
  40. *
  41. * @param integer $number
  42. */
  43. public function setMinNumberOfElements($number)
  44. {
  45. $this->minNumberOfElements = $number;
  46. }
  47. /**
  48. * The name of the attribute which value should be used as key.
  49. *
  50. * This is only relevant for XML configurations, and only in combination
  51. * with a prototype based node.
  52. *
  53. * For example, if "id" is the keyAttribute, then:
  54. *
  55. * array('id' => 'my_name', 'foo' => 'bar')
  56. *
  57. * becomes
  58. *
  59. * 'my_name' => array('foo' => 'bar')
  60. *
  61. * If $remove is false, the resulting array will still have the
  62. * "'id' => 'my_name'" item in it.
  63. *
  64. * @param string $attribute The name of the attribute which value is to be used as a key
  65. * @param Boolean $remove Whether or not to remove the key
  66. */
  67. public function setKeyAttribute($attribute, $remove = true)
  68. {
  69. $this->keyAttribute = $attribute;
  70. $this->removeKeyAttribute = $remove;
  71. }
  72. /**
  73. * Sets the default value of this node.
  74. *
  75. * @param string $value
  76. * @throws \InvalidArgumentException if the default value is not an array
  77. */
  78. public function setDefaultValue($value)
  79. {
  80. if (!is_array($value)) {
  81. throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.');
  82. }
  83. $this->defaultValue = $value;
  84. }
  85. /**
  86. * Checks if the node has a default value.
  87. *
  88. * @return Boolean
  89. */
  90. public function hasDefaultValue()
  91. {
  92. return true;
  93. }
  94. /**
  95. * Retrieves the default value.
  96. *
  97. * @return array The default value
  98. */
  99. public function getDefaultValue()
  100. {
  101. return $this->defaultValue ?: array();
  102. }
  103. /**
  104. * Sets the node prototype.
  105. *
  106. * @param PrototypeNodeInterface $node
  107. */
  108. public function setPrototype(PrototypeNodeInterface $node)
  109. {
  110. $this->prototype = $node;
  111. }
  112. /**
  113. * Disable adding concrete children for prototyped nodes.
  114. *
  115. * @param NodeInterface $node The child node to add
  116. * @throws \RuntimeException Prototyped array nodes can't have concrete children.
  117. */
  118. public function addChild(NodeInterface $node)
  119. {
  120. throw new \RuntimeException('A prototyped array node can not have concrete children.');
  121. }
  122. /**
  123. * Finalizes the value of this node.
  124. *
  125. * @param mixed $value
  126. * @return mixed The finalised value
  127. * @throws UnsetKeyException
  128. * @throws InvalidConfigurationException if the node doesn't have enough children
  129. */
  130. protected function finalizeValue($value)
  131. {
  132. if (false === $value) {
  133. $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
  134. throw new UnsetKeyException($msg);
  135. }
  136. foreach ($value as $k => $v) {
  137. $this->prototype->setName($k);
  138. try {
  139. $value[$k] = $this->prototype->finalize($v);
  140. } catch (UnsetKeyException $unset) {
  141. unset($value[$k]);
  142. }
  143. }
  144. if (count($value) < $this->minNumberOfElements) {
  145. $msg = sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements);
  146. $ex = new InvalidConfigurationException($msg);
  147. $ex->setPath($this->getPath());
  148. throw $ex;
  149. }
  150. return $value;
  151. }
  152. /**
  153. * Normalizes the value.
  154. *
  155. * @param mixed $value The value to normalize
  156. * @return mixed The normalized value
  157. */
  158. protected function normalizeValue($value)
  159. {
  160. if (false === $value) {
  161. return $value;
  162. }
  163. $value = $this->remapXml($value);
  164. $normalized = array();
  165. foreach ($value as $k => $v) {
  166. if (null !== $this->keyAttribute && is_array($v)) {
  167. if (!isset($v[$this->keyAttribute]) && is_int($k)) {
  168. $msg = sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath());
  169. $ex = new InvalidConfigurationException($msg);
  170. $ex->setPath($this->getPath());
  171. throw $ex;
  172. } else if (isset($v[$this->keyAttribute])) {
  173. $k = $v[$this->keyAttribute];
  174. // remove the key attribute when required
  175. if ($this->removeKeyAttribute) {
  176. unset($v[$this->keyAttribute]);
  177. }
  178. // if only "value" is left
  179. if (1 == count($v) && isset($v['value'])) {
  180. $v = $v['value'];
  181. }
  182. }
  183. if (array_key_exists($k, $normalized)) {
  184. $msg = sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath());
  185. $ex = new DuplicateKeyException($msg);
  186. $ex->setPath($this->getPath());
  187. throw $ex;
  188. }
  189. }
  190. $this->prototype->setName($k);
  191. if (null !== $this->keyAttribute) {
  192. $normalized[$k] = $this->prototype->normalize($v);
  193. } else {
  194. $normalized[] = $this->prototype->normalize($v);
  195. }
  196. }
  197. return $normalized;
  198. }
  199. /**
  200. * Merges values together.
  201. *
  202. * @param mixed $leftSide The left side to merge.
  203. * @param mixed $rightSide The right side to merge.
  204. * @return mixed The merged values
  205. * @throws InvalidConfigurationException
  206. * @throws \RuntimeException
  207. */
  208. protected function mergeValues($leftSide, $rightSide)
  209. {
  210. if (false === $rightSide) {
  211. // if this is still false after the last config has been merged the
  212. // finalization pass will take care of removing this key entirely
  213. return false;
  214. }
  215. if (false === $leftSide || !$this->performDeepMerging) {
  216. return $rightSide;
  217. }
  218. foreach ($rightSide as $k => $v) {
  219. // prototype, and key is irrelevant, so simply append the element
  220. if (null === $this->keyAttribute) {
  221. $leftSide[] = $v;
  222. continue;
  223. }
  224. // no conflict
  225. if (!array_key_exists($k, $leftSide)) {
  226. if (!$this->allowNewKeys) {
  227. $ex = new InvalidConfigurationException(sprintf(
  228. 'You are not allowed to define new elements for path "%s". ' .
  229. 'Please define all elements for this path in one config file.',
  230. $this->getPath()
  231. ));
  232. $ex->setPath($this->getPath());
  233. throw $ex;
  234. }
  235. $leftSide[$k] = $v;
  236. continue;
  237. }
  238. $this->prototype->setName($k);
  239. $leftSide[$k] = $this->prototype->merge($leftSide[$k], $v);
  240. }
  241. return $leftSide;
  242. }
  243. }