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\DuplicateKeyException;
 16: use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
 17: use Symfony\Component\Config\Definition\Exception\Exception;
 18: 
 19:  20:  21:  22:  23: 
 24: class PrototypedArrayNode extends ArrayNode
 25: {
 26:     protected $prototype;
 27:     protected $keyAttribute;
 28:     protected $removeKeyAttribute;
 29:     protected $minNumberOfElements;
 30:     protected $defaultValue;
 31:     protected $defaultChildren;
 32: 
 33:      34:  35:  36:  37:  38: 
 39:     public function __construct($name, NodeInterface $parent = null)
 40:     {
 41:         parent::__construct($name, $parent);
 42: 
 43:         $this->minNumberOfElements = 0;
 44:         $this->defaultValue = array();
 45:     }
 46: 
 47:      48:  49:  50:  51:  52: 
 53:     public function setMinNumberOfElements($number)
 54:     {
 55:         $this->minNumberOfElements = $number;
 56:     }
 57: 
 58:      59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81: 
 82:     public function setKeyAttribute($attribute, $remove = true)
 83:     {
 84:         $this->keyAttribute = $attribute;
 85:         $this->removeKeyAttribute = $remove;
 86:     }
 87: 
 88:      89:  90:  91:  92: 
 93:     public function getKeyAttribute()
 94:     {
 95:         return $this->keyAttribute;
 96:     }
 97: 
 98:      99: 100: 101: 102: 103: 104: 
105:     public function setDefaultValue($value)
106:     {
107:         if (!is_array($value)) {
108:             throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.');
109:         }
110: 
111:         $this->defaultValue = $value;
112:     }
113: 
114:     115: 116: 117: 118: 
119:     public function hasDefaultValue()
120:     {
121:         return true;
122:     }
123: 
124:     125: 126: 127: 128: 
129:     public function setAddChildrenIfNoneSet($children = array('defaults'))
130:     {
131:         if (null === $children) {
132:             $this->defaultChildren = array('defaults');
133:         } else {
134:             $this->defaultChildren = is_integer($children) && $children > 0 ? range(1, $children) : (array) $children;
135:         }
136:     }
137: 
138:     139: 140: 141: 142: 143: 144: 145: 
146:     public function getDefaultValue()
147:     {
148:         if (null !== $this->defaultChildren) {
149:             $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : array();
150:             $defaults = array();
151:             foreach (array_values($this->defaultChildren) as $i => $name) {
152:                 $defaults[null === $this->keyAttribute ? $i : $name] = $default;
153:             }
154: 
155:             return $defaults;
156:         }
157: 
158:         return $this->defaultValue;
159:     }
160: 
161:     162: 163: 164: 165: 
166:     public function setPrototype(PrototypeNodeInterface $node)
167:     {
168:         $this->prototype = $node;
169:     }
170: 
171:     172: 173: 174: 175: 
176:     public function getPrototype()
177:     {
178:         return $this->prototype;
179:     }
180: 
181:     182: 183: 184: 185: 186: 187: 
188:     public function addChild(NodeInterface $node)
189:     {
190:         throw new Exception('A prototyped array node can not have concrete children.');
191:     }
192: 
193:     194: 195: 196: 197: 198: 199: 200: 201: 202: 
203:     protected function finalizeValue($value)
204:     {
205:         if (false === $value) {
206:             $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value));
207:             throw new UnsetKeyException($msg);
208:         }
209: 
210:         foreach ($value as $k => $v) {
211:             $this->prototype->setName($k);
212:             try {
213:                 $value[$k] = $this->prototype->finalize($v);
214:             } catch (UnsetKeyException $unset) {
215:                 unset($value[$k]);
216:             }
217:         }
218: 
219:         if (count($value) < $this->minNumberOfElements) {
220:             $msg = sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements);
221:             $ex = new InvalidConfigurationException($msg);
222:             $ex->setPath($this->getPath());
223: 
224:             throw $ex;
225:         }
226: 
227:         return $value;
228:     }
229: 
230:     231: 232: 233: 234: 235: 236: 237: 238: 239: 
240:     protected function normalizeValue($value)
241:     {
242:         if (false === $value) {
243:             return $value;
244:         }
245: 
246:         $value = $this->remapXml($value);
247: 
248:         $isAssoc = array_keys($value) !== range(0, count($value) -1);
249:         $normalized = array();
250:         foreach ($value as $k => $v) {
251:             if (null !== $this->keyAttribute && is_array($v)) {
252:                 if (!isset($v[$this->keyAttribute]) && is_int($k) && !$isAssoc) {
253:                     $msg = sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath());
254:                     $ex = new InvalidConfigurationException($msg);
255:                     $ex->setPath($this->getPath());
256: 
257:                     throw $ex;
258:                 } elseif (isset($v[$this->keyAttribute])) {
259:                     $k = $v[$this->keyAttribute];
260: 
261:                     
262:                     if ($this->removeKeyAttribute) {
263:                         unset($v[$this->keyAttribute]);
264:                     }
265: 
266:                     
267:                     if (1 == count($v) && isset($v['value'])) {
268:                         $v = $v['value'];
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:             $this->prototype->setName($k);
282:             if (null !== $this->keyAttribute || $isAssoc) {
283:                 $normalized[$k] = $this->prototype->normalize($v);
284:             } else {
285:                 $normalized[] = $this->prototype->normalize($v);
286:             }
287:         }
288: 
289:         return $normalized;
290:     }
291: 
292:     293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 
303:     protected function mergeValues($leftSide, $rightSide)
304:     {
305:         if (false === $rightSide) {
306:             
307:             
308:             return false;
309:         }
310: 
311:         if (false === $leftSide || !$this->performDeepMerging) {
312:             return $rightSide;
313:         }
314: 
315:         foreach ($rightSide as $k => $v) {
316:             
317:             if (null === $this->keyAttribute) {
318:                 $leftSide[] = $v;
319:                 continue;
320:             }
321: 
322:             
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:             $this->prototype->setName($k);
340:             $leftSide[$k] = $this->prototype->merge($leftSide[$k], $v);
341:         }
342: 
343:         return $leftSide;
344:     }
345: }
346: