ArrayNodeDefinition.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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\Builder;
  11. use Symfony\Component\Config\Definition\ArrayNode;
  12. use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
  13. use Symfony\Component\Config\Definition\PrototypedArrayNode;
  14. /**
  15. * This class provides a fluent interface for defining an array node.
  16. *
  17. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  18. */
  19. class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface
  20. {
  21. protected $performDeepMerging = true;
  22. protected $ignoreExtraKeys = false;
  23. protected $removeExtraKeys = true;
  24. protected $children = array();
  25. protected $prototype;
  26. protected $atLeastOne = false;
  27. protected $allowNewKeys = true;
  28. protected $key;
  29. protected $removeKeyItem;
  30. protected $addDefaults = false;
  31. protected $addDefaultChildren = false;
  32. protected $nodeBuilder;
  33. protected $normalizeKeys = true;
  34. /**
  35. * {@inheritdoc}
  36. */
  37. public function __construct($name, NodeParentInterface $parent = null)
  38. {
  39. parent::__construct($name, $parent);
  40. $this->nullEquivalent = array();
  41. $this->trueEquivalent = array();
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. public function setBuilder(NodeBuilder $builder)
  47. {
  48. $this->nodeBuilder = $builder;
  49. }
  50. /**
  51. * {@inheritdoc}
  52. */
  53. public function children()
  54. {
  55. return $this->getNodeBuilder();
  56. }
  57. /**
  58. * Sets a prototype for child nodes.
  59. *
  60. * @param string $type The type of node
  61. *
  62. * @return NodeDefinition
  63. */
  64. public function prototype($type)
  65. {
  66. return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this);
  67. }
  68. /**
  69. * Adds the default value if the node is not set in the configuration.
  70. *
  71. * This method is applicable to concrete nodes only (not to prototype nodes).
  72. * If this function has been called and the node is not set during the finalization
  73. * phase, it's default value will be derived from its children default values.
  74. *
  75. * @return $this
  76. */
  77. public function addDefaultsIfNotSet()
  78. {
  79. $this->addDefaults = true;
  80. return $this;
  81. }
  82. /**
  83. * Adds children with a default value when none are defined.
  84. *
  85. * This method is applicable to prototype nodes only.
  86. *
  87. * @param int|string|array|null $children The number of children|The child name|The children names to be added
  88. *
  89. * @return $this
  90. */
  91. public function addDefaultChildrenIfNoneSet($children = null)
  92. {
  93. $this->addDefaultChildren = $children;
  94. return $this;
  95. }
  96. /**
  97. * Requires the node to have at least one element.
  98. *
  99. * This method is applicable to prototype nodes only.
  100. *
  101. * @return $this
  102. */
  103. public function requiresAtLeastOneElement()
  104. {
  105. $this->atLeastOne = true;
  106. return $this;
  107. }
  108. /**
  109. * Disallows adding news keys in a subsequent configuration.
  110. *
  111. * If used all keys have to be defined in the same configuration file.
  112. *
  113. * @return $this
  114. */
  115. public function disallowNewKeysInSubsequentConfigs()
  116. {
  117. $this->allowNewKeys = false;
  118. return $this;
  119. }
  120. /**
  121. * Sets a normalization rule for XML configurations.
  122. *
  123. * @param string $singular The key to remap
  124. * @param string $plural The plural of the key for irregular plurals
  125. *
  126. * @return $this
  127. */
  128. public function fixXmlConfig($singular, $plural = null)
  129. {
  130. $this->normalization()->remap($singular, $plural);
  131. return $this;
  132. }
  133. /**
  134. * Sets the attribute which value is to be used as key.
  135. *
  136. * This is useful when you have an indexed array that should be an
  137. * associative array. You can select an item from within the array
  138. * to be the key of the particular item. For example, if "id" is the
  139. * "key", then:
  140. *
  141. * array(
  142. * array('id' => 'my_name', 'foo' => 'bar'),
  143. * );
  144. *
  145. * becomes
  146. *
  147. * array(
  148. * 'my_name' => array('foo' => 'bar'),
  149. * );
  150. *
  151. * If you'd like "'id' => 'my_name'" to still be present in the resulting
  152. * array, then you can set the second argument of this method to false.
  153. *
  154. * This method is applicable to prototype nodes only.
  155. *
  156. * @param string $name The name of the key
  157. * @param bool $removeKeyItem Whether or not the key item should be removed
  158. *
  159. * @return $this
  160. */
  161. public function useAttributeAsKey($name, $removeKeyItem = true)
  162. {
  163. $this->key = $name;
  164. $this->removeKeyItem = $removeKeyItem;
  165. return $this;
  166. }
  167. /**
  168. * Sets whether the node can be unset.
  169. *
  170. * @param bool $allow
  171. *
  172. * @return $this
  173. */
  174. public function canBeUnset($allow = true)
  175. {
  176. $this->merge()->allowUnset($allow);
  177. return $this;
  178. }
  179. /**
  180. * Adds an "enabled" boolean to enable the current section.
  181. *
  182. * By default, the section is disabled. If any configuration is specified then
  183. * the node will be automatically enabled:
  184. *
  185. * enableableArrayNode: {enabled: true, ...} # The config is enabled & default values get overridden
  186. * enableableArrayNode: ~ # The config is enabled & use the default values
  187. * enableableArrayNode: true # The config is enabled & use the default values
  188. * enableableArrayNode: {other: value, ...} # The config is enabled & default values get overridden
  189. * enableableArrayNode: {enabled: false, ...} # The config is disabled
  190. * enableableArrayNode: false # The config is disabled
  191. *
  192. * @return $this
  193. */
  194. public function canBeEnabled()
  195. {
  196. $this
  197. ->addDefaultsIfNotSet()
  198. ->treatFalseLike(array('enabled' => false))
  199. ->treatTrueLike(array('enabled' => true))
  200. ->treatNullLike(array('enabled' => true))
  201. ->beforeNormalization()
  202. ->ifArray()
  203. ->then(function ($v) {
  204. $v['enabled'] = isset($v['enabled']) ? $v['enabled'] : true;
  205. return $v;
  206. })
  207. ->end()
  208. ->children()
  209. ->booleanNode('enabled')
  210. ->defaultFalse()
  211. ;
  212. return $this;
  213. }
  214. /**
  215. * Adds an "enabled" boolean to enable the current section.
  216. *
  217. * By default, the section is enabled.
  218. *
  219. * @return $this
  220. */
  221. public function canBeDisabled()
  222. {
  223. $this
  224. ->addDefaultsIfNotSet()
  225. ->treatFalseLike(array('enabled' => false))
  226. ->treatTrueLike(array('enabled' => true))
  227. ->treatNullLike(array('enabled' => true))
  228. ->children()
  229. ->booleanNode('enabled')
  230. ->defaultTrue()
  231. ;
  232. return $this;
  233. }
  234. /**
  235. * Disables the deep merging of the node.
  236. *
  237. * @return $this
  238. */
  239. public function performNoDeepMerging()
  240. {
  241. $this->performDeepMerging = false;
  242. return $this;
  243. }
  244. /**
  245. * Allows extra config keys to be specified under an array without
  246. * throwing an exception.
  247. *
  248. * Those config values are simply ignored and removed from the
  249. * resulting array. This should be used only in special cases where
  250. * you want to send an entire configuration array through a special
  251. * tree that processes only part of the array.
  252. *
  253. * @param bool $remove Whether to remove the extra keys
  254. *
  255. * @return $this
  256. */
  257. public function ignoreExtraKeys($remove = true)
  258. {
  259. $this->ignoreExtraKeys = true;
  260. $this->removeExtraKeys = $remove;
  261. return $this;
  262. }
  263. /**
  264. * Sets key normalization.
  265. *
  266. * @param bool $bool Whether to enable key normalization
  267. *
  268. * @return $this
  269. */
  270. public function normalizeKeys($bool)
  271. {
  272. $this->normalizeKeys = (bool) $bool;
  273. return $this;
  274. }
  275. /**
  276. * {@inheritdoc}
  277. */
  278. public function append(NodeDefinition $node)
  279. {
  280. $this->children[$node->name] = $node->setParent($this);
  281. return $this;
  282. }
  283. /**
  284. * Returns a node builder to be used to add children and prototype.
  285. *
  286. * @return NodeBuilder The node builder
  287. */
  288. protected function getNodeBuilder()
  289. {
  290. if (null === $this->nodeBuilder) {
  291. $this->nodeBuilder = new NodeBuilder();
  292. }
  293. return $this->nodeBuilder->setParent($this);
  294. }
  295. /**
  296. * {@inheritdoc}
  297. */
  298. protected function createNode()
  299. {
  300. if (null === $this->prototype) {
  301. $node = new ArrayNode($this->name, $this->parent);
  302. $this->validateConcreteNode($node);
  303. $node->setAddIfNotSet($this->addDefaults);
  304. foreach ($this->children as $child) {
  305. $child->parent = $node;
  306. $node->addChild($child->getNode());
  307. }
  308. } else {
  309. $node = new PrototypedArrayNode($this->name, $this->parent);
  310. $this->validatePrototypeNode($node);
  311. if (null !== $this->key) {
  312. $node->setKeyAttribute($this->key, $this->removeKeyItem);
  313. }
  314. if (true === $this->atLeastOne) {
  315. $node->setMinNumberOfElements(1);
  316. }
  317. if ($this->default) {
  318. $node->setDefaultValue($this->defaultValue);
  319. }
  320. if (false !== $this->addDefaultChildren) {
  321. $node->setAddChildrenIfNoneSet($this->addDefaultChildren);
  322. if ($this->prototype instanceof static && null === $this->prototype->prototype) {
  323. $this->prototype->addDefaultsIfNotSet();
  324. }
  325. }
  326. $this->prototype->parent = $node;
  327. $node->setPrototype($this->prototype->getNode());
  328. }
  329. $node->setAllowNewKeys($this->allowNewKeys);
  330. $node->addEquivalentValue(null, $this->nullEquivalent);
  331. $node->addEquivalentValue(true, $this->trueEquivalent);
  332. $node->addEquivalentValue(false, $this->falseEquivalent);
  333. $node->setPerformDeepMerging($this->performDeepMerging);
  334. $node->setRequired($this->required);
  335. $node->setIgnoreExtraKeys($this->ignoreExtraKeys, $this->removeExtraKeys);
  336. $node->setNormalizeKeys($this->normalizeKeys);
  337. if (null !== $this->normalization) {
  338. $node->setNormalizationClosures($this->normalization->before);
  339. $node->setXmlRemappings($this->normalization->remappings);
  340. }
  341. if (null !== $this->merge) {
  342. $node->setAllowOverwrite($this->merge->allowOverwrite);
  343. $node->setAllowFalse($this->merge->allowFalse);
  344. }
  345. if (null !== $this->validation) {
  346. $node->setFinalValidationClosures($this->validation->rules);
  347. }
  348. return $node;
  349. }
  350. /**
  351. * Validate the configuration of a concrete node.
  352. *
  353. * @throws InvalidDefinitionException
  354. */
  355. protected function validateConcreteNode(ArrayNode $node)
  356. {
  357. $path = $node->getPath();
  358. if (null !== $this->key) {
  359. throw new InvalidDefinitionException(sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s"', $path));
  360. }
  361. if (true === $this->atLeastOne) {
  362. throw new InvalidDefinitionException(sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s"', $path));
  363. }
  364. if ($this->default) {
  365. throw new InvalidDefinitionException(sprintf('->defaultValue() is not applicable to concrete nodes at path "%s"', $path));
  366. }
  367. if (false !== $this->addDefaultChildren) {
  368. throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s"', $path));
  369. }
  370. }
  371. /**
  372. * Validate the configuration of a prototype node.
  373. *
  374. * @throws InvalidDefinitionException
  375. */
  376. protected function validatePrototypeNode(PrototypedArrayNode $node)
  377. {
  378. $path = $node->getPath();
  379. if ($this->addDefaults) {
  380. throw new InvalidDefinitionException(sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s"', $path));
  381. }
  382. if (false !== $this->addDefaultChildren) {
  383. if ($this->default) {
  384. throw new InvalidDefinitionException(sprintf('A default value and default children might not be used together at path "%s"', $path));
  385. }
  386. if (null !== $this->key && (null === $this->addDefaultChildren || \is_int($this->addDefaultChildren) && $this->addDefaultChildren > 0)) {
  387. throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s"', $path));
  388. }
  389. if (null === $this->key && (\is_string($this->addDefaultChildren) || \is_array($this->addDefaultChildren))) {
  390. throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s"', $path));
  391. }
  392. }
  393. }
  394. }