Updated to Drupal 8.5. Core Media not yet in use.
[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      * {@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             $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
189             throw new UnsetKeyException($msg);
190         }
191
192         foreach ($value as $k => $v) {
193             $prototype = $this->getPrototypeForChild($k);
194             try {
195                 $value[$k] = $prototype->finalize($v);
196             } catch (UnsetKeyException $e) {
197                 unset($value[$k]);
198             }
199         }
200
201         if (count($value) < $this->minNumberOfElements) {
202             $msg = sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements);
203             $ex = new InvalidConfigurationException($msg);
204             $ex->setPath($this->getPath());
205
206             throw $ex;
207         }
208
209         return $value;
210     }
211
212     /**
213      * Normalizes the value.
214      *
215      * @param mixed $value The value to normalize
216      *
217      * @return mixed The normalized value
218      *
219      * @throws InvalidConfigurationException
220      * @throws DuplicateKeyException
221      */
222     protected function normalizeValue($value)
223     {
224         if (false === $value) {
225             return $value;
226         }
227
228         $value = $this->remapXml($value);
229
230         $isAssoc = array_keys($value) !== range(0, count($value) - 1);
231         $normalized = array();
232         foreach ($value as $k => $v) {
233             if (null !== $this->keyAttribute && is_array($v)) {
234                 if (!isset($v[$this->keyAttribute]) && is_int($k) && !$isAssoc) {
235                     $msg = sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath());
236                     $ex = new InvalidConfigurationException($msg);
237                     $ex->setPath($this->getPath());
238
239                     throw $ex;
240                 } elseif (isset($v[$this->keyAttribute])) {
241                     $k = $v[$this->keyAttribute];
242
243                     // remove the key attribute when required
244                     if ($this->removeKeyAttribute) {
245                         unset($v[$this->keyAttribute]);
246                     }
247
248                     // if only "value" is left
249                     if (array_keys($v) === array('value')) {
250                         $v = $v['value'];
251                         if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && array_key_exists('value', $children)) {
252                             $valuePrototype = current($this->valuePrototypes) ?: clone $children['value'];
253                             $valuePrototype->parent = $this;
254                             $originalClosures = $this->prototype->normalizationClosures;
255                             if (is_array($originalClosures)) {
256                                 $valuePrototypeClosures = $valuePrototype->normalizationClosures;
257                                 $valuePrototype->normalizationClosures = is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
258                             }
259                             $this->valuePrototypes[$k] = $valuePrototype;
260                         }
261                     }
262                 }
263
264                 if (array_key_exists($k, $normalized)) {
265                     $msg = sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath());
266                     $ex = new DuplicateKeyException($msg);
267                     $ex->setPath($this->getPath());
268
269                     throw $ex;
270                 }
271             }
272
273             $prototype = $this->getPrototypeForChild($k);
274             if (null !== $this->keyAttribute || $isAssoc) {
275                 $normalized[$k] = $prototype->normalize($v);
276             } else {
277                 $normalized[] = $prototype->normalize($v);
278             }
279         }
280
281         return $normalized;
282     }
283
284     /**
285      * Merges values together.
286      *
287      * @param mixed $leftSide  The left side to merge
288      * @param mixed $rightSide The right side to merge
289      *
290      * @return mixed The merged values
291      *
292      * @throws InvalidConfigurationException
293      * @throws \RuntimeException
294      */
295     protected function mergeValues($leftSide, $rightSide)
296     {
297         if (false === $rightSide) {
298             // if this is still false after the last config has been merged the
299             // finalization pass will take care of removing this key entirely
300             return false;
301         }
302
303         if (false === $leftSide || !$this->performDeepMerging) {
304             return $rightSide;
305         }
306
307         foreach ($rightSide as $k => $v) {
308             // prototype, and key is irrelevant, so simply append the element
309             if (null === $this->keyAttribute) {
310                 $leftSide[] = $v;
311                 continue;
312             }
313
314             // no conflict
315             if (!array_key_exists($k, $leftSide)) {
316                 if (!$this->allowNewKeys) {
317                     $ex = new InvalidConfigurationException(sprintf(
318                         'You are not allowed to define new elements for path "%s". '.
319                         'Please define all elements for this path in one config file.',
320                         $this->getPath()
321                     ));
322                     $ex->setPath($this->getPath());
323
324                     throw $ex;
325                 }
326
327                 $leftSide[$k] = $v;
328                 continue;
329             }
330
331             $prototype = $this->getPrototypeForChild($k);
332             $leftSide[$k] = $prototype->merge($leftSide[$k], $v);
333         }
334
335         return $leftSide;
336     }
337
338     /**
339      * Returns a prototype for the child node that is associated to $key in the value array.
340      * For general child nodes, this will be $this->prototype.
341      * But if $this->removeKeyAttribute is true and there are only two keys in the child node:
342      * one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
343      *
344      * For example, assume $this->keyAttribute is 'name' and the value array is as follows:
345      * array(
346      *     array(
347      *         'name' => 'name001',
348      *         'value' => 'value001'
349      *     )
350      * )
351      *
352      * Now, the key is 0 and the child node is:
353      * array(
354      *    'name' => 'name001',
355      *    'value' => 'value001'
356      * )
357      *
358      * When normalizing the value array, the 'name' element will removed from the child node
359      * and its value becomes the new key of the child node:
360      * array(
361      *     'name001' => array('value' => 'value001')
362      * )
363      *
364      * Now only 'value' element is left in the child node which can be further simplified into a string:
365      * array('name001' => 'value001')
366      *
367      * Now, the key becomes 'name001' and the child node becomes 'value001' and
368      * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
369      *
370      * @param string $key The key of the child node
371      *
372      * @return mixed The prototype instance
373      */
374     private function getPrototypeForChild($key)
375     {
376         $prototype = isset($this->valuePrototypes[$key]) ? $this->valuePrototypes[$key] : $this->prototype;
377         $prototype->setName($key);
378
379         return $prototype;
380     }
381 }