4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Config\Definition;
14 use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
15 use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
16 use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
19 * Represents an Array node in the config tree.
21 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
23 class ArrayNode extends BaseNode implements PrototypeNodeInterface
25 protected $xmlRemappings = array();
26 protected $children = array();
27 protected $allowFalse = false;
28 protected $allowNewKeys = true;
29 protected $addIfNotSet = false;
30 protected $performDeepMerging = true;
31 protected $ignoreExtraKeys = false;
32 protected $removeExtraKeys = true;
33 protected $normalizeKeys = true;
35 public function setNormalizeKeys($normalizeKeys)
37 $this->normalizeKeys = (bool) $normalizeKeys;
41 * Normalizes keys between the different configuration formats.
43 * Namely, you mostly have foo_bar in YAML while you have foo-bar in XML.
44 * After running this method, all keys are normalized to foo_bar.
46 * If you have a mixed key like foo-bar_moo, it will not be altered.
47 * The key will also not be altered if the target key already exists.
51 * @return array The value with normalized keys
53 protected function preNormalize($value)
55 if (!$this->normalizeKeys || !is_array($value)) {
59 $normalized = array();
61 foreach ($value as $k => $v) {
62 if (false !== strpos($k, '-') && false === strpos($k, '_') && !array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) {
63 $normalized[$normalizedKey] = $v;
73 * Retrieves the children of this node.
75 * @return array The children
77 public function getChildren()
79 return $this->children;
83 * Sets the xml remappings that should be performed.
85 * @param array $remappings an array of the form array(array(string, string))
87 public function setXmlRemappings(array $remappings)
89 $this->xmlRemappings = $remappings;
93 * Gets the xml remappings that should be performed.
95 * @return array $remappings an array of the form array(array(string, string))
97 public function getXmlRemappings()
99 return $this->xmlRemappings;
103 * Sets whether to add default values for this array if it has not been
104 * defined in any of the configuration files.
106 * @param bool $boolean
108 public function setAddIfNotSet($boolean)
110 $this->addIfNotSet = (bool) $boolean;
114 * Sets whether false is allowed as value indicating that the array should be unset.
118 public function setAllowFalse($allow)
120 $this->allowFalse = (bool) $allow;
124 * Sets whether new keys can be defined in subsequent configurations.
128 public function setAllowNewKeys($allow)
130 $this->allowNewKeys = (bool) $allow;
134 * Sets if deep merging should occur.
136 * @param bool $boolean
138 public function setPerformDeepMerging($boolean)
140 $this->performDeepMerging = (bool) $boolean;
144 * Whether extra keys should just be ignore without an exception.
146 * @param bool $boolean To allow extra keys
147 * @param bool $remove To remove extra keys
149 public function setIgnoreExtraKeys($boolean, $remove = true)
151 $this->ignoreExtraKeys = (bool) $boolean;
152 $this->removeExtraKeys = $this->ignoreExtraKeys && $remove;
156 * Sets the node Name.
158 * @param string $name The node's name
160 public function setName($name)
166 * Checks if the node has a default value.
170 public function hasDefaultValue()
172 return $this->addIfNotSet;
176 * Retrieves the default value.
178 * @return array The default value
180 * @throws \RuntimeException if the node has no default value
182 public function getDefaultValue()
184 if (!$this->hasDefaultValue()) {
185 throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
189 foreach ($this->children as $name => $child) {
190 if ($child->hasDefaultValue()) {
191 $defaults[$name] = $child->getDefaultValue();
201 * @param NodeInterface $node The child node to add
203 * @throws \InvalidArgumentException when the child node has no name
204 * @throws \InvalidArgumentException when the child node's name is not unique
206 public function addChild(NodeInterface $node)
208 $name = $node->getName();
209 if (!strlen($name)) {
210 throw new \InvalidArgumentException('Child nodes must be named.');
212 if (isset($this->children[$name])) {
213 throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name));
216 $this->children[$name] = $node;
220 * Finalizes the value of this node.
222 * @param mixed $value
224 * @return mixed The finalised value
226 * @throws UnsetKeyException
227 * @throws InvalidConfigurationException if the node doesn't have enough children
229 protected function finalizeValue($value)
231 if (false === $value) {
232 $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
233 throw new UnsetKeyException($msg);
236 foreach ($this->children as $name => $child) {
237 if (!array_key_exists($name, $value)) {
238 if ($child->isRequired()) {
239 $msg = sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath());
240 $ex = new InvalidConfigurationException($msg);
241 $ex->setPath($this->getPath());
246 if ($child->hasDefaultValue()) {
247 $value[$name] = $child->getDefaultValue();
254 $value[$name] = $child->finalize($value[$name]);
255 } catch (UnsetKeyException $e) {
256 unset($value[$name]);
264 * Validates the type of the value.
266 * @param mixed $value
268 * @throws InvalidTypeException
270 protected function validateType($value)
272 if (!is_array($value) && (!$this->allowFalse || false !== $value)) {
273 $ex = new InvalidTypeException(sprintf(
274 'Invalid type for path "%s". Expected array, but got %s',
278 if ($hint = $this->getInfo()) {
281 $ex->setPath($this->getPath());
288 * Normalizes the value.
290 * @param mixed $value The value to normalize
292 * @return mixed The normalized value
294 * @throws InvalidConfigurationException
296 protected function normalizeValue($value)
298 if (false === $value) {
302 $value = $this->remapXml($value);
304 $normalized = array();
305 foreach ($value as $name => $val) {
306 if (isset($this->children[$name])) {
307 $normalized[$name] = $this->children[$name]->normalize($val);
308 unset($value[$name]);
309 } elseif (!$this->removeExtraKeys) {
310 $normalized[$name] = $val;
314 // if extra fields are present, throw exception
315 if (count($value) && !$this->ignoreExtraKeys) {
316 $msg = sprintf('Unrecognized option%s "%s" under "%s"', 1 === count($value) ? '' : 's', implode(', ', array_keys($value)), $this->getPath());
317 $ex = new InvalidConfigurationException($msg);
318 $ex->setPath($this->getPath());
327 * Remaps multiple singular values to a single plural value.
329 * @param array $value The source values
331 * @return array The remapped values
333 protected function remapXml($value)
335 foreach ($this->xmlRemappings as $transformation) {
336 list($singular, $plural) = $transformation;
338 if (!isset($value[$singular])) {
342 $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
343 unset($value[$singular]);
350 * Merges values together.
352 * @param mixed $leftSide The left side to merge
353 * @param mixed $rightSide The right side to merge
355 * @return mixed The merged values
357 * @throws InvalidConfigurationException
358 * @throws \RuntimeException
360 protected function mergeValues($leftSide, $rightSide)
362 if (false === $rightSide) {
363 // if this is still false after the last config has been merged the
364 // finalization pass will take care of removing this key entirely
368 if (false === $leftSide || !$this->performDeepMerging) {
372 foreach ($rightSide as $k => $v) {
374 if (!array_key_exists($k, $leftSide)) {
375 if (!$this->allowNewKeys) {
376 $ex = new InvalidConfigurationException(sprintf(
377 'You are not allowed to define new elements for path "%s". '
378 .'Please define all elements for this path in one config file. '
379 .'If you are trying to overwrite an element, make sure you redefine it '
380 .'with the same name.',
383 $ex->setPath($this->getPath());
392 if (!isset($this->children[$k])) {
393 throw new \RuntimeException('merge() expects a normalized config array.');
396 $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);