1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Symfony\Component\Config\Definition;
13:
14: use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
15: use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
16: use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
17:
18: 19: 20: 21: 22:
23: class ArrayNode extends BaseNode implements PrototypeNodeInterface
24: {
25: protected $xmlRemappings;
26: protected $children;
27: protected $allowFalse;
28: protected $allowNewKeys;
29: protected $addIfNotSet;
30: protected $performDeepMerging;
31: protected ;
32: protected $normalizeKeys;
33:
34: 35: 36: 37: 38: 39:
40: public function __construct($name, NodeInterface $parent = null)
41: {
42: parent::__construct($name, $parent);
43:
44: $this->children = array();
45: $this->xmlRemappings = array();
46: $this->removeKeyAttribute = true;
47: $this->allowFalse = false;
48: $this->addIfNotSet = false;
49: $this->allowNewKeys = true;
50: $this->performDeepMerging = true;
51: $this->normalizeKeys = true;
52: }
53:
54: public function setNormalizeKeys($normalizeKeys)
55: {
56: $this->normalizeKeys = (Boolean) $normalizeKeys;
57: }
58:
59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71:
72: protected function preNormalize($value)
73: {
74: if (!$this->normalizeKeys || !is_array($value)) {
75: return $value;
76: }
77:
78: foreach ($value as $k => $v) {
79: if (false !== strpos($k, '-') && false === strpos($k, '_') && !array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) {
80: $value[$normalizedKey] = $v;
81: unset($value[$k]);
82: }
83: }
84:
85: return $value;
86: }
87:
88: 89: 90: 91: 92:
93: public function getChildren()
94: {
95: return $this->children;
96: }
97:
98: 99: 100: 101: 102:
103: public function setXmlRemappings(array $remappings)
104: {
105: $this->xmlRemappings = $remappings;
106: }
107:
108: 109: 110: 111: 112: 113:
114: public function setAddIfNotSet($boolean)
115: {
116: $this->addIfNotSet = (Boolean) $boolean;
117: }
118:
119: 120: 121: 122: 123:
124: public function setAllowFalse($allow)
125: {
126: $this->allowFalse = (Boolean) $allow;
127: }
128:
129: 130: 131: 132: 133:
134: public function setAllowNewKeys($allow)
135: {
136: $this->allowNewKeys = (Boolean) $allow;
137: }
138:
139: 140: 141: 142: 143:
144: public function setPerformDeepMerging($boolean)
145: {
146: $this->performDeepMerging = (Boolean) $boolean;
147: }
148:
149: 150: 151: 152: 153:
154: public function ($boolean)
155: {
156: $this->ignoreExtraKeys = (Boolean) $boolean;
157: }
158:
159: 160: 161: 162: 163:
164: public function setName($name)
165: {
166: $this->name = $name;
167: }
168:
169: 170: 171: 172: 173:
174: public function hasDefaultValue()
175: {
176: return $this->addIfNotSet;
177: }
178:
179: 180: 181: 182: 183: 184: 185:
186: public function getDefaultValue()
187: {
188: if (!$this->hasDefaultValue()) {
189: throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
190: }
191:
192: $defaults = array();
193: foreach ($this->children as $name => $child) {
194: if ($child->hasDefaultValue()) {
195: $defaults[$name] = $child->getDefaultValue();
196: }
197: }
198:
199: return $defaults;
200: }
201:
202: 203: 204: 205: 206: 207: 208: 209:
210: public function addChild(NodeInterface $node)
211: {
212: $name = $node->getName();
213: if (empty($name)) {
214: throw new \InvalidArgumentException('Child nodes must be named.');
215: }
216: if (isset($this->children[$name])) {
217: throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name));
218: }
219:
220: $this->children[$name] = $node;
221: }
222:
223: 224: 225: 226: 227: 228: 229: 230: 231: 232:
233: protected function finalizeValue($value)
234: {
235: if (false === $value) {
236: $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
237: throw new UnsetKeyException($msg);
238: }
239:
240: foreach ($this->children as $name => $child) {
241: if (!array_key_exists($name, $value)) {
242: if ($child->isRequired()) {
243: $msg = sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath());
244: $ex = new InvalidConfigurationException($msg);
245: $ex->setPath($this->getPath());
246:
247: throw $ex;
248: }
249:
250: if ($child->hasDefaultValue()) {
251: $value[$name] = $child->getDefaultValue();
252: }
253:
254: continue;
255: }
256:
257: try {
258: $value[$name] = $child->finalize($value[$name]);
259: } catch (UnsetKeyException $unset) {
260: unset($value[$name]);
261: }
262: }
263:
264: return $value;
265: }
266:
267: 268: 269: 270: 271: 272: 273:
274: protected function validateType($value)
275: {
276: if (!is_array($value) && (!$this->allowFalse || false !== $value)) {
277: $ex = new InvalidTypeException(sprintf(
278: 'Invalid type for path "%s". Expected array, but got %s',
279: $this->getPath(),
280: gettype($value)
281: ));
282: $ex->setPath($this->getPath());
283:
284: throw $ex;
285: }
286: }
287:
288: 289: 290: 291: 292: 293: 294: 295: 296:
297: protected function normalizeValue($value)
298: {
299: if (false === $value) {
300: return $value;
301: }
302:
303: $value = $this->remapXml($value);
304:
305: $normalized = array();
306: foreach ($this->children as $name => $child) {
307: if (array_key_exists($name, $value)) {
308: $normalized[$name] = $child->normalize($value[$name]);
309: unset($value[$name]);
310: }
311: }
312:
313:
314: if (count($value) && !$this->ignoreExtraKeys) {
315: $msg = sprintf('Unrecognized options "%s" under "%s"', implode(', ', array_keys($value)), $this->getPath());
316: $ex = new InvalidConfigurationException($msg);
317: $ex->setPath($this->getPath());
318:
319: throw $ex;
320: }
321:
322: return $normalized;
323: }
324:
325: 326: 327: 328: 329: 330: 331:
332: protected function remapXml($value)
333: {
334: foreach ($this->xmlRemappings as $transformation) {
335: list($singular, $plural) = $transformation;
336:
337: if (!isset($value[$singular])) {
338: continue;
339: }
340:
341: $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
342: unset($value[$singular]);
343: }
344:
345: return $value;
346: }
347:
348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358:
359: protected function mergeValues($leftSide, $rightSide)
360: {
361: if (false === $rightSide) {
362:
363:
364: return false;
365: }
366:
367: if (false === $leftSide || !$this->performDeepMerging) {
368: return $rightSide;
369: }
370:
371: foreach ($rightSide as $k => $v) {
372:
373: if (!array_key_exists($k, $leftSide)) {
374: if (!$this->allowNewKeys) {
375: $ex = new InvalidConfigurationException(sprintf(
376: 'You are not allowed to define new elements for path "%s". '
377: .'Please define all elements for this path in one config file. '
378: .'If you are trying to overwrite an element, make sure you redefine it '
379: .'with the same name.',
380: $this->getPath()
381: ));
382: $ex->setPath($this->getPath());
383:
384: throw $ex;
385: }
386:
387: $leftSide[$k] = $v;
388: continue;
389: }
390:
391: if (!isset($this->children[$k])) {
392: throw new \RuntimeException('merge() expects a normalized config array.');
393: }
394:
395: $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
396: }
397:
398: return $leftSide;
399: }
400: }
401: