1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @package toolkit |
4
|
|
|
*/ |
5
|
|
|
/** |
6
|
|
|
* `XMLElement` is a class used to simulate PHP's `DOMElement` |
7
|
|
|
* class. Each object is a representation of a XML element |
8
|
|
|
* and can store it's children in an array. When an `XMLElement` |
9
|
|
|
* is generated, it is output as an XML string. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
class XMLElement implements IteratorAggregate |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* This is an array of HTML elements that are self closing. |
16
|
|
|
* @var array |
17
|
|
|
*/ |
18
|
|
|
protected static $no_end_tags = [ |
19
|
|
|
'area', |
20
|
|
|
'base', |
21
|
|
|
'br', |
22
|
|
|
'col', |
23
|
|
|
'hr', |
24
|
|
|
'img', |
25
|
|
|
'input', |
26
|
|
|
'link', |
27
|
|
|
'meta', |
28
|
|
|
'param', |
29
|
|
|
]; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The name of the HTML Element, eg. 'p' |
33
|
|
|
* @var string |
34
|
|
|
*/ |
35
|
|
|
private $name; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The value of this `XMLElement` as an array or a string |
39
|
|
|
* @var string|array |
40
|
|
|
*/ |
41
|
|
|
private $value = []; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Any additional attributes can be included in an associative array |
45
|
|
|
* with the key being the name and the value being the value of the |
46
|
|
|
* attribute. |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
private $attributes = []; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Children of this `XMLElement`, which will also be `XMLElement`'s |
53
|
|
|
* @var array |
54
|
|
|
*/ |
55
|
|
|
private $children = []; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* The type of element, defaults to 'xml'. Used when determining the style |
59
|
|
|
* of end tag for this element when generated |
60
|
|
|
* @var string |
61
|
|
|
*/ |
62
|
|
|
private $elementStyle = 'xml'; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Specifies whether this HTML element has an closing element, or if |
66
|
|
|
* it self closing. Defaults to `true`. |
67
|
|
|
* eg. `<p></p>` or `<input />` |
68
|
|
|
* @var boolean |
69
|
|
|
*/ |
70
|
|
|
private $selfClosing = true; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Specifies whether attributes need to have a value or if they can |
74
|
|
|
* be shorthand. Defaults to `true`. An example of this would be: |
75
|
|
|
* `<option selected>Value</option>` |
76
|
|
|
* @var boolean |
77
|
|
|
*/ |
78
|
|
|
private $allowEmptyAttributes = true; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* The constructor for the `XMLElement` |
82
|
|
|
* |
83
|
|
|
* @param string $name |
84
|
|
|
* The name of the `XMLElement`, 'p'. |
85
|
|
|
* @param string|XMLElement $value (optional) |
86
|
|
|
* The value of this `XMLElement`, it can be a string |
87
|
|
|
* or another `XMLElement` object. |
88
|
|
|
* @param array $attributes (optional) |
89
|
|
|
* Any additional attributes can be included in an associative array with |
90
|
|
|
* the key being the name and the value being the value of the attribute. |
91
|
|
|
* Attributes set from this array will override existing attributes |
92
|
|
|
* set by previous params. |
93
|
|
|
* @param boolean $createHandle |
94
|
|
|
* Whether this function should convert the `$name` to a handle. Defaults to |
95
|
|
|
* `false`. |
96
|
|
|
*/ |
97
|
|
|
public function __construct($name, $value = null, array $attributes = [], $createHandle = false) |
98
|
|
|
{ |
99
|
|
|
$this->setName($name, $createHandle); |
100
|
|
|
$this->setValue($value); |
101
|
|
|
|
102
|
|
|
if (is_array($attributes) && !empty($attributes)) { |
103
|
|
|
$this->setAttributeArray($attributes); |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Accessor for `$name` |
109
|
|
|
* |
110
|
|
|
* @return string |
111
|
|
|
*/ |
112
|
|
|
public function getName() |
113
|
|
|
{ |
114
|
|
|
return $this->name; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Accessor for `$value`, converted to a string |
119
|
|
|
* |
120
|
|
|
* @return string |
121
|
|
|
*/ |
122
|
|
|
public function getValue() |
123
|
|
|
{ |
124
|
|
|
$value = ''; |
125
|
|
|
$values = $this->value; |
126
|
|
|
|
127
|
|
|
if (!is_array($values)) { |
128
|
|
|
$values = [$values]; |
129
|
|
|
} |
130
|
|
|
foreach ($values as $v) { |
131
|
|
|
if ($v instanceof XMLElement) { |
132
|
|
|
$value .= $v->generate(); |
133
|
|
|
} elseif ($v) { |
134
|
|
|
$value .= $v; |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
return $value; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Retrieves the value of an attribute by name |
143
|
|
|
* |
144
|
|
|
* @param string $name |
145
|
|
|
* @return string |
146
|
|
|
*/ |
147
|
|
|
public function getAttribute($name) |
148
|
|
|
{ |
149
|
|
|
if (!isset($this->attributes[$name])) { |
150
|
|
|
return null; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return $this->attributes[$name]; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Accessor for `$this->attributes` |
158
|
|
|
* |
159
|
|
|
* @return array |
160
|
|
|
*/ |
161
|
|
|
public function getAttributes() |
162
|
|
|
{ |
163
|
|
|
return $this->attributes; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Retrieves a child-element by position |
168
|
|
|
* |
169
|
|
|
* @since Symphony 2.3 |
170
|
|
|
* @param integer $position |
171
|
|
|
* @return XMLElement |
172
|
|
|
*/ |
173
|
|
|
public function getChild($position) |
174
|
|
|
{ |
175
|
|
|
if (!isset($this->children[$this->getRealIndex($position)])) { |
176
|
|
|
return null; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
return $this->children[$this->getRealIndex($position)]; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Accessor for `$this->children` |
184
|
|
|
* |
185
|
|
|
* @return array |
186
|
|
|
*/ |
187
|
|
|
public function getChildren() |
188
|
|
|
{ |
189
|
|
|
return $this->children; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Accessor for `$this->children`, returning only `XMLElement` children, |
194
|
|
|
* not text nodes. |
195
|
|
|
* |
196
|
|
|
* @return XMLElementChildrenFilter |
197
|
|
|
*/ |
198
|
|
|
public function getIterator() |
199
|
|
|
{ |
200
|
|
|
return new XMLElementChildrenFilter(new ArrayIterator($this->children)); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Retrieves child-element by name and position. If no child is found, |
205
|
|
|
* `NULL` will be returned. |
206
|
|
|
* |
207
|
|
|
* @since Symphony 2.3 |
208
|
|
|
* @param string $name |
209
|
|
|
* @param integer $position |
210
|
|
|
* @return XMLElement |
211
|
|
|
*/ |
212
|
|
|
public function getChildByName($name, $position) |
213
|
|
|
{ |
214
|
|
|
$result = array_values($this->getChildrenByName($name)); |
215
|
|
|
|
216
|
|
|
if (!isset($result[$position])) { |
217
|
|
|
return null; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return $result[$position]; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Accessor to return an associative array of all `$this->children` |
225
|
|
|
* whose's name matches the given `$name`. If no children are found, |
226
|
|
|
* an empty array will be returned. |
227
|
|
|
* |
228
|
|
|
* @since Symphony 2.2.2 |
229
|
|
|
* @param string $name |
230
|
|
|
* @return array |
231
|
|
|
* An associative array where the key is the `$index` of the child |
232
|
|
|
* in `$this->children` |
233
|
|
|
*/ |
234
|
|
|
public function getChildrenByName($name) |
235
|
|
|
{ |
236
|
|
|
$result = []; |
237
|
|
|
|
238
|
|
|
foreach ($this as $i => $child) { |
239
|
|
|
if ($child->getName() != $name) { |
240
|
|
|
continue; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
$result[$i] = $child; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return $result; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Accessor for `$elementStyle` |
251
|
|
|
* |
252
|
|
|
* @return string |
253
|
|
|
*/ |
254
|
|
|
public function getElementStyle() |
255
|
|
|
{ |
256
|
|
|
return $this->elementStyle; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Sets the style of the `XMLElement`. Used when the |
261
|
|
|
* `XMLElement` is being generated to determine whether |
262
|
|
|
* needs to be closed, is self closing or is standalone. |
263
|
|
|
* |
264
|
|
|
* @param string $style |
265
|
|
|
* If not 'xml', will trigger the |
266
|
|
|
* XMLElement to be closed by itself or left standalone. |
267
|
|
|
* @return XMLElement |
268
|
|
|
* The current instance |
269
|
|
|
*/ |
270
|
|
|
public function setElementStyle($style) |
271
|
|
|
{ |
272
|
|
|
$this->elementStyle = $style; |
273
|
|
|
return $this; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Sets whether this `XMLElement` is self closing or not. |
278
|
|
|
* |
279
|
|
|
* @param bool $value |
280
|
|
|
* @return XMLElement |
281
|
|
|
* The current instance |
282
|
|
|
*/ |
283
|
|
|
public function setSelfClosingTag($value) |
284
|
|
|
{ |
285
|
|
|
$this->selfClosing = $value; |
286
|
|
|
return $this; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Makes this `XMLElement` to self close. |
291
|
|
|
* |
292
|
|
|
* @since Symphony 3.0.0 |
293
|
|
|
* @uses setSelfClosingTag() |
294
|
|
|
* @return XMLElement |
295
|
|
|
* The current instance |
296
|
|
|
*/ |
297
|
|
|
public function renderSelfClosingTag() |
298
|
|
|
{ |
299
|
|
|
return $this->setSelfClosingTag(true); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Specifies whether attributes need to have a value |
304
|
|
|
* or if they can be shorthand on this `XMLElement`. |
305
|
|
|
* |
306
|
|
|
* @param bool $value |
307
|
|
|
* @return XMLElement |
308
|
|
|
* The current instance |
309
|
|
|
*/ |
310
|
|
|
public function setAllowEmptyAttributes($value) |
311
|
|
|
{ |
312
|
|
|
$this->allowEmptyAttributes = $value; |
313
|
|
|
return $this; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Makes this `XMLElement` render empty attributes. |
318
|
|
|
* |
319
|
|
|
* @since Symphony 3.0.0 |
320
|
|
|
* @uses setAllowEmptyAttributes() |
321
|
|
|
* @return XMLElement |
322
|
|
|
* The current instance |
323
|
|
|
*/ |
324
|
|
|
public function renderEmptyAttributes() |
325
|
|
|
{ |
326
|
|
|
return $this->setAllowEmptyAttributes(true); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Sets the name of this `XMLElement`, ie. 'p' => <p /> |
331
|
|
|
* |
332
|
|
|
* @since Symphony 2.3.2 |
333
|
|
|
* @param string $name |
334
|
|
|
* The name of the `XMLElement`, 'p'. |
335
|
|
|
* @param boolean $createHandle |
336
|
|
|
* Whether this function should convert the `$name` to a handle. |
337
|
|
|
* Defaults to `false`. |
338
|
|
|
* @return XMLElement |
339
|
|
|
* The current instance |
340
|
|
|
*/ |
341
|
|
|
public function setName($name, $createHandle = false) |
342
|
|
|
{ |
343
|
|
|
$this->name = ($createHandle) ? Lang::createHandle($name) : $name; |
344
|
|
|
return $this; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Sets and appends the value of the `XMLElement`. |
349
|
|
|
* |
350
|
|
|
* @param string|XMLElement|array $value |
351
|
|
|
* @return XMLElement |
352
|
|
|
* The current instance |
353
|
|
|
*/ |
354
|
|
|
public function setValue($value) |
355
|
|
|
{ |
356
|
|
|
if (is_array($value)) { |
357
|
|
|
$value = implode(', ', $value); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if (!is_null($value)) { |
361
|
|
|
$this->value = $value; |
|
|
|
|
362
|
|
|
$this->appendChild($value); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
return $this; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* This function will remove all text attributes from the `XMLElement` node |
370
|
|
|
* and replace them with the given value. |
371
|
|
|
* |
372
|
|
|
* @since Symphony 2.4 |
373
|
|
|
* @param string|XMLElement|array $value |
374
|
|
|
* @return XMLElement |
375
|
|
|
* The current instance |
376
|
|
|
*/ |
377
|
|
|
public function replaceValue($value) |
378
|
|
|
{ |
379
|
|
|
foreach ($this->children as $i => $child) { |
380
|
|
|
if ($child instanceof XMLElement) { |
381
|
|
|
continue; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
unset($this->children[$i]); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
$this->setValue($value); |
388
|
|
|
|
389
|
|
|
return $this; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Sets an attribute |
394
|
|
|
* |
395
|
|
|
* @param string $name |
396
|
|
|
* The name of the attribute |
397
|
|
|
* @param string $value |
398
|
|
|
* The value of the attribute |
399
|
|
|
* @return XMLElement |
400
|
|
|
* The current instance |
401
|
|
|
*/ |
402
|
|
|
public function setAttribute($name, $value) |
403
|
|
|
{ |
404
|
|
|
$this->attributes[$name] = $value; |
405
|
|
|
return $this; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* A convenience method to quickly add multiple attributes to |
410
|
|
|
* an `XMLElement` |
411
|
|
|
* |
412
|
|
|
* @param array $attributes |
413
|
|
|
* Associative array with the key being the name and |
414
|
|
|
* the value being the value of the attribute. |
415
|
|
|
* @return XMLElement |
416
|
|
|
* The current instance |
417
|
|
|
*/ |
418
|
|
|
public function setAttributeArray(array $attributes) |
419
|
|
|
{ |
420
|
|
|
foreach ($attributes as $name => $value) { |
421
|
|
|
$this->setAttribute($name, $value); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
return $this; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* A convenience method that encapsulate validation of a child node. |
429
|
|
|
* This should prevent generate errors by catching them earlier. |
430
|
|
|
* |
431
|
|
|
* @since Symphony 2.5.0 |
432
|
|
|
* @param XMLElement $child |
433
|
|
|
* The child to validate |
434
|
|
|
* @return XMLElement |
435
|
|
|
* The current instance |
436
|
|
|
* @throws Exception |
437
|
|
|
* If the child is not valid |
438
|
|
|
*/ |
439
|
|
|
protected function validateChild($child) |
440
|
|
|
{ |
441
|
|
|
if ($this === $child) { |
442
|
|
|
throw new Exception(__('Can not add the element itself as one of its child')); |
443
|
|
|
} elseif ($child instanceof XMLDocument) { |
444
|
|
|
throw new Exception(__('Can not add an `XMLDocument` object as a child')); |
445
|
|
|
} |
446
|
|
|
return $this; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* This function expects an array of `XMLElement` that will completely |
451
|
|
|
* replace the contents of `$this->children`. Take care when using |
452
|
|
|
* this function. |
453
|
|
|
* |
454
|
|
|
* @since Symphony 2.2.2 |
455
|
|
|
* @uses validateChild() |
456
|
|
|
* @param array $children |
457
|
|
|
* An array of XMLElement's to act as the children for the current |
458
|
|
|
* XMLElement instance |
459
|
|
|
* @return XMLElement |
460
|
|
|
* The current instance |
461
|
|
|
*/ |
462
|
|
|
public function setChildren(array $children) |
463
|
|
|
{ |
464
|
|
|
foreach ($children as $child) { |
465
|
|
|
$this->validateChild($child); |
466
|
|
|
} |
467
|
|
|
$this->children = $children; |
468
|
|
|
return $this; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* Adds an `XMLElement` to the children array |
473
|
|
|
* |
474
|
|
|
* @uses validateChild() |
475
|
|
|
* @param XMLElement|string $child |
476
|
|
|
* @return XMLElement |
477
|
|
|
* The current instance |
478
|
|
|
*/ |
479
|
|
|
public function appendChild($child) |
480
|
|
|
{ |
481
|
|
|
$this->validateChild($child); |
|
|
|
|
482
|
|
|
$this->children[] = $child; |
483
|
|
|
return $this; |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
/** |
487
|
|
|
* A convenience method to add children to an `XMLElement` |
488
|
|
|
* quickly. |
489
|
|
|
* |
490
|
|
|
* @uses appendChild() |
491
|
|
|
* @param array $children |
492
|
|
|
* @return XMLElement |
493
|
|
|
* The current instance |
494
|
|
|
*/ |
495
|
|
|
public function appendChildArray(array $children) |
496
|
|
|
{ |
497
|
|
|
foreach ($children as $child) { |
498
|
|
|
$this->appendChild($child); |
499
|
|
|
} |
500
|
|
|
return $this; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Adds an `XMLElement` to the start of the children |
505
|
|
|
* array, this will mean it is output before any other |
506
|
|
|
* children when the `XMLElement` is generated |
507
|
|
|
* |
508
|
|
|
* @uses validateChild() |
509
|
|
|
* @param XMLElement $child |
510
|
|
|
* @return XMLElement |
511
|
|
|
* The current instance |
512
|
|
|
*/ |
513
|
|
|
public function prependChild(XMLElement $child) |
514
|
|
|
{ |
515
|
|
|
$this->validateChild($child); |
516
|
|
|
array_unshift($this->children, $child); |
517
|
|
|
return $this; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* A convenience method to quickly add a CSS class to this `XMLElement`'s |
522
|
|
|
* existing class attribute. If the attribute does not exist, it will |
523
|
|
|
* be created. |
524
|
|
|
* It also make sure that classes are separated by a single space. |
525
|
|
|
* |
526
|
|
|
* @since Symphony 2.2.2 |
527
|
|
|
* @uses setAttribute() |
528
|
|
|
* @param string $class |
529
|
|
|
* The CSS class name to add to this `XMLElement` |
530
|
|
|
* @return XMLElement |
531
|
|
|
* The current instance |
532
|
|
|
*/ |
533
|
|
|
public function addClass($class) |
534
|
|
|
{ |
535
|
|
|
$current = preg_split('%\s+%', $this->getAttribute('class'), 0, PREG_SPLIT_NO_EMPTY); |
536
|
|
|
$added = preg_split('%\s+%', $class, 0, PREG_SPLIT_NO_EMPTY); |
537
|
|
|
$current = array_merge($current, $added); |
|
|
|
|
538
|
|
|
$classes = implode(' ', $current); |
539
|
|
|
return $this->setAttribute('class', $classes); |
540
|
|
|
} |
541
|
|
|
|
542
|
|
|
/** |
543
|
|
|
* A convenience method to quickly remove a CSS class from an |
544
|
|
|
* `XMLElement`'s existing class attribute. If the attribute does not |
545
|
|
|
* exist, this method will do nothing. |
546
|
|
|
* It also make sure that classes are separated by a single space. |
547
|
|
|
* |
548
|
|
|
* @since Symphony 2.2.2 |
549
|
|
|
* @uses setAttribute() |
550
|
|
|
* @param string $class |
551
|
|
|
* The CSS class name to remove from this `XMLElement` |
552
|
|
|
* @return XMLElement |
553
|
|
|
* The current instance |
554
|
|
|
*/ |
555
|
|
|
public function removeClass($class) |
556
|
|
|
{ |
557
|
|
|
$classes = preg_split('%\s+%', $this->getAttribute('class'), 0, PREG_SPLIT_NO_EMPTY); |
558
|
|
|
$removed = preg_split('%\s+%', $class, 0, PREG_SPLIT_NO_EMPTY); |
559
|
|
|
$classes = array_diff($classes, $removed); |
|
|
|
|
560
|
|
|
$classes = implode(' ', $classes); |
561
|
|
|
return $this->setAttribute('class', $classes); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Returns the number of children this `XMLElement` has. |
566
|
|
|
* @return integer |
567
|
|
|
*/ |
568
|
|
|
public function getNumberOfChildren() |
569
|
|
|
{ |
570
|
|
|
return count($this->children); |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Given the position of the child in the `$this->children`, |
575
|
|
|
* this function will unset the child at that position. This function |
576
|
|
|
* is not reversible. This function does not alter the key's of |
577
|
|
|
* `$this->children` after removing a child |
578
|
|
|
* |
579
|
|
|
* @since Symphony 2.2.2 |
580
|
|
|
* @param integer $index |
581
|
|
|
* The index of the child to be removed. If the index given is negative |
582
|
|
|
* it will be calculated from the end of `$this->children`. |
583
|
|
|
* @return XMLElement |
584
|
|
|
* The current instance |
585
|
|
|
* @throws Exception |
586
|
|
|
* If the $index is not an integer or the index is not valid. |
587
|
|
|
*/ |
588
|
|
|
public function removeChildAt($index) |
589
|
|
|
{ |
590
|
|
|
General::ensureType([ |
591
|
|
|
'index' => ['var' => $index, 'type' => 'int'], |
592
|
|
|
]); |
593
|
|
|
|
594
|
|
|
$index = $this->getRealIndex($index); |
595
|
|
|
|
596
|
|
|
if (!isset($this->children[$index])) { |
597
|
|
|
throw new Exception("Index out of range. No child at index `$index`."); |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
unset($this->children[$index]); |
601
|
|
|
|
602
|
|
|
return $this; |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
/** |
606
|
|
|
* Given a desired index, and an `XMLElement`, this function will insert |
607
|
|
|
* the child at that index in `$this->children` shuffling all children |
608
|
|
|
* greater than `$index` down one. If the `$index` given is greater then |
609
|
|
|
* the number of children for this `XMLElement`, the `$child` will be |
610
|
|
|
* appended to the current `$this->children` array. |
611
|
|
|
* |
612
|
|
|
* @since Symphony 2.2.2 |
613
|
|
|
* @uses validateChild() |
614
|
|
|
* @uses setChildren() |
615
|
|
|
* @param integer $index |
616
|
|
|
* The index where the `$child` should be inserted. If this is negative |
617
|
|
|
* the index will be calculated from the end of `$this->children`. |
618
|
|
|
* @param XMLElement $child |
619
|
|
|
* The XMLElement to insert at the desired `$index` |
620
|
|
|
* @return XMLElement |
621
|
|
|
* The current instance |
622
|
|
|
* @throws Exception |
623
|
|
|
* If the $index is not an integer. |
624
|
|
|
*/ |
625
|
|
|
public function insertChildAt($index, XMLElement $child) |
626
|
|
|
{ |
627
|
|
|
General::ensureType([ |
628
|
|
|
'index' => ['var' => $index, 'type' => 'int'], |
629
|
|
|
]); |
630
|
|
|
|
631
|
|
|
$this->validateChild($child); |
632
|
|
|
|
633
|
|
|
if ($index >= $this->getNumberOfChildren()) { |
634
|
|
|
return $this->appendChild($child); |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
$start = array_slice($this->children, 0, $index); |
638
|
|
|
$end = array_slice($this->children, $index); |
639
|
|
|
|
640
|
|
|
$merge = array_merge($start, [$index => $child], $end); |
641
|
|
|
|
642
|
|
|
return $this->setChildren($merge); |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* Given the position of the child to replace, and an `XMLElement` |
647
|
|
|
* of the replacement child, this function will replace one child |
648
|
|
|
* with another |
649
|
|
|
* |
650
|
|
|
* @since Symphony 2.2.2 |
651
|
|
|
* @param integer $index |
652
|
|
|
* The index of the child to be replaced. If the index given is negative |
653
|
|
|
* it will be calculated from the end of `$this->children`. |
654
|
|
|
* @param XMLElement $child |
655
|
|
|
* An XMLElement of the new child |
656
|
|
|
* @return XMLElement |
657
|
|
|
* The current instance |
658
|
|
|
* @throws Exception |
659
|
|
|
* If the $index is not an integer or the index is not valid. |
660
|
|
|
*/ |
661
|
|
|
public function replaceChildAt($index, XMLElement $child) |
662
|
|
|
{ |
663
|
|
|
General::ensureType([ |
664
|
|
|
'index' => ['var' => $index, 'type' => 'int'], |
665
|
|
|
]); |
666
|
|
|
|
667
|
|
|
$this->validateChild($child); |
668
|
|
|
|
669
|
|
|
$index = $this->getRealIndex($index); |
670
|
|
|
|
671
|
|
|
if (!isset($this->children[$index])) { |
672
|
|
|
throw new Exception("Index out of range. No child at index `$index`."); |
673
|
|
|
} |
674
|
|
|
|
675
|
|
|
$this->children[$index] = $child; |
676
|
|
|
|
677
|
|
|
return $this; |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* Given an `$index`, return the real index in `$this->children` |
682
|
|
|
* depending on if the value is negative or not. Negative values |
683
|
|
|
* will work from the end of an array. |
684
|
|
|
* |
685
|
|
|
* @since Symphony 2.2.2 |
686
|
|
|
* @param integer $index |
687
|
|
|
* Positive indexes are returned as is, negative indexes are deducted |
688
|
|
|
* from the end of `$this->children` |
689
|
|
|
* @return integer |
690
|
|
|
*/ |
691
|
|
|
private function getRealIndex($index) |
692
|
|
|
{ |
693
|
|
|
if ($index >= 0) { |
694
|
|
|
return $index; |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
return $this->getNumberOfChildren() + $index; |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
/** |
701
|
|
|
* This function strips characters that are not allowed in XML |
702
|
|
|
* |
703
|
|
|
* @since Symphony 2.3 |
704
|
|
|
* @link http://www.w3.org/TR/xml/#charsets |
705
|
|
|
* @link http://www.phpedit.net/snippet/Remove-Invalid-XML-Characters |
706
|
|
|
* @param string $value |
707
|
|
|
* @return string |
708
|
|
|
*/ |
709
|
|
|
public static function stripInvalidXMLCharacters($value) |
710
|
|
|
{ |
711
|
|
|
if (Lang::isUnicodeCompiled()) { |
712
|
|
|
return preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $value); |
713
|
|
|
} else { |
714
|
|
|
$ret = ''; |
715
|
|
|
|
716
|
|
|
if (empty($value)) { |
717
|
|
|
return $ret; |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
$length = strlen($value); |
721
|
|
|
|
722
|
|
|
for ($i=0; $i < $length; $i++) { |
723
|
|
|
$current = ord($value{$i}); |
724
|
|
|
if (($current == 0x9) || |
725
|
|
|
($current == 0xA) || |
726
|
|
|
($current == 0xD) || |
727
|
|
|
(($current >= 0x20) && ($current <= 0xD7FF)) || |
728
|
|
|
(($current >= 0xE000) && ($current <= 0xFFFD)) || |
729
|
|
|
(($current >= 0x10000) && ($current <= 0x10FFFF))) { |
730
|
|
|
$ret .= chr($current); |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
return $ret; |
735
|
|
|
} |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* This function will turn the `XMLElement` into a string |
740
|
|
|
* representing the element as it would appear in the markup. |
741
|
|
|
* The result is valid XML. |
742
|
|
|
* |
743
|
|
|
* @param boolean $indent |
744
|
|
|
* Defaults to false |
745
|
|
|
* @param integer $tabDepth |
746
|
|
|
* Defaults to 0, indicates the number of tabs (\t) that this |
747
|
|
|
* element should be indented by in the output string |
748
|
|
|
* @return string |
749
|
|
|
* The XML string |
750
|
|
|
*/ |
751
|
|
|
public function generate($indent = false, $tabDepth = 0) |
752
|
|
|
{ |
753
|
|
|
$result = null; |
754
|
|
|
$newline = ($indent ? PHP_EOL : null); |
755
|
|
|
|
756
|
|
|
$result .= ($indent ? str_repeat("\t", $tabDepth) : null) . '<' . $this->getName(); |
757
|
|
|
|
758
|
|
|
foreach ($this->attributes as $attribute => $value) { |
759
|
|
|
if (!empty($value) || $this->allowEmptyAttributes) { |
760
|
|
|
$result .= sprintf(' %s="%s"', $attribute, $value); |
761
|
|
|
} |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
$addedNewline = false; |
765
|
|
|
|
766
|
|
|
if ($this->getNumberOfChildren() > 0 || !empty($this->value) || !$this->selfClosing) { |
767
|
|
|
$result .= '>'; |
768
|
|
|
|
769
|
|
|
foreach ($this->children as $i => $child) { |
770
|
|
|
if (!($child instanceof XMLElement)) { |
771
|
|
|
$result .= $child; |
772
|
|
|
} else { |
773
|
|
|
if ($addedNewline === false) { |
774
|
|
|
$addedNewline = true; |
775
|
|
|
$result .= $newline; |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
$child->setElementStyle($this->elementStyle); |
779
|
|
|
$result .= $child->generate($indent, $tabDepth + 1, true); |
|
|
|
|
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
$result .= sprintf( |
784
|
|
|
"%s</%s>%s", |
785
|
|
|
($indent && $addedNewline ? str_repeat("\t", $tabDepth) : null), |
786
|
|
|
$this->name, |
787
|
|
|
$newline |
788
|
|
|
); |
789
|
|
|
|
790
|
|
|
// Empty elements: |
791
|
|
|
} else { |
792
|
|
|
if ($this->elementStyle === 'xml') { |
793
|
|
|
$result .= ' />'; |
794
|
|
|
} elseif (in_array($this->name, static::$no_end_tags) || (substr($this->name, 0, 3) === '!--')) { |
795
|
|
|
$result .= '>'; |
796
|
|
|
} else { |
797
|
|
|
$result .= sprintf("></%s>", $this->name); |
798
|
|
|
} |
799
|
|
|
|
800
|
|
|
$result .= $newline; |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
return $result; |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* Given a string of XML, this function will create a new `XMLElement` |
808
|
|
|
* object, copy all attributes and children and return the result. |
809
|
|
|
* |
810
|
|
|
* @uses fromDOMDocument() |
811
|
|
|
* @since Symphony 3.0.0 |
812
|
|
|
* @param string $xml |
813
|
|
|
* A string of XML |
814
|
|
|
* @return XMLElement |
815
|
|
|
* The new `XMLElement` derived from `string $xml`. |
816
|
|
|
*/ |
817
|
|
|
public static function fromXMLString($xml) |
818
|
|
|
{ |
819
|
|
|
General::ensureType([ |
820
|
|
|
'xml' => ['var' => $xml, 'type' => 'string'], |
821
|
|
|
]); |
822
|
|
|
|
823
|
|
|
$doc = new DOMDocument('1.0', 'utf-8'); |
824
|
|
|
$doc->loadXML($xml); |
825
|
|
|
|
826
|
|
|
return static::fromDOMDocument($doc); |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
/** |
830
|
|
|
* Given a string of XML, this function will convert it to an `XMLElement` |
831
|
|
|
* object and return the result. |
832
|
|
|
* |
833
|
|
|
* @since Symphony 2.4 |
834
|
|
|
* @deprecated @since Symphony 3.0.0 |
835
|
|
|
* Use `fromXMLString()` |
836
|
|
|
* @param string $root_element |
837
|
|
|
* @param string $xml |
838
|
|
|
* A string of XML |
839
|
|
|
* @return XMLElement |
840
|
|
|
*/ |
841
|
|
|
public static function convertFromXMLString($root_element, $xml) |
842
|
|
|
{ |
843
|
|
|
if (Symphony::Log()) { |
844
|
|
|
Symphony::Log()->pushDeprecateWarningToLog( |
845
|
|
|
'XMLElement::convertFromXMLString()', |
846
|
|
|
'XMLElement::fromXMLString()' |
847
|
|
|
); |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
$doc = new DOMDocument('1.0', 'utf-8'); |
851
|
|
|
$doc->loadXML($xml); |
852
|
|
|
|
853
|
|
|
return self::convertFromDOMDocument($root_element, $doc); |
|
|
|
|
854
|
|
|
} |
855
|
|
|
|
856
|
|
|
/** |
857
|
|
|
* Given a `DOMDocument`, this function will create a new `XMLElement` |
858
|
|
|
* object, copy all attributes and children and return the result. |
859
|
|
|
* |
860
|
|
|
* @since Symphony 3.0.0 |
861
|
|
|
* @param DOMDocument $doc |
862
|
|
|
* A DOMDocument to copy from |
863
|
|
|
* @return XMLElement |
864
|
|
|
* The new `XMLElement` derived from `DOMDocument $doc`. |
865
|
|
|
*/ |
866
|
|
|
public static function fromDOMDocument(DOMDocument $doc) |
867
|
|
|
{ |
868
|
|
|
$root = new XMLElement($doc->documentElement->nodeName); |
869
|
|
|
static::copyDOMNode($root, $doc->documentElement); |
870
|
|
|
return $root; |
871
|
|
|
} |
872
|
|
|
|
873
|
|
|
/** |
874
|
|
|
* Given a `DOMDocument`, this function will convert it to an `XMLElement` |
875
|
|
|
* object and return the result. |
876
|
|
|
* |
877
|
|
|
* @since Symphony 2.4 |
878
|
|
|
* @deprecated @since Symphony 3.0.0 |
879
|
|
|
* Use `fromDOMDocument()` |
880
|
|
|
* @param string $root_element |
881
|
|
|
* @param DOMDocument $doc |
882
|
|
|
* @return XMLElement |
883
|
|
|
*/ |
884
|
|
|
public static function convertFromDOMDocument($root_element, DOMDocument $doc) |
885
|
|
|
{ |
886
|
|
|
if (Symphony::Log()) { |
887
|
|
|
Symphony::Log()->pushDeprecateWarningToLog( |
888
|
|
|
'XMLElement::convertFromDOMDocument()', |
889
|
|
|
'XMLElement::fromDOMDocument()' |
890
|
|
|
); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
$xpath = new DOMXPath($doc); |
894
|
|
|
$root = new XMLElement($root_element); |
895
|
|
|
|
896
|
|
|
foreach ($xpath->query('.') as $node) { |
897
|
|
|
static::copyDOMNode($root, $node); |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
return $root; |
901
|
|
|
} |
902
|
|
|
|
903
|
|
|
/** |
904
|
|
|
* Given a DOMNode, this function will help replicate it as an |
905
|
|
|
* XMLElement object |
906
|
|
|
* |
907
|
|
|
* @since Symphony 2.5.2 |
908
|
|
|
* @param XMLElement $element |
909
|
|
|
* @param DOMNode $node |
910
|
|
|
*/ |
911
|
|
|
protected static function copyDOMNode(XMLElement $element, DOMNode $node) |
912
|
|
|
{ |
913
|
|
|
if ($node->hasAttributes()) { |
914
|
|
|
foreach ($node->attributes as $name => $attrEl) { |
915
|
|
|
$element->setAttribute($name, General::sanitize($attrEl->value)); |
916
|
|
|
} |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
if ($node->hasChildNodes()) { |
920
|
|
|
foreach ($node->childNodes as $childNode) { |
921
|
|
|
if ($childNode instanceof DOMCdataSection) { |
922
|
|
|
$element->setValue(General::wrapInCDATA($childNode->data)); |
923
|
|
|
} elseif ($childNode instanceof DOMText) { |
924
|
|
|
if ($childNode->isWhitespaceInElementContent() === false) { |
925
|
|
|
$element->setValue(General::sanitize($childNode->data)); |
926
|
|
|
} |
927
|
|
|
} elseif ($childNode instanceof DOMElement) { |
928
|
|
|
$el = new XMLElement($childNode->tagName); |
929
|
|
|
static::copyDOMNode($el, $childNode); |
930
|
|
|
$element->appendChild($el); |
931
|
|
|
} |
932
|
|
|
} |
933
|
|
|
} |
934
|
|
|
} |
935
|
|
|
} |
936
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.