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;
158 public function setName($name)
166 public function hasDefaultValue()
168 return $this->addIfNotSet;
174 public function getDefaultValue()
176 if (!$this->hasDefaultValue()) {
177 throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
181 foreach ($this->children as $name => $child) {
182 if ($child->hasDefaultValue()) {
183 $defaults[$name] = $child->getDefaultValue();
193 * @throws \InvalidArgumentException when the child node has no name
194 * @throws \InvalidArgumentException when the child node's name is not unique
196 public function addChild(NodeInterface $node)
198 $name = $node->getName();
199 if (!\strlen($name)) {
200 throw new \InvalidArgumentException('Child nodes must be named.');
202 if (isset($this->children[$name])) {
203 throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name));
206 $this->children[$name] = $node;
210 * Finalizes the value of this node.
212 * @param mixed $value
214 * @return mixed The finalised value
216 * @throws UnsetKeyException
217 * @throws InvalidConfigurationException if the node doesn't have enough children
219 protected function finalizeValue($value)
221 if (false === $value) {
222 throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value)));
225 foreach ($this->children as $name => $child) {
226 if (!array_key_exists($name, $value)) {
227 if ($child->isRequired()) {
228 $ex = new InvalidConfigurationException(sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath()));
229 $ex->setPath($this->getPath());
234 if ($child->hasDefaultValue()) {
235 $value[$name] = $child->getDefaultValue();
241 if ($child->isDeprecated()) {
242 @trigger_error($child->getDeprecationMessage($name, $this->getPath()), E_USER_DEPRECATED);
246 $value[$name] = $child->finalize($value[$name]);
247 } catch (UnsetKeyException $e) {
248 unset($value[$name]);
256 * Validates the type of the value.
258 * @param mixed $value
260 * @throws InvalidTypeException
262 protected function validateType($value)
264 if (!\is_array($value) && (!$this->allowFalse || false !== $value)) {
265 $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected array, but got %s', $this->getPath(), \gettype($value)));
266 if ($hint = $this->getInfo()) {
269 $ex->setPath($this->getPath());
276 * Normalizes the value.
278 * @param mixed $value The value to normalize
280 * @return mixed The normalized value
282 * @throws InvalidConfigurationException
284 protected function normalizeValue($value)
286 if (false === $value) {
290 $value = $this->remapXml($value);
292 $normalized = array();
293 foreach ($value as $name => $val) {
294 if (isset($this->children[$name])) {
295 $normalized[$name] = $this->children[$name]->normalize($val);
296 unset($value[$name]);
297 } elseif (!$this->removeExtraKeys) {
298 $normalized[$name] = $val;
302 // if extra fields are present, throw exception
303 if (\count($value) && !$this->ignoreExtraKeys) {
304 $ex = new InvalidConfigurationException(sprintf('Unrecognized option%s "%s" under "%s"', 1 === \count($value) ? '' : 's', implode(', ', array_keys($value)), $this->getPath()));
305 $ex->setPath($this->getPath());
314 * Remaps multiple singular values to a single plural value.
316 * @param array $value The source values
318 * @return array The remapped values
320 protected function remapXml($value)
322 foreach ($this->xmlRemappings as list($singular, $plural)) {
323 if (!isset($value[$singular])) {
327 $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
328 unset($value[$singular]);
335 * Merges values together.
337 * @param mixed $leftSide The left side to merge
338 * @param mixed $rightSide The right side to merge
340 * @return mixed The merged values
342 * @throws InvalidConfigurationException
343 * @throws \RuntimeException
345 protected function mergeValues($leftSide, $rightSide)
347 if (false === $rightSide) {
348 // if this is still false after the last config has been merged the
349 // finalization pass will take care of removing this key entirely
353 if (false === $leftSide || !$this->performDeepMerging) {
357 foreach ($rightSide as $k => $v) {
359 if (!array_key_exists($k, $leftSide)) {
360 if (!$this->allowNewKeys) {
361 $ex = new InvalidConfigurationException(sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file. If you are trying to overwrite an element, make sure you redefine it with the same name.', $this->getPath()));
362 $ex->setPath($this->getPath());
371 if (!isset($this->children[$k])) {
372 throw new \RuntimeException('merge() expects a normalized config array.');
375 $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);