Version 1
[yaffs-website] / vendor / symfony / config / Definition / PrototypedArrayNode.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Config\Definition;
13
14 use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
15 use Symfony\Component\Config\Definition\Exception\DuplicateKeyException;
16 use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
17 use Symfony\Component\Config\Definition\Exception\Exception;
18
19 /**
20  * Represents a prototyped Array node in the config tree.
21  *
22  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
23  */
24 class PrototypedArrayNode extends ArrayNode
25 {
26     protected $prototype;
27     protected $keyAttribute;
28     protected $removeKeyAttribute = false;
29     protected $minNumberOfElements = 0;
30     protected $defaultValue = array();
31     protected $defaultChildren;
32     /**
33      * @var NodeInterface[] An array of the prototypes of the simplified value children
34      */
35     private $valuePrototypes = array();
36
37     /**
38      * Sets the minimum number of elements that a prototype based node must
39      * contain. By default this is zero, meaning no elements.
40      *
41      * @param int $number
42      */
43     public function setMinNumberOfElements($number)
44     {
45         $this->minNumberOfElements = $number;
46     }
47
48     /**
49      * Sets the attribute which value is to be used as key.
50      *
51      * This is useful when you have an indexed array that should be an
52      * associative array. You can select an item from within the array
53      * to be the key of the particular item. For example, if "id" is the
54      * "key", then:
55      *
56      *     array(
57      *         array('id' => 'my_name', 'foo' => 'bar'),
58      *     );
59      *
60      *  becomes
61      *
62      *      array(
63      *          'my_name' => array('foo' => 'bar'),
64      *      );
65      *
66      * If you'd like "'id' => 'my_name'" to still be present in the resulting
67      * array, then you can set the second argument of this method to false.
68      *
69      * @param string $attribute The name of the attribute which value is to be used as a key
70      * @param bool   $remove    Whether or not to remove the key
71      */
72     public function setKeyAttribute($attribute, $remove = true)
73     {
74         $this->keyAttribute = $attribute;
75         $this->removeKeyAttribute = $remove;
76     }
77
78     /**
79      * Retrieves the name of the attribute which value should be used as key.
80      *
81      * @return string The name of the attribute
82      */
83     public function getKeyAttribute()
84     {
85         return $this->keyAttribute;
86     }
87
88     /**
89      * Sets the default value of this node.
90      *
91      * @param string $value
92      *
93      * @throws \InvalidArgumentException if the default value is not an array
94      */
95     public function setDefaultValue($value)
96     {
97         if (!is_array($value)) {
98             throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.');
99         }
100
101         $this->defaultValue = $value;
102     }
103
104     /**
105      * Checks if the node has a default value.
106      *
107      * @return bool
108      */
109     public function hasDefaultValue()
110     {
111         return true;
112     }
113
114     /**
115      * Adds default children when none are set.
116      *
117      * @param int|string|array|null $children The number of children|The child name|The children names to be added
118      */
119     public function setAddChildrenIfNoneSet($children = array('defaults'))
120     {
121         if (null === $children) {
122             $this->defaultChildren = array('defaults');
123         } else {
124             $this->defaultChildren = is_int($children) && $children > 0 ? range(1, $children) : (array) $children;
125         }
126     }
127
128     /**
129      * Retrieves the default value.
130      *
131      * The default value could be either explicited or derived from the prototype
132      * default value.
133      *
134      * @return array The default value
135      */
136     public function getDefaultValue()
137     {
138         if (null !== $this->defaultChildren) {
139             $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : array();
140             $defaults = array();
141             foreach (array_values($this->defaultChildren) as $i => $name) {
142                 $defaults[null === $this->keyAttribute ? $i : $name] = $default;
143             }
144
145             return $defaults;
146         }
147
148         return $this->defaultValue;
149     }
150
151     /**
152      * Sets the node prototype.
153      *
154      * @param PrototypeNodeInterface $node
155      */
156     public function setPrototype(PrototypeNodeInterface $node)
157     {
158         $this->prototype = $node;
159     }
160
161     /**
162      * Retrieves the prototype.
163      *
164      * @return PrototypeNodeInterface The prototype
165      */
166     public function getPrototype()
167     {
168         return $this->prototype;
169     }
170
171     /**
172      * Disable adding concrete children for prototyped nodes.
173      *
174      * @param NodeInterface $node The child node to add
175      *
176      * @throws Exception
177      */
178     public function addChild(NodeInterface $node)
179     {
180         throw new Exception('A prototyped array node can not have concrete children.');
181     }
182
183     /**
184      * Finalizes the value of this node.
185      *
186      * @param mixed $value
187      *
188      * @return mixed The finalized value
189      *
190      * @throws UnsetKeyException
191      * @throws InvalidConfigurationException if the node doesn't have enough children
192      */
193     protected function finalizeValue($value)
194     {
195         if (false === $value) {
196             $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
197             throw new UnsetKeyException($msg);
198         }
199
200         foreach ($value as $k => $v) {
201             $prototype = $this->getPrototypeForChild($k);
202             try {
203                 $value[$k] = $prototype->finalize($v);
204             } catch (UnsetKeyException $e) {
205                 unset($value[$k]);
206             }
207         }
208
209         if (count($value) < $this->minNumberOfElements) {
210             $msg = sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements);
211             $ex = new InvalidConfigurationException($msg);
212             $ex->setPath($this->getPath());
213
214             throw $ex;
215         }
216
217         return $value;
218     }
219
220     /**
221      * Normalizes the value.
222      *
223      * @param mixed $value The value to normalize
224      *
225      * @return mixed The normalized value
226      *
227      * @throws InvalidConfigurationException
228      * @throws DuplicateKeyException
229      */
230     protected function normalizeValue($value)
231     {
232         if (false === $value) {
233             return $value;
234         }
235
236         $value = $this->remapXml($value);
237
238         $isAssoc = array_keys($value) !== range(0, count($value) - 1);
239         $normalized = array();
240         foreach ($value as $k => $v) {
241             if (null !== $this->keyAttribute && is_array($v)) {
242                 if (!isset($v[$this->keyAttribute]) && is_int($k) && !$isAssoc) {
243                     $msg = sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath());
244                     $ex = new InvalidConfigurationException($msg);
245                     $ex->setPath($this->getPath());
246
247                     throw $ex;
248                 } elseif (isset($v[$this->keyAttribute])) {
249                     $k = $v[$this->keyAttribute];
250
251                     // remove the key attribute when required
252                     if ($this->removeKeyAttribute) {
253                         unset($v[$this->keyAttribute]);
254                     }
255
256                     // if only "value" is left
257                     if (array_keys($v) === array('value')) {
258                         $v = $v['value'];
259                         if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && array_key_exists('value', $children)) {
260                             $valuePrototype = current($this->valuePrototypes) ?: clone $children['value'];
261                             $valuePrototype->parent = $this;
262                             $originalClosures = $this->prototype->normalizationClosures;
263                             if (is_array($originalClosures)) {
264                                 $valuePrototypeClosures = $valuePrototype->normalizationClosures;
265                                 $valuePrototype->normalizationClosures = is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
266                             }
267                             $this->valuePrototypes[$k] = $valuePrototype;
268                         }
269                     }
270                 }
271
272                 if (array_key_exists($k, $normalized)) {
273                     $msg = sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath());
274                     $ex = new DuplicateKeyException($msg);
275                     $ex->setPath($this->getPath());
276
277                     throw $ex;
278                 }
279             }
280
281             $prototype = $this->getPrototypeForChild($k);
282             if (null !== $this->keyAttribute || $isAssoc) {
283                 $normalized[$k] = $prototype->normalize($v);
284             } else {
285                 $normalized[] = $prototype->normalize($v);
286             }
287         }
288
289         return $normalized;
290     }
291
292     /**
293      * Merges values together.
294      *
295      * @param mixed $leftSide  The left side to merge
296      * @param mixed $rightSide The right side to merge
297      *
298      * @return mixed The merged values
299      *
300      * @throws InvalidConfigurationException
301      * @throws \RuntimeException
302      */
303     protected function mergeValues($leftSide, $rightSide)
304     {
305         if (false === $rightSide) {
306             // if this is still false after the last config has been merged the
307             // finalization pass will take care of removing this key entirely
308             return false;
309         }
310
311         if (false === $leftSide || !$this->performDeepMerging) {
312             return $rightSide;
313         }
314
315         foreach ($rightSide as $k => $v) {
316             // prototype, and key is irrelevant, so simply append the element
317             if (null === $this->keyAttribute) {
318                 $leftSide[] = $v;
319                 continue;
320             }
321
322             // no conflict
323             if (!array_key_exists($k, $leftSide)) {
324                 if (!$this->allowNewKeys) {
325                     $ex = new InvalidConfigurationException(sprintf(
326                         'You are not allowed to define new elements for path "%s". '.
327                         'Please define all elements for this path in one config file.',
328                         $this->getPath()
329                     ));
330                     $ex->setPath($this->getPath());
331
332                     throw $ex;
333                 }
334
335                 $leftSide[$k] = $v;
336                 continue;
337             }
338
339             $prototype = $this->getPrototypeForChild($k);
340             $leftSide[$k] = $prototype->merge($leftSide[$k], $v);
341         }
342
343         return $leftSide;
344     }
345
346     /**
347      * Returns a prototype for the child node that is associated to $key in the value array.
348      * For general child nodes, this will be $this->prototype.
349      * But if $this->removeKeyAttribute is true and there are only two keys in the child node:
350      * one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
351      *
352      * For example, assume $this->keyAttribute is 'name' and the value array is as follows:
353      * array(
354      *     array(
355      *         'name' => 'name001',
356      *         'value' => 'value001'
357      *     )
358      * )
359      *
360      * Now, the key is 0 and the child node is:
361      * array(
362      *    'name' => 'name001',
363      *    'value' => 'value001'
364      * )
365      *
366      * When normalizing the value array, the 'name' element will removed from the child node
367      * and its value becomes the new key of the child node:
368      * array(
369      *     'name001' => array('value' => 'value001')
370      * )
371      *
372      * Now only 'value' element is left in the child node which can be further simplified into a string:
373      * array('name001' => 'value001')
374      *
375      * Now, the key becomes 'name001' and the child node becomes 'value001' and
376      * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
377      *
378      * @param string $key The key of the child node
379      *
380      * @return mixed The prototype instance
381      */
382     private function getPrototypeForChild($key)
383     {
384         $prototype = isset($this->valuePrototypes[$key]) ? $this->valuePrototypes[$key] : $this->prototype;
385         $prototype->setName($key);
386
387         return $prototype;
388     }
389 }