Pull merge.
[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\DuplicateKeyException;
15 use Symfony\Component\Config\Definition\Exception\Exception;
16 use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17 use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
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      * {@inheritdoc}
106      */
107     public function hasDefaultValue()
108     {
109         return true;
110     }
111
112     /**
113      * Adds default children when none are set.
114      *
115      * @param int|string|array|null $children The number of children|The child name|The children names to be added
116      */
117     public function setAddChildrenIfNoneSet($children = array('defaults'))
118     {
119         if (null === $children) {
120             $this->defaultChildren = array('defaults');
121         } else {
122             $this->defaultChildren = \is_int($children) && $children > 0 ? range(1, $children) : (array) $children;
123         }
124     }
125
126     /**
127      * {@inheritdoc}
128      *
129      * The default value could be either explicited or derived from the prototype
130      * default value.
131      */
132     public function getDefaultValue()
133     {
134         if (null !== $this->defaultChildren) {
135             $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : array();
136             $defaults = array();
137             foreach (array_values($this->defaultChildren) as $i => $name) {
138                 $defaults[null === $this->keyAttribute ? $i : $name] = $default;
139             }
140
141             return $defaults;
142         }
143
144         return $this->defaultValue;
145     }
146
147     /**
148      * Sets the node prototype.
149      */
150     public function setPrototype(PrototypeNodeInterface $node)
151     {
152         $this->prototype = $node;
153     }
154
155     /**
156      * Retrieves the prototype.
157      *
158      * @return PrototypeNodeInterface The prototype
159      */
160     public function getPrototype()
161     {
162         return $this->prototype;
163     }
164
165     /**
166      * Disable adding concrete children for prototyped nodes.
167      *
168      * @throws Exception
169      */
170     public function addChild(NodeInterface $node)
171     {
172         throw new Exception('A prototyped array node can not have concrete children.');
173     }
174
175     /**
176      * Finalizes the value of this node.
177      *
178      * @param mixed $value
179      *
180      * @return mixed The finalized value
181      *
182      * @throws UnsetKeyException
183      * @throws InvalidConfigurationException if the node doesn't have enough children
184      */
185     protected function finalizeValue($value)
186     {
187         if (false === $value) {
188             throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value)));
189         }
190
191         foreach ($value as $k => $v) {
192             $prototype = $this->getPrototypeForChild($k);
193             try {
194                 $value[$k] = $prototype->finalize($v);
195             } catch (UnsetKeyException $e) {
196                 unset($value[$k]);
197             }
198         }
199
200         if (\count($value) < $this->minNumberOfElements) {
201             $ex = new InvalidConfigurationException(sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements));
202             $ex->setPath($this->getPath());
203
204             throw $ex;
205         }
206
207         return $value;
208     }
209
210     /**
211      * Normalizes the value.
212      *
213      * @param mixed $value The value to normalize
214      *
215      * @return mixed The normalized value
216      *
217      * @throws InvalidConfigurationException
218      * @throws DuplicateKeyException
219      */
220     protected function normalizeValue($value)
221     {
222         if (false === $value) {
223             return $value;
224         }
225
226         $value = $this->remapXml($value);
227
228         $isAssoc = array_keys($value) !== range(0, \count($value) - 1);
229         $normalized = array();
230         foreach ($value as $k => $v) {
231             if (null !== $this->keyAttribute && \is_array($v)) {
232                 if (!isset($v[$this->keyAttribute]) && \is_int($k) && !$isAssoc) {
233                     $ex = new InvalidConfigurationException(sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath()));
234                     $ex->setPath($this->getPath());
235
236                     throw $ex;
237                 } elseif (isset($v[$this->keyAttribute])) {
238                     $k = $v[$this->keyAttribute];
239
240                     // remove the key attribute when required
241                     if ($this->removeKeyAttribute) {
242                         unset($v[$this->keyAttribute]);
243                     }
244
245                     // if only "value" is left
246                     if (array_keys($v) === array('value')) {
247                         $v = $v['value'];
248                         if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && array_key_exists('value', $children)) {
249                             $valuePrototype = current($this->valuePrototypes) ?: clone $children['value'];
250                             $valuePrototype->parent = $this;
251                             $originalClosures = $this->prototype->normalizationClosures;
252                             if (\is_array($originalClosures)) {
253                                 $valuePrototypeClosures = $valuePrototype->normalizationClosures;
254                                 $valuePrototype->normalizationClosures = \is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
255                             }
256                             $this->valuePrototypes[$k] = $valuePrototype;
257                         }
258                     }
259                 }
260
261                 if (array_key_exists($k, $normalized)) {
262                     $ex = new DuplicateKeyException(sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath()));
263                     $ex->setPath($this->getPath());
264
265                     throw $ex;
266                 }
267             }
268
269             $prototype = $this->getPrototypeForChild($k);
270             if (null !== $this->keyAttribute || $isAssoc) {
271                 $normalized[$k] = $prototype->normalize($v);
272             } else {
273                 $normalized[] = $prototype->normalize($v);
274             }
275         }
276
277         return $normalized;
278     }
279
280     /**
281      * Merges values together.
282      *
283      * @param mixed $leftSide  The left side to merge
284      * @param mixed $rightSide The right side to merge
285      *
286      * @return mixed The merged values
287      *
288      * @throws InvalidConfigurationException
289      * @throws \RuntimeException
290      */
291     protected function mergeValues($leftSide, $rightSide)
292     {
293         if (false === $rightSide) {
294             // if this is still false after the last config has been merged the
295             // finalization pass will take care of removing this key entirely
296             return false;
297         }
298
299         if (false === $leftSide || !$this->performDeepMerging) {
300             return $rightSide;
301         }
302
303         foreach ($rightSide as $k => $v) {
304             // prototype, and key is irrelevant, so simply append the element
305             if (null === $this->keyAttribute) {
306                 $leftSide[] = $v;
307                 continue;
308             }
309
310             // no conflict
311             if (!array_key_exists($k, $leftSide)) {
312                 if (!$this->allowNewKeys) {
313                     $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.', $this->getPath()));
314                     $ex->setPath($this->getPath());
315
316                     throw $ex;
317                 }
318
319                 $leftSide[$k] = $v;
320                 continue;
321             }
322
323             $prototype = $this->getPrototypeForChild($k);
324             $leftSide[$k] = $prototype->merge($leftSide[$k], $v);
325         }
326
327         return $leftSide;
328     }
329
330     /**
331      * Returns a prototype for the child node that is associated to $key in the value array.
332      * For general child nodes, this will be $this->prototype.
333      * But if $this->removeKeyAttribute is true and there are only two keys in the child node:
334      * one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
335      *
336      * For example, assume $this->keyAttribute is 'name' and the value array is as follows:
337      *
338      *     array(
339      *         array(
340      *             'name' => 'name001',
341      *             'value' => 'value001'
342      *         )
343      *     )
344      *
345      * Now, the key is 0 and the child node is:
346      *
347      *     array(
348      *        'name' => 'name001',
349      *        'value' => 'value001'
350      *     )
351      *
352      * When normalizing the value array, the 'name' element will removed from the child node
353      * and its value becomes the new key of the child node:
354      *
355      *     array(
356      *         'name001' => array('value' => 'value001')
357      *     )
358      *
359      * Now only 'value' element is left in the child node which can be further simplified into a string:
360      *
361      *     array('name001' => 'value001')
362      *
363      * Now, the key becomes 'name001' and the child node becomes 'value001' and
364      * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
365      *
366      * @param string $key The key of the child node
367      *
368      * @return mixed The prototype instance
369      */
370     private function getPrototypeForChild($key)
371     {
372         $prototype = isset($this->valuePrototypes[$key]) ? $this->valuePrototypes[$key] : $this->prototype;
373         $prototype->setName($key);
374
375         return $prototype;
376     }
377 }