ArrayNode.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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\InvalidTypeException;
  14. use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
  15. /**
  16. * Represents an ARRAY node in the config tree.
  17. *
  18. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  19. */
  20. class ArrayNode extends BaseNode implements PrototypeNodeInterface
  21. {
  22. protected $xmlRemappings;
  23. protected $children;
  24. protected $prototype;
  25. protected $keyAttribute;
  26. protected $removeKeyAttribute;
  27. protected $allowFalse;
  28. protected $allowNewKeys;
  29. protected $addIfNotSet;
  30. protected $minNumberOfElements;
  31. protected $performDeepMerging;
  32. protected $defaultValue;
  33. protected $ignoreExtraKeys;
  34. /**
  35. * Constructor.
  36. *
  37. * @param string $name The Node's name
  38. * @param NodeInterface $parent The node parent
  39. */
  40. public function __construct($name, NodeInterface $parent = null)
  41. {
  42. parent::__construct($name, $parent);
  43. $this->children = array();
  44. $this->xmlRemappings = array();
  45. $this->removeKeyAttribute = true;
  46. $this->allowFalse = false;
  47. $this->addIfNotSet = false;
  48. $this->allowNewKeys = true;
  49. $this->performDeepMerging = true;
  50. $this->minNumberOfElements = 0;
  51. }
  52. /**
  53. * Sets the xml remappings that should be performed.
  54. *
  55. * @param array $remappings an array of the form array(array(string, string))
  56. * @return void
  57. */
  58. public function setXmlRemappings(array $remappings)
  59. {
  60. $this->xmlRemappings = $remappings;
  61. }
  62. /**
  63. * Sets the minimum number of elements that a prototype based node must
  64. * contain. By default this is zero, meaning no elements.
  65. *
  66. * @param integer $number
  67. * @return void
  68. */
  69. public function setMinNumberOfElements($number)
  70. {
  71. $this->minNumberOfElements = $number;
  72. }
  73. /**
  74. * The name of the attribute that should be used as key.
  75. *
  76. * This is only relevant for XML configurations, and only in combination
  77. * with a prototype based node.
  78. *
  79. * For example, if "id" is the keyAttribute, then:
  80. *
  81. * array('id' => 'my_name', 'foo' => 'bar')
  82. *
  83. * becomes
  84. *
  85. * 'id' => array('foo' => 'bar')
  86. *
  87. * If $remove is false, the resulting array will still have the
  88. * "'id' => 'my_name'" item in it.
  89. *
  90. * @param string $attribute The name of the attribute to use as a key
  91. * @param Boolean $remove Whether or not to remove the key
  92. * @return void
  93. */
  94. public function setKeyAttribute($attribute, $remove = true)
  95. {
  96. $this->keyAttribute = $attribute;
  97. $this->removeKeyAttribute = $remove;
  98. }
  99. /**
  100. * Sets whether to add default values for this array if it has not been
  101. * defined in any of the configuration files.
  102. *
  103. * @param Boolean $boolean
  104. * @return void
  105. */
  106. public function setAddIfNotSet($boolean)
  107. {
  108. $this->addIfNotSet = (Boolean) $boolean;
  109. }
  110. /**
  111. * Sets whether false is allowed as value indicating that the array should
  112. * be unset.
  113. *
  114. * @param Boolean $allow
  115. * @return void
  116. */
  117. public function setAllowFalse($allow)
  118. {
  119. $this->allowFalse = (Boolean) $allow;
  120. }
  121. /**
  122. * Sets whether new keys can be defined in subsequent configurations.
  123. *
  124. * @param Boolean $allow
  125. * @return void
  126. */
  127. public function setAllowNewKeys($allow)
  128. {
  129. $this->allowNewKeys = (Boolean) $allow;
  130. }
  131. /**
  132. * Sets if deep merging should occur.
  133. *
  134. * @param Boolean $boolean
  135. */
  136. public function setPerformDeepMerging($boolean)
  137. {
  138. $this->performDeepMerging = (Boolean) $boolean;
  139. }
  140. /**
  141. * Whether extra keys should just be ignore without an exception.
  142. *
  143. * @param Boolean $boolean To allow extra keys
  144. */
  145. public function setIgnoreExtraKeys($boolean)
  146. {
  147. $this->ignoreExtraKeys = (Boolean) $boolean;
  148. }
  149. /**
  150. * Sets the node Name.
  151. *
  152. * @param string $name The node's name
  153. */
  154. public function setName($name)
  155. {
  156. $this->name = $name;
  157. }
  158. /**
  159. * Sets the default value of this node.
  160. *
  161. * @param string $value
  162. * @throws \InvalidArgumentException if the default value is not an array
  163. * @throws \RuntimeException if the node does not have a prototype
  164. */
  165. public function setDefaultValue($value)
  166. {
  167. if (!is_array($value)) {
  168. throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.');
  169. }
  170. if (null === $this->prototype) {
  171. throw new \RuntimeException($this->getPath().': An ARRAY node can have a specified default value only when using a prototype');
  172. }
  173. $this->defaultValue = $value;
  174. }
  175. /**
  176. * Checks if the node has a default value.
  177. *
  178. * @return boolean
  179. */
  180. public function hasDefaultValue()
  181. {
  182. if (null !== $this->prototype) {
  183. return true;
  184. }
  185. return $this->addIfNotSet;
  186. }
  187. /**
  188. * Retrieves the default value.
  189. *
  190. * @return array The default value
  191. * @throws \RuntimeException if the node has no default value
  192. */
  193. public function getDefaultValue()
  194. {
  195. if (!$this->hasDefaultValue()) {
  196. throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
  197. }
  198. if (null !== $this->prototype) {
  199. return $this->defaultValue ?: array();
  200. }
  201. $defaults = array();
  202. foreach ($this->children as $name => $child) {
  203. if (!$child->hasDefaultValue()) {
  204. continue;
  205. }
  206. $defaults[$name] = $child->getDefaultValue();
  207. }
  208. return $defaults;
  209. }
  210. /**
  211. * Sets the node prototype.
  212. *
  213. * @param PrototypeNodeInterface $node
  214. * @throws \RuntimeException if the node doesn't have concrete children
  215. */
  216. public function setPrototype(PrototypeNodeInterface $node)
  217. {
  218. if (count($this->children) > 0) {
  219. throw new \RuntimeException($this->getPath().': An ARRAY node must either have concrete children, or a prototype node.');
  220. }
  221. $this->prototype = $node;
  222. }
  223. /**
  224. * Adds a child node.
  225. *
  226. * @param NodeInterface $node The child node to add
  227. * @throws \InvalidArgumentException when the child node has no name
  228. * @throws \InvalidArgumentException when the child node's name is not unique
  229. * @throws \RuntimeException if this array node is not a prototype
  230. */
  231. public function addChild(NodeInterface $node)
  232. {
  233. $name = $node->getName();
  234. if (empty($name)) {
  235. throw new \InvalidArgumentException('Node name cannot be empty.');
  236. }
  237. if (isset($this->children[$name])) {
  238. throw new \InvalidArgumentException(sprintf('The node "%s" already exists.', $name));
  239. }
  240. if (null !== $this->prototype) {
  241. throw new \RuntimeException('An ARRAY node must either have a prototype, or concrete children.');
  242. }
  243. $this->children[$name] = $node;
  244. }
  245. /**
  246. * Finalises the value of this node.
  247. *
  248. * @param mixed $value
  249. * @return mixed The finalised value
  250. * @throws UnsetKeyException
  251. * @throws InvalidConfigurationException if the node doesn't have enough children
  252. */
  253. protected function finalizeValue($value)
  254. {
  255. if (false === $value) {
  256. throw new UnsetKeyException(sprintf(
  257. 'Unsetting key for path "%s", value: %s',
  258. $this->getPath(),
  259. json_encode($value)
  260. ));
  261. }
  262. if (null !== $this->prototype) {
  263. foreach ($value as $k => $v) {
  264. $this->prototype->setName($k);
  265. try {
  266. $value[$k] = $this->prototype->finalize($v);
  267. } catch (UnsetKeyException $unset) {
  268. unset($value[$k]);
  269. }
  270. }
  271. if (count($value) < $this->minNumberOfElements) {
  272. throw new InvalidConfigurationException(sprintf(
  273. 'You must define at least %d element(s) for path "%s".',
  274. $this->minNumberOfElements,
  275. $this->getPath()
  276. ));
  277. }
  278. return $value;
  279. }
  280. foreach ($this->children as $name => $child) {
  281. if (!array_key_exists($name, $value)) {
  282. if ($child->isRequired()) {
  283. throw new InvalidConfigurationException(sprintf(
  284. 'The node at path "%s" must be configured.',
  285. $this->getPath().'.'.$name
  286. ));
  287. }
  288. if ($child->hasDefaultValue()) {
  289. $value[$name] = $child->getDefaultValue();
  290. }
  291. continue;
  292. }
  293. try {
  294. $value[$name] = $child->finalize($value[$name]);
  295. } catch (UnsetKeyException $unset) {
  296. unset($value[$name]);
  297. }
  298. }
  299. return $value;
  300. }
  301. /**
  302. * Validates the type of the value.
  303. *
  304. * @param mixed $value
  305. * @throws InvalidTypeException
  306. */
  307. protected function validateType($value)
  308. {
  309. if (!is_array($value) && (!$this->allowFalse || false !== $value)) {
  310. throw new InvalidTypeException(sprintf(
  311. 'Invalid type for path "%s". Expected array, but got %s',
  312. $this->getPath(),
  313. json_encode($value)
  314. ));
  315. }
  316. }
  317. /**
  318. * Normalises the value.
  319. *
  320. * @param mixed $value The value to normalise
  321. * @return mixed The normalised value
  322. */
  323. protected function normalizeValue($value)
  324. {
  325. if (false === $value) {
  326. return $value;
  327. }
  328. foreach ($this->xmlRemappings as $transformation) {
  329. list($singular, $plural) = $transformation;
  330. if (!isset($value[$singular])) {
  331. continue;
  332. }
  333. $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
  334. unset($value[$singular]);
  335. }
  336. if (null !== $this->prototype) {
  337. $normalized = array();
  338. foreach ($value as $k => $v) {
  339. if (null !== $this->keyAttribute && is_array($v)) {
  340. if (!isset($v[$this->keyAttribute]) && is_int($k)) {
  341. throw new InvalidConfigurationException(sprintf(
  342. 'You must set a "%s" attribute for path "%s".',
  343. $this->keyAttribute,
  344. $this->getPath()
  345. ));
  346. } else if (isset($v[$this->keyAttribute])) {
  347. $k = $v[$this->keyAttribute];
  348. // remove the key attribute if configured to
  349. if ($this->removeKeyAttribute) {
  350. unset($v[$this->keyAttribute]);
  351. }
  352. }
  353. if (array_key_exists($k, $normalized)) {
  354. throw new DuplicateKeyException(sprintf(
  355. 'Duplicate key "%s" for path "%s".',
  356. $k,
  357. $this->getPath()
  358. ));
  359. }
  360. }
  361. $this->prototype->setName($k);
  362. if (null !== $this->keyAttribute) {
  363. $normalized[$k] = $this->prototype->normalize($v);
  364. } else {
  365. $normalized[] = $this->prototype->normalize($v);
  366. }
  367. }
  368. return $normalized;
  369. }
  370. $normalized = array();
  371. foreach ($this->children as $name => $child) {
  372. if (!array_key_exists($name, $value)) {
  373. continue;
  374. }
  375. $normalized[$name] = $child->normalize($value[$name]);
  376. unset($value[$name]);
  377. }
  378. // if extra fields are present, throw exception
  379. if (count($value) && !$this->ignoreExtraKeys) {
  380. $msg = sprintf('Unrecognized options "%s" under "%s"', implode(', ', array_keys($value)), $this->getPath());
  381. throw new InvalidConfigurationException($msg);
  382. }
  383. return $normalized;
  384. }
  385. /**
  386. * Merges values together.
  387. *
  388. * @param mixed $leftSide The left side to merge.
  389. * @param mixed $rightSide The right side to merge.
  390. * @return mixed The merged values
  391. * @throws InvalidConfigurationException
  392. * @throws \RuntimeException
  393. */
  394. protected function mergeValues($leftSide, $rightSide)
  395. {
  396. if (false === $rightSide) {
  397. // if this is still false after the last config has been merged the
  398. // finalization pass will take care of removing this key entirely
  399. return false;
  400. }
  401. if (false === $leftSide || !$this->performDeepMerging) {
  402. return $rightSide;
  403. }
  404. foreach ($rightSide as $k => $v) {
  405. // prototype, and key is irrelevant, so simply append the element
  406. if (null !== $this->prototype && null === $this->keyAttribute) {
  407. $leftSide[] = $v;
  408. continue;
  409. }
  410. // no conflict
  411. if (!array_key_exists($k, $leftSide)) {
  412. if (!$this->allowNewKeys) {
  413. throw new InvalidConfigurationException(sprintf(
  414. 'You are not allowed to define new elements for path "%s". '
  415. .'Please define all elements for this path in one config file. '
  416. .'If you are trying to overwrite an element, make sure you redefine it '
  417. .'with the same name.',
  418. $this->getPath()
  419. ));
  420. }
  421. $leftSide[$k] = $v;
  422. continue;
  423. }
  424. if (null !== $this->prototype) {
  425. $this->prototype->setName($k);
  426. $leftSide[$k] = $this->prototype->merge($leftSide[$k], $v);
  427. } else {
  428. if (!isset($this->children[$k])) {
  429. throw new \RuntimeException('merge() expects a normalized config array.');
  430. }
  431. $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
  432. }
  433. }
  434. return $leftSide;
  435. }
  436. }