1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
// Copyright (c) 2016, Daniele Orlando <fluidxml(at)danieleorlando.com> |
4
|
|
|
// All rights reserved. |
5
|
|
|
// |
6
|
|
|
// Redistribution and use in source and binary forms, with or without modification, |
7
|
|
|
// are permitted provided that the following conditions are met: |
8
|
|
|
// |
9
|
|
|
// 1. Redistributions of source code must retain the above copyright notice, this |
10
|
|
|
// list of conditions and the following disclaimer. |
11
|
|
|
// |
12
|
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice, |
13
|
|
|
// this list of conditions and the following disclaimer in the documentation |
14
|
|
|
// and/or other materials provided with the distribution. |
15
|
|
|
// |
16
|
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
17
|
|
|
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
18
|
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
19
|
|
|
// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, |
20
|
|
|
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
21
|
|
|
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
22
|
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
23
|
|
|
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE |
24
|
|
|
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED |
25
|
|
|
// OF THE POSSIBILITY OF SUCH DAMAGE. |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* FluidXML is a PHP library, under the Servo PHP framework umbrella, |
29
|
|
|
* specifically designed to manipulate XML documents with a concise |
30
|
|
|
* and fluent interface. |
31
|
|
|
* |
32
|
|
|
* It leverages XPath and the fluent programming technique to be fun |
33
|
|
|
* and effective. |
34
|
|
|
* |
35
|
|
|
* @author Daniele Orlando <fluidxml(at)danieleorlando.com> |
36
|
|
|
* |
37
|
|
|
* @license BSD-2-Clause |
38
|
|
|
* @license https://opensource.org/licenses/BSD-2-Clause |
39
|
|
|
*/ |
40
|
|
|
|
41
|
|
|
namespace FluidXml |
42
|
|
|
{ |
43
|
|
|
|
44
|
|
|
use \FluidXml\Core\FluidInterface; |
45
|
|
|
use \FluidXml\Core\FluidDocument; |
46
|
|
|
use \FluidXml\Core\FluidContext; |
47
|
|
|
use \FluidXml\Core\NewableTrait; |
48
|
|
|
use \FluidXml\Core\ReservedCallTrait; |
49
|
|
|
use \FluidXml\Core\ReservedCallStaticTrait; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Constructs a new FluidXml instance. |
53
|
|
|
* |
54
|
|
|
* ```php |
55
|
|
|
* $xml = fluidxml(); |
56
|
|
|
* // is the same of |
57
|
|
|
* $xml = new FluidXml(); |
58
|
|
|
* |
59
|
|
|
* $xml = fluidxml([ |
60
|
|
|
* |
61
|
|
|
* 'root' => 'doc', |
62
|
|
|
* |
63
|
|
|
* 'version' => '1.0', |
64
|
|
|
* |
65
|
|
|
* 'encoding' => 'UTF-8', |
66
|
|
|
* |
67
|
|
|
* 'stylesheet' => null ]); |
68
|
|
|
* ``` |
69
|
|
|
* |
70
|
|
|
* @param array $arguments Options that influence the construction of the XML document. |
71
|
|
|
* |
72
|
|
|
* @return FluidXml A new FluidXml instance. |
73
|
|
|
*/ |
74
|
|
|
function fluidify(...$arguments) |
75
|
|
|
{ |
76
|
1 |
|
return FluidXml::load(...$arguments); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
function fluidxml(...$arguments) |
80
|
|
|
{ |
81
|
1 |
|
return new FluidXml(...$arguments); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
function fluidns(...$arguments) |
85
|
|
|
{ |
86
|
1 |
|
return new FluidNamespace(...$arguments); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
function is_an_xml_string($string) |
90
|
|
|
{ |
91
|
|
|
// Removes any empty new line at the beginning, |
92
|
|
|
// otherwise the first character check may fail. |
93
|
1 |
|
$string = \ltrim($string); |
94
|
|
|
|
95
|
1 |
|
return $string[0] === '<'; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
function domdocument_to_string_without_headers(\DOMDocument $dom) |
99
|
|
|
{ |
100
|
1 |
|
return $dom->saveXML($dom->documentElement); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
function domnodelist_to_string(\DOMNodeList $nodelist) |
104
|
|
|
{ |
105
|
1 |
|
$nodes = []; |
106
|
|
|
|
107
|
1 |
|
foreach ($nodelist as $n) { |
108
|
1 |
|
$nodes[] = $n; |
109
|
|
|
} |
110
|
|
|
|
111
|
1 |
|
return domnodes_to_string($nodes); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
function domnodes_to_string(array $nodes) |
115
|
|
|
{ |
116
|
1 |
|
$dom = $nodes[0]->ownerDocument; |
117
|
1 |
|
$xml = ''; |
118
|
|
|
|
119
|
1 |
|
foreach ($nodes as $n) { |
120
|
1 |
|
$xml .= $dom->saveXML($n) . PHP_EOL; |
121
|
|
|
} |
122
|
|
|
|
123
|
1 |
|
return \rtrim($xml); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
function simplexml_to_string_without_headers(\SimpleXMLElement $element) |
127
|
|
|
{ |
128
|
1 |
|
$dom = \dom_import_simplexml($element); |
129
|
|
|
|
130
|
1 |
|
return $dom->ownerDocument->saveXML($dom); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
class FluidXml implements FluidInterface |
134
|
|
|
{ |
135
|
|
|
use NewableTrait, |
136
|
|
|
ReservedCallTrait, // For compatibility with PHP 5.6. |
137
|
|
|
ReservedCallStaticTrait; // For compatibility with PHP 5.6. |
138
|
|
|
|
139
|
|
|
const ROOT_NODE = 'doc'; |
140
|
|
|
|
141
|
|
|
private $document; |
142
|
|
|
|
143
|
1 |
|
public static function load($document) |
144
|
|
|
{ |
145
|
1 |
|
if (\is_string($document) && ! is_an_xml_string($document)) { |
146
|
|
|
// Removes any empty new line at the beginning, |
147
|
|
|
// otherwise the first character check fails. |
148
|
|
|
|
149
|
1 |
|
$file = $document; |
150
|
1 |
|
$is_file = \is_file($file); |
151
|
1 |
|
$is_readable = \is_readable($file); |
152
|
|
|
|
153
|
1 |
|
if ($is_file && $is_readable) { |
154
|
1 |
|
$document = \file_get_contents($file); |
155
|
|
|
} |
156
|
|
|
|
157
|
1 |
|
if (! $is_file || ! $is_readable || ! $document) { |
158
|
1 |
|
throw new \Exception("File '$file' not accessible."); |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
1 |
|
return (new FluidXml(['root' => null]))->appendChild($document); |
163
|
|
|
} |
164
|
|
|
|
165
|
1 |
|
public function __construct($root = null, $options = []) |
166
|
|
|
{ |
167
|
1 |
|
$this->document = new FluidDocument(); |
168
|
1 |
|
$doc = $this->document; |
169
|
|
|
|
170
|
1 |
|
$defaults = [ 'root' => self::ROOT_NODE, |
171
|
1 |
|
'version' => '1.0', |
172
|
1 |
|
'encoding' => 'UTF-8', |
173
|
|
|
'stylesheet' => null ]; |
174
|
|
|
|
175
|
1 |
|
if (\is_string($root)) { |
176
|
|
|
// The root option can be specified as first argument |
177
|
|
|
// because it is the most common. |
178
|
1 |
|
$defaults['root'] = $root; |
179
|
1 |
|
} else if (\is_array($root)) { |
180
|
|
|
// If the first argument is an array, the user has skipped |
181
|
|
|
// the root option and is passing a bunch of options all together. |
182
|
1 |
|
$options = $root; |
183
|
|
|
} |
184
|
|
|
|
185
|
1 |
|
$opts = \array_merge($defaults, $options); |
186
|
|
|
|
187
|
1 |
|
$doc->dom = new \DOMDocument($opts['version'], $opts['encoding']); |
188
|
1 |
|
$doc->dom->formatOutput = true; |
189
|
1 |
|
$doc->dom->preserveWhiteSpace = false; |
190
|
|
|
|
191
|
1 |
|
$doc->xpath = new \DOMXPath($doc->dom); |
192
|
|
|
|
193
|
1 |
|
if (! empty($opts['root'])) { |
194
|
1 |
|
$this->appendSibling($opts['root']); |
195
|
|
|
} |
196
|
|
|
|
197
|
1 |
|
if (! empty($opts['stylesheet'])) { |
198
|
|
|
$attrs = 'type="text/xsl" ' |
199
|
1 |
|
. "encoding=\"{$opts['encoding']}\" " |
200
|
1 |
|
. 'indent="yes" ' |
201
|
1 |
|
. "href=\"{$opts['stylesheet']}\""; |
202
|
1 |
|
$stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs); |
203
|
|
|
|
204
|
1 |
|
$doc->dom->insertBefore($stylesheet, $doc->dom->documentElement); |
205
|
|
|
} |
206
|
1 |
|
} |
207
|
|
|
|
208
|
1 |
|
public function xml($strip = false) |
209
|
|
|
{ |
210
|
1 |
|
if ($strip) { |
211
|
1 |
|
return domdocument_to_string_without_headers($this->document->dom); |
212
|
|
|
} |
213
|
|
|
|
214
|
1 |
|
return $this->document->dom->saveXML(); |
215
|
|
|
} |
216
|
|
|
|
217
|
1 |
|
public function dom() |
218
|
|
|
{ |
219
|
1 |
|
return $this->document->dom; |
220
|
|
|
} |
221
|
|
|
|
222
|
1 |
|
public function namespaces() |
223
|
|
|
{ |
224
|
1 |
|
return $this->document->namespaces; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
// This method should be called 'namespace', |
228
|
|
|
// but for compatibility with PHP 5.6 |
229
|
|
|
// it is shadowed by the __call() method. |
230
|
1 |
|
protected function namespace_(...$arguments) |
231
|
|
|
{ |
232
|
1 |
|
$namespaces = []; |
233
|
|
|
|
234
|
1 |
|
if (\is_string($arguments[0])) { |
235
|
1 |
|
$args = [ $arguments[0], $arguments[1] ]; |
236
|
|
|
|
237
|
1 |
|
if (isset($arguments[2])) { |
238
|
1 |
|
$args[] = $arguments[2]; |
239
|
|
|
} |
240
|
|
|
|
241
|
1 |
|
$namespaces[] = new FluidNamespace(...$args); |
242
|
1 |
|
} else if (\is_array($arguments[0])) { |
243
|
1 |
|
$namespaces = $arguments[0]; |
244
|
|
|
} else { |
245
|
1 |
|
$namespaces = $arguments; |
246
|
|
|
} |
247
|
|
|
|
248
|
1 |
|
foreach ($namespaces as $n) { |
249
|
1 |
|
$this->document->namespaces[$n->id()] = $n; |
250
|
1 |
|
$this->document->xpath->registerNamespace($n->id(), $n->uri()); |
251
|
|
|
} |
252
|
|
|
|
253
|
1 |
|
return $this; |
254
|
|
|
} |
255
|
|
|
|
256
|
1 |
|
public function query(...$xpath) |
257
|
|
|
{ |
258
|
1 |
|
return $this->context()->query(...$xpath); |
259
|
|
|
} |
260
|
|
|
|
261
|
1 |
|
public function times($times, callable $fn = null) |
262
|
|
|
{ |
263
|
1 |
|
return $this->context()->times($times, $fn); |
264
|
|
|
} |
265
|
|
|
|
266
|
1 |
|
public function each(callable $fn) |
267
|
|
|
{ |
268
|
1 |
|
return $this->context()->each($fn); |
269
|
|
|
} |
270
|
|
|
|
271
|
1 |
|
public function appendChild($child, ...$optionals) |
272
|
|
|
{ |
273
|
|
|
// If the user has requested ['root' => null] at construction time |
274
|
|
|
// 'context()' promotes DOMDocument as root node. |
275
|
1 |
|
$context = $this->context(); |
276
|
1 |
|
$new_context = $context->appendChild($child, ...$optionals); |
277
|
|
|
|
278
|
1 |
|
return $this->chooseContext($context, $new_context); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
// Alias of appendChild(). |
282
|
1 |
|
public function add($child, ...$optionals) |
283
|
|
|
{ |
284
|
1 |
|
return $this->appendChild($child, ...$optionals); |
285
|
|
|
} |
286
|
|
|
|
287
|
1 |
View Code Duplication |
public function prependSibling($sibling, ...$optionals) |
|
|
|
|
288
|
|
|
{ |
289
|
1 |
|
if ($this->document->dom->documentElement === null) { |
290
|
|
|
// If the document doesn't have at least one root node, |
291
|
|
|
// the sibling creation fails. In this case we replace |
292
|
|
|
// the sibling creation with the creation of a generic node. |
293
|
1 |
|
return $this->appendChild($sibling, ...$optionals); |
294
|
|
|
} |
295
|
|
|
|
296
|
1 |
|
$context = $this->context(); |
297
|
1 |
|
$new_context = $context->prependSibling($sibling, ...$optionals); |
298
|
|
|
|
299
|
1 |
|
return $this->chooseContext($context, $new_context); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
// Alias of prependSibling(). |
303
|
1 |
|
public function prepend($sibling, ...$optionals) |
304
|
|
|
{ |
305
|
1 |
|
return $this->prependSibling($sibling, ...$optionals); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// Alias of prependSibling(). |
309
|
1 |
|
public function insertSiblingBefore($sibling, ...$optionals) |
310
|
|
|
{ |
311
|
1 |
|
return $this->prependSibling($sibling, ...$optionals); |
312
|
|
|
} |
313
|
|
|
|
314
|
1 |
View Code Duplication |
public function appendSibling($sibling, ...$optionals) |
|
|
|
|
315
|
|
|
{ |
316
|
1 |
|
if ($this->document->dom->documentElement === null) { |
317
|
|
|
// If the document doesn't have at least one root node, |
318
|
|
|
// the sibling creation fails. In this case we replace |
319
|
|
|
// the sibling creation with the creation of a generic node. |
320
|
1 |
|
return $this->appendChild($sibling, ...$optionals); |
321
|
|
|
} |
322
|
|
|
|
323
|
1 |
|
$context = $this->context(); |
324
|
1 |
|
$new_context = $context->appendSibling($sibling, ...$optionals); |
325
|
|
|
|
326
|
1 |
|
return $this->chooseContext($context, $new_context); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
// Alias of appendSibling(). |
330
|
1 |
|
public function append($sibling, ...$optionals) |
331
|
|
|
{ |
332
|
1 |
|
return $this->appendSibling($sibling, ...$optionals); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
// Alias of appendSibling(). |
336
|
1 |
|
public function insertSiblingAfter($sibling, ...$optionals) |
337
|
|
|
{ |
338
|
1 |
|
return $this->appendSibling($sibling, ...$optionals); |
339
|
|
|
} |
340
|
|
|
|
341
|
1 |
|
public function setAttribute(...$arguments) |
342
|
|
|
{ |
343
|
1 |
|
$this->context()->setAttribute(...$arguments); |
344
|
|
|
|
345
|
1 |
|
return $this; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
// Alias of setAttribute(). |
349
|
1 |
|
public function attr(...$arguments) |
350
|
|
|
{ |
351
|
1 |
|
return $this->setAttribute(...$arguments); |
352
|
|
|
} |
353
|
|
|
|
354
|
1 |
|
public function appendText($text) |
355
|
|
|
{ |
356
|
1 |
|
$this->context()->appendText($text); |
357
|
|
|
|
358
|
1 |
|
return $this; |
359
|
|
|
} |
360
|
|
|
|
361
|
1 |
|
public function appendCdata($text) |
362
|
|
|
{ |
363
|
1 |
|
$this->context()->appendCdata($text); |
364
|
|
|
|
365
|
1 |
|
return $this; |
366
|
|
|
} |
367
|
|
|
|
368
|
1 |
|
public function setText($text) |
369
|
|
|
{ |
370
|
1 |
|
$this->context()->setText($text); |
371
|
|
|
|
372
|
1 |
|
return $this; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
// Alias of setText(). |
376
|
1 |
|
public function text($text) |
377
|
|
|
{ |
378
|
1 |
|
return $this->setText($text); |
379
|
|
|
} |
380
|
|
|
|
381
|
1 |
|
public function setCdata($text) |
382
|
|
|
{ |
383
|
1 |
|
$this->context()->setCdata($text); |
384
|
|
|
|
385
|
1 |
|
return $this; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
// Alias of setCdata(). |
389
|
1 |
|
public function cdata($text) |
390
|
|
|
{ |
391
|
1 |
|
return $this->setCdata($text); |
392
|
|
|
} |
393
|
|
|
|
394
|
1 |
|
public function remove(...$xpath) |
395
|
|
|
{ |
396
|
1 |
|
$this->context()->remove(...$xpath); |
397
|
|
|
|
398
|
1 |
|
return $this; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
private $context; |
402
|
|
|
private $contextEl; |
403
|
|
|
|
404
|
1 |
|
protected function context() |
405
|
|
|
{ |
406
|
1 |
|
if ($this->document->dom->documentElement === null) { |
407
|
|
|
// If the user has requested ['root' => null] at construction time |
408
|
|
|
// the 'documentElement' property is null because we have not created |
409
|
|
|
// a root node yet. Whether there is not a root node, the DOMDocument |
410
|
|
|
// is promoted as root node. |
411
|
1 |
|
if ($this->context === null) { |
412
|
1 |
|
$this->context = new FluidContext($this->document, $this->document->dom); |
413
|
|
|
} |
414
|
|
|
|
415
|
1 |
|
return $this->context; |
416
|
|
|
} |
417
|
|
|
|
418
|
1 |
|
if ($this->contextEl !== $this->document->dom->documentElement) { |
419
|
|
|
// The user can prepend a root node to the current root node. |
420
|
|
|
// In this case we have to update the context with the new first root node. |
421
|
1 |
|
$this->context = new FluidContext($this->document, $this->document->dom->documentElement); |
422
|
1 |
|
$this->contextEl = $this->document->dom->documentElement; |
423
|
|
|
} |
424
|
|
|
|
425
|
1 |
|
return $this->context; |
426
|
|
|
} |
427
|
|
|
|
428
|
1 |
|
protected function chooseContext($help_context, $new_context) |
429
|
|
|
{ |
430
|
|
|
// If the two contextes are diffent, the user has requested |
431
|
|
|
// a switch of the context and we have to return it. |
432
|
1 |
|
if ($help_context !== $new_context) { |
433
|
1 |
|
return $new_context; |
434
|
|
|
} |
435
|
|
|
|
436
|
1 |
|
return $this; |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
class FluidNamespace |
441
|
|
|
{ |
442
|
|
|
const ID = 'id' ; |
443
|
|
|
const URI = 'uri' ; |
444
|
|
|
const MODE = 'mode'; |
445
|
|
|
|
446
|
|
|
const MODE_IMPLICIT = 0; |
447
|
|
|
const MODE_EXPLICIT = 1; |
448
|
|
|
|
449
|
|
|
private $config = [ self::ID => '', |
450
|
|
|
self::URI => '', |
451
|
|
|
self::MODE => self::MODE_EXPLICIT ]; |
452
|
|
|
|
453
|
1 |
|
public function __construct($id, $uri, $mode = 1) |
454
|
|
|
{ |
455
|
1 |
|
if (\is_array($id)) { |
456
|
1 |
|
$args = $id; |
457
|
1 |
|
$id = $args[self::ID]; |
458
|
1 |
|
$uri = $args[self::URI]; |
459
|
|
|
|
460
|
1 |
|
if (isset($args[self::MODE])) { |
461
|
1 |
|
$mode = $args[self::MODE]; |
462
|
|
|
} |
463
|
|
|
} |
464
|
|
|
|
465
|
1 |
|
$this->config[self::ID] = $id; |
466
|
1 |
|
$this->config[self::URI] = $uri; |
467
|
1 |
|
$this->config[self::MODE] = $mode; |
468
|
1 |
|
} |
469
|
|
|
|
470
|
1 |
|
public function id() |
471
|
|
|
{ |
472
|
1 |
|
return $this->config[self::ID]; |
473
|
|
|
} |
474
|
|
|
|
475
|
1 |
|
public function uri() |
476
|
|
|
{ |
477
|
1 |
|
return $this->config[self::URI]; |
478
|
|
|
} |
479
|
|
|
|
480
|
1 |
|
public function mode() |
481
|
|
|
{ |
482
|
1 |
|
return $this->config[self::MODE]; |
483
|
|
|
} |
484
|
|
|
|
485
|
1 |
|
public function querify($xpath) |
486
|
|
|
{ |
487
|
1 |
|
$id = $this->id(); |
488
|
|
|
|
489
|
1 |
|
if ($id) { |
490
|
1 |
|
$id .= ':'; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
// An XPath query may not start with a slash ('/'). |
494
|
|
|
// Relative queries are an example '../target". |
495
|
1 |
|
$new_xpath = ''; |
496
|
|
|
|
497
|
1 |
|
$nodes = \explode('/', $xpath); |
498
|
|
|
|
499
|
1 |
|
foreach ($nodes as $node) { |
500
|
|
|
// An XPath query may have multiple slashes ('/') |
501
|
|
|
// example: //target |
502
|
1 |
|
if ($node) { |
503
|
1 |
|
$new_xpath .= "{$id}{$node}"; |
504
|
|
|
} |
505
|
|
|
|
506
|
1 |
|
$new_xpath .= '/'; |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
// Removes the last appended slash. |
510
|
1 |
|
return \substr($new_xpath, 0, -1); |
511
|
|
|
} |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
} // END OF NAMESPACE FluidXml |
515
|
|
|
|
516
|
|
|
namespace FluidXml\Core |
517
|
|
|
{ |
518
|
|
|
|
519
|
|
|
use \FluidXml\FluidXml; |
|
|
|
|
520
|
|
|
use \FluidXml\FluidNamespace; |
|
|
|
|
521
|
|
|
|
522
|
|
|
use function \FluidXml\is_an_xml_string; |
|
|
|
|
523
|
|
|
use function \FluidXml\domnodes_to_string; |
|
|
|
|
524
|
|
|
|
525
|
|
|
interface FluidInterface |
526
|
|
|
{ |
527
|
|
|
/** |
528
|
|
|
* Executes an XPath query. |
529
|
|
|
* |
530
|
|
|
* ```php |
531
|
|
|
* $xml = fluidxml(); |
532
|
|
|
|
533
|
|
|
* $xml->query("/doc/book[@id='123']"); |
534
|
|
|
* |
535
|
|
|
* // Relative queries are valid. |
536
|
|
|
* $xml->query("/doc")->query("book[@id='123']"); |
537
|
|
|
* ``` |
538
|
|
|
* |
539
|
|
|
* @param string $xpath The XPath to execute. |
540
|
|
|
* |
541
|
|
|
* @return FluidContext The context associated to the DOMNodeList. |
542
|
|
|
*/ |
543
|
|
|
public function query(...$xpath); |
544
|
|
|
public function times($times, callable $fn = null); |
545
|
|
|
public function each(callable $fn); |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* Append a new node as child of the current context. |
549
|
|
|
* |
550
|
|
|
* ```php |
551
|
|
|
* $xml = fluidxml(); |
552
|
|
|
|
553
|
|
|
* $xml->appendChild('title', 'The Theory Of Everything'); |
554
|
|
|
* $xml->appendChild([ 'author' => 'S. Hawking' ]); |
555
|
|
|
* |
556
|
|
|
* $xml->appendChild('chapters', true)->appendChild('chapter', ['id'=> 1]); |
557
|
|
|
* |
558
|
|
|
* ``` |
559
|
|
|
* |
560
|
|
|
* @param string|array $child The child/children to add. |
561
|
|
|
* @param string $value The child text content. |
|
|
|
|
562
|
|
|
* @param bool $switchContext Whether to return the current context |
|
|
|
|
563
|
|
|
* or the context of the created node. |
564
|
|
|
* |
565
|
|
|
* @return FluidContext The context associated to the DOMNodeList. |
566
|
|
|
*/ |
567
|
|
|
public function appendChild($child, ...$optionals); |
568
|
|
|
public function prependSibling($sibling, ...$optionals); |
569
|
|
|
public function appendSibling($sibling, ...$optionals); |
570
|
|
|
public function setAttribute(...$arguments); |
571
|
|
|
public function setText($text); |
572
|
|
|
public function appendText($text); |
573
|
|
|
public function setCdata($text); |
574
|
|
|
public function appendCdata($text); |
575
|
|
|
public function remove(...$xpath); |
576
|
|
|
public function xml($strip = false); |
577
|
|
|
// Aliases: |
578
|
|
|
public function add($child, ...$optionals); |
579
|
|
|
public function prepend($sibling, ...$optionals); |
580
|
|
|
public function insertSiblingBefore($sibling, ...$optionals); |
581
|
|
|
public function append($sibling, ...$optionals); |
582
|
|
|
public function insertSiblingAfter($sibling, ...$optionals); |
583
|
|
|
public function attr(...$arguments); |
584
|
|
|
public function text($text); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
trait ReservedCallTrait |
588
|
|
|
{ |
589
|
1 |
|
public function __call($method, $arguments) |
590
|
|
|
{ |
591
|
1 |
|
$m = "{$method}_"; |
592
|
|
|
|
593
|
1 |
|
if (\method_exists($this, $m)) { |
594
|
1 |
|
return $this->$m(...$arguments); |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
throw new \Exception("Method '$method' not found."); |
598
|
|
|
} |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
trait ReservedCallStaticTrait |
602
|
|
|
{ |
603
|
1 |
|
public static function __callStatic($method, $arguments) |
604
|
|
|
{ |
605
|
1 |
|
$m = "{$method}_"; |
606
|
|
|
|
607
|
1 |
|
if (\method_exists(static::class, $m)) { |
608
|
1 |
|
return static::$m(...$arguments); |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
throw new \Exception("Method '$method' not found."); |
612
|
|
|
} |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
trait NewableTrait |
616
|
|
|
{ |
617
|
|
|
// This method should be called 'new', |
618
|
|
|
// but for compatibility with PHP 5.6 |
619
|
|
|
// it is shadowed by the __callStatic() method. |
620
|
1 |
|
public static function new_(...$arguments) |
621
|
|
|
{ |
622
|
1 |
|
return new static(...$arguments); |
623
|
|
|
} |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
class FluidDocument |
627
|
|
|
{ |
628
|
|
|
public $dom; |
629
|
|
|
public $xpath; |
630
|
|
|
public $namespaces = []; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
class FluidRepeater |
634
|
|
|
{ |
635
|
|
|
private $document; |
636
|
|
|
private $context; |
637
|
|
|
private $times; |
638
|
|
|
|
639
|
1 |
|
public function __construct($document, $context, $times) |
640
|
|
|
{ |
641
|
1 |
|
$this->document = $document; |
642
|
1 |
|
$this->context = $context; |
643
|
1 |
|
$this->times = $times; |
644
|
1 |
|
} |
645
|
|
|
|
646
|
1 |
|
public function __call($method, $arguments) |
647
|
|
|
{ |
648
|
1 |
|
$nodes = []; |
649
|
1 |
|
$new_context = $this->context; |
650
|
|
|
|
651
|
1 |
|
for ($i = 0, $l = $this->times; $i < $l; ++$i) { |
652
|
1 |
|
$new_context = $this->context->$method(...$arguments); |
653
|
1 |
|
$nodes = \array_merge($nodes, $new_context->asArray()); |
654
|
|
|
} |
655
|
|
|
|
656
|
1 |
|
if ($new_context !== $this->context) { |
657
|
1 |
|
return new FluidContext($this->document, $nodes); |
658
|
|
|
} |
659
|
|
|
|
660
|
1 |
|
return $this->context; |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator |
665
|
|
|
{ |
666
|
|
|
use NewableTrait, |
667
|
|
|
ReservedCallTrait, // For compatibility with PHP 5.6. |
668
|
|
|
ReservedCallStaticTrait; // For compatibility with PHP 5.6. |
669
|
|
|
|
670
|
|
|
private $document; |
671
|
|
|
private $nodes = []; |
672
|
|
|
private $seek = 0; |
673
|
|
|
|
674
|
1 |
|
public function __construct($document, $context) |
675
|
|
|
{ |
676
|
1 |
|
$this->document = $document; |
677
|
|
|
|
678
|
1 |
|
if (! \is_array($context) && ! $context instanceof \Traversable) { |
679
|
|
|
// DOMDocument, DOMElement and DOMNode are not iterable. |
680
|
|
|
// DOMNodeList and FluidContext are iterable. |
681
|
1 |
|
$context = [ $context ]; |
682
|
|
|
} |
683
|
|
|
|
684
|
1 |
|
foreach ($context as $n) { |
685
|
1 |
|
if (! $n instanceof \DOMNode) { |
686
|
|
|
throw new \Exception('Node type not recognized.'); |
687
|
|
|
} |
688
|
|
|
|
689
|
1 |
|
$this->nodes[] = $n; |
690
|
|
|
} |
691
|
1 |
|
} |
692
|
|
|
|
693
|
1 |
|
public function asArray() |
694
|
|
|
{ |
695
|
1 |
|
return $this->nodes; |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
// \ArrayAccess interface. |
699
|
1 |
|
public function offsetSet($offset, $value) |
700
|
|
|
{ |
701
|
|
|
// if (\is_null($offset)) { |
702
|
|
|
// $this->nodes[] = $value; |
703
|
|
|
// } else { |
704
|
|
|
// $this->nodes[$offset] = $value; |
705
|
|
|
// } |
706
|
1 |
|
throw new \Exception('Setting a context element is not allowed.'); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
// \ArrayAccess interface. |
710
|
1 |
|
public function offsetExists($offset) |
711
|
|
|
{ |
712
|
1 |
|
return isset($this->nodes[$offset]); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
// \ArrayAccess interface. |
716
|
1 |
|
public function offsetUnset($offset) |
717
|
|
|
{ |
718
|
|
|
// unset($this->nodes[$offset]); |
719
|
1 |
|
\array_splice($this->nodes, $offset, 1); |
720
|
1 |
|
} |
721
|
|
|
|
722
|
|
|
// \ArrayAccess interface. |
723
|
1 |
|
public function offsetGet($offset) |
724
|
|
|
{ |
725
|
1 |
|
if (isset($this->nodes[$offset])) { |
726
|
1 |
|
return $this->nodes[$offset]; |
727
|
|
|
} |
728
|
|
|
|
729
|
1 |
|
return null; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
// \Iterator interface. |
733
|
1 |
|
public function rewind() |
734
|
|
|
{ |
735
|
1 |
|
$this->seek = 0; |
736
|
1 |
|
} |
737
|
|
|
|
738
|
|
|
// \Iterator interface. |
739
|
1 |
|
public function current() |
740
|
|
|
{ |
741
|
1 |
|
return $this->nodes[$this->seek]; |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
// \Iterator interface. |
745
|
1 |
|
public function key() |
746
|
|
|
{ |
747
|
1 |
|
return $this->seek; |
748
|
|
|
} |
749
|
|
|
|
750
|
|
|
// \Iterator interface. |
751
|
1 |
|
public function next() |
752
|
|
|
{ |
753
|
1 |
|
++$this->seek; |
754
|
1 |
|
} |
755
|
|
|
|
756
|
|
|
// \Iterator interface. |
757
|
1 |
|
public function valid() |
758
|
|
|
{ |
759
|
1 |
|
return isset($this->nodes[$this->seek]); |
760
|
|
|
} |
761
|
|
|
|
762
|
1 |
|
public function length() |
763
|
|
|
{ |
764
|
1 |
|
return \count($this->nodes); |
765
|
|
|
} |
766
|
|
|
|
767
|
1 |
|
public function query(...$xpath) |
768
|
|
|
{ |
769
|
1 |
|
$xpaths = $xpath; |
770
|
|
|
|
771
|
1 |
|
if (\is_array($xpath[0])) { |
772
|
1 |
|
$xpaths = $xpath[0]; |
773
|
|
|
} |
774
|
|
|
|
775
|
1 |
|
$results = []; |
776
|
|
|
|
777
|
1 |
|
foreach ($this->nodes as $n) { |
778
|
1 |
|
foreach ($xpaths as $x) { |
779
|
|
|
// Returns a DOMNodeList. |
780
|
1 |
|
$res = $this->document->xpath->query($x, $n); |
781
|
|
|
|
782
|
|
|
// Algorithm 1: |
783
|
1 |
|
$results = \array_merge($results, \iterator_to_array($res)); |
784
|
|
|
|
785
|
|
|
// Algorithm 2: |
786
|
|
|
// foreach ($res as $r) { |
787
|
|
|
// $results[] = $r; |
788
|
|
|
// } |
789
|
|
|
|
790
|
|
|
// Algorithm 3: |
791
|
|
|
// for ($i = 0, $l = $res->length; $i < $l; ++$i) { |
792
|
|
|
// $results[] = $res->item($i); |
793
|
|
|
// } |
794
|
|
|
} |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
// Performing over multiple sibling nodes a query that ascends |
798
|
|
|
// the xpath, relative (../..) or absolute (//), returns identical |
799
|
|
|
// matching results that must be collapsed in an unique result |
800
|
|
|
// otherwise a subsequent operation is performed multiple times. |
801
|
1 |
|
$unique_results = []; |
802
|
1 |
|
foreach ($results as $r) { |
803
|
1 |
|
$found = false; |
804
|
|
|
|
805
|
1 |
|
foreach ($unique_results as $u) { |
806
|
1 |
|
if ($r === $u) { |
807
|
1 |
|
$found = true; |
808
|
|
|
} |
809
|
|
|
} |
810
|
|
|
|
811
|
1 |
|
if (! $found) { |
812
|
1 |
|
$unique_results[] = $r; |
813
|
|
|
} |
814
|
|
|
} |
815
|
|
|
|
816
|
1 |
|
return $this->newContext($unique_results); |
817
|
|
|
} |
818
|
|
|
|
819
|
1 |
|
public function times($times, callable $fn = null) |
820
|
|
|
{ |
821
|
1 |
|
if ($fn === null) { |
822
|
1 |
|
return new FluidRepeater($this->document, $this, $times); |
823
|
|
|
} |
824
|
|
|
|
825
|
1 |
|
for ($i = 0; $i < $times; ++$i) { |
826
|
1 |
|
$args = [$this, $i]; |
827
|
|
|
|
828
|
1 |
|
if ($fn instanceof \Closure) { |
829
|
1 |
|
$fn = $fn->bindTo($this); |
830
|
|
|
|
831
|
1 |
|
\array_shift($args); |
832
|
|
|
} |
833
|
|
|
|
834
|
1 |
|
\call_user_func($fn, ...$args); |
835
|
|
|
} |
836
|
|
|
|
837
|
1 |
|
return $this; |
838
|
|
|
} |
839
|
|
|
|
840
|
1 |
|
public function each(callable $fn) |
841
|
|
|
{ |
842
|
1 |
|
foreach ($this->nodes as $i => $n) { |
843
|
1 |
|
$cx = $this->newContext($n); |
844
|
1 |
|
$args = [$cx, $i, $n]; |
845
|
|
|
|
846
|
1 |
|
if ($fn instanceof \Closure) { |
847
|
1 |
|
$fn = $fn->bindTo($cx); |
848
|
|
|
|
849
|
1 |
|
\array_shift($args); |
850
|
|
|
} |
851
|
|
|
|
852
|
1 |
|
\call_user_func($fn, ...$args); |
853
|
|
|
} |
854
|
|
|
|
855
|
1 |
|
return $this; |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
// appendChild($child, $value?, $attributes? = [], $switchContext? = false) |
859
|
1 |
|
public function appendChild($child, ...$optionals) |
860
|
|
|
{ |
861
|
|
|
return $this->insertElement($child, $optionals, function($parent, $element) { |
862
|
1 |
|
return $parent->appendChild($element); |
863
|
1 |
|
}); |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
// Alias of appendChild(). |
867
|
1 |
|
public function add($child, ...$optionals) |
868
|
|
|
{ |
869
|
1 |
|
return $this->appendChild($child, ...$optionals); |
870
|
|
|
} |
871
|
|
|
|
872
|
1 |
|
public function prependSibling($sibling, ...$optionals) |
873
|
|
|
{ |
874
|
|
|
return $this->insertElement($sibling, $optionals, function($sibling, $element) { |
875
|
1 |
|
return $sibling->parentNode->insertBefore($element, $sibling); |
876
|
1 |
|
}); |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
// Alias of prependSibling(). |
880
|
1 |
|
public function prepend($sibling, ...$optionals) |
881
|
|
|
{ |
882
|
1 |
|
return $this->prependSibling($sibling, ...$optionals); |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
// Alias of prependSibling(). |
886
|
1 |
|
public function insertSiblingBefore($sibling, ...$optionals) |
887
|
|
|
{ |
888
|
1 |
|
return $this->prependSibling($sibling, ...$optionals); |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
public function appendSibling($sibling, ...$optionals) |
892
|
|
|
{ |
893
|
1 |
|
return $this->insertElement($sibling, $optionals, function($sibling, $element) { |
894
|
|
|
// If ->nextSibling is null, $element is simply appended as last sibling. |
895
|
1 |
|
return $sibling->parentNode->insertBefore($element, $sibling->nextSibling); |
896
|
1 |
|
}); |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
// Alias of appendSibling(). |
900
|
1 |
|
public function append($sibling, ...$optionals) |
901
|
|
|
{ |
902
|
1 |
|
return $this->appendSibling($sibling, ...$optionals); |
903
|
|
|
} |
904
|
|
|
|
905
|
|
|
// Alias of appendSibling(). |
906
|
1 |
|
public function insertSiblingAfter($sibling, ...$optionals) |
907
|
|
|
{ |
908
|
1 |
|
return $this->appendSibling($sibling, ...$optionals); |
909
|
|
|
} |
910
|
|
|
|
911
|
|
|
// Arguments can be in the form of: |
912
|
|
|
// setAttribute($name, $value) |
913
|
|
|
// setAttribute(['name' => 'value', ...]) |
914
|
1 |
|
public function setAttribute(...$arguments) |
915
|
|
|
{ |
916
|
|
|
// Default case is: |
917
|
|
|
// [ 'name' => 'value', ... ] |
918
|
1 |
|
$attrs = $arguments[0]; |
919
|
|
|
|
920
|
|
|
// If the first argument is not an array, |
921
|
|
|
// the user has passed two arguments: |
922
|
|
|
// 1. is the attribute name |
923
|
|
|
// 2. is the attribute value |
924
|
1 |
|
if (! \is_array($arguments[0])) { |
925
|
1 |
|
$attrs = [$arguments[0] => $arguments[1]]; |
926
|
|
|
} |
927
|
|
|
|
928
|
1 |
|
foreach ($this->nodes as $n) { |
929
|
1 |
|
foreach ($attrs as $k => $v) { |
930
|
|
|
// Algorithm 1: |
931
|
1 |
|
$n->setAttribute($k, $v); |
932
|
|
|
|
933
|
|
|
// Algorithm 2: |
934
|
|
|
// $n->setAttributeNode(new \DOMAttr($k, $v)); |
935
|
|
|
|
936
|
|
|
// Algorithm 3: |
937
|
|
|
// $n->appendChild(new \DOMAttr($k, $v)); |
938
|
|
|
|
939
|
|
|
// Algorithm 2 and 3 have a different behaviour |
940
|
|
|
// from Algorithm 1. |
941
|
|
|
// The attribute is still created or setted, but |
942
|
|
|
// changing the value of an existing attribute |
943
|
|
|
// changes even the order of that attribute |
944
|
|
|
// in the attribute list. |
945
|
|
|
} |
946
|
|
|
} |
947
|
|
|
|
948
|
1 |
|
return $this; |
949
|
|
|
} |
950
|
|
|
|
951
|
|
|
// Alias of setAttribute(). |
952
|
1 |
|
public function attr(...$arguments) |
953
|
|
|
{ |
954
|
1 |
|
return $this->setAttribute(...$arguments); |
955
|
|
|
} |
956
|
|
|
|
957
|
1 |
|
public function appendText($text) |
958
|
|
|
{ |
959
|
1 |
|
foreach ($this->nodes as $n) { |
960
|
1 |
|
$n->appendChild(new \DOMText($text)); |
961
|
|
|
} |
962
|
|
|
|
963
|
1 |
|
return $this; |
964
|
|
|
} |
965
|
|
|
|
966
|
1 |
|
public function appendCdata($text) |
967
|
|
|
{ |
968
|
1 |
|
foreach ($this->nodes as $n) { |
969
|
1 |
|
$n->appendChild(new \DOMCDATASection($text)); |
970
|
|
|
} |
971
|
|
|
|
972
|
1 |
|
return $this; |
973
|
|
|
} |
974
|
|
|
|
975
|
1 |
|
public function setText($text) |
976
|
|
|
{ |
977
|
1 |
|
foreach ($this->nodes as $n) { |
978
|
|
|
// Algorithm 1: |
979
|
1 |
|
$n->nodeValue = $text; |
980
|
|
|
|
981
|
|
|
// Algorithm 2: |
982
|
|
|
// foreach ($n->childNodes as $c) { |
983
|
|
|
// $n->removeChild($c); |
984
|
|
|
// } |
985
|
|
|
// $n->appendChild(new \DOMText($text)); |
986
|
|
|
|
987
|
|
|
// Algorithm 3: |
988
|
|
|
// foreach ($n->childNodes as $c) { |
989
|
|
|
// $n->replaceChild(new \DOMText($text), $c); |
990
|
|
|
// } |
991
|
|
|
} |
992
|
|
|
|
993
|
1 |
|
return $this; |
994
|
|
|
} |
995
|
|
|
|
996
|
|
|
// Alias of setText(). |
997
|
1 |
|
public function text($text) |
998
|
|
|
{ |
999
|
1 |
|
return $this->setText($text); |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
1 |
|
public function setCdata($text) |
1003
|
|
|
{ |
1004
|
1 |
|
foreach ($this->nodes as $n) { |
1005
|
1 |
|
$n->nodeValue = ''; |
1006
|
1 |
|
$n->appendChild(new \DOMCDATASection($text)); |
1007
|
|
|
} |
1008
|
|
|
|
1009
|
1 |
|
return $this; |
1010
|
|
|
} |
1011
|
|
|
|
1012
|
|
|
// Alias of setCdata(). |
1013
|
1 |
|
public function cdata($text) |
1014
|
|
|
{ |
1015
|
1 |
|
return $this->setCdata($text); |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
1 |
|
public function remove(...$xpath) |
1019
|
|
|
{ |
1020
|
|
|
// Arguments can be empty, a string or an array of strings. |
1021
|
|
|
|
1022
|
1 |
|
if (empty($xpath)) { |
1023
|
|
|
// The user has requested to remove the nodes of this context. |
1024
|
1 |
|
$targets = $this->nodes; |
1025
|
|
|
} else { |
1026
|
1 |
|
$targets = $this->query(...$xpath); |
1027
|
|
|
} |
1028
|
|
|
|
1029
|
1 |
|
foreach ($targets as $t) { |
1030
|
1 |
|
$t->parentNode->removeChild($t); |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
1 |
|
return $this; |
1034
|
|
|
} |
1035
|
|
|
|
1036
|
1 |
|
public function xml($strip = false) |
1037
|
|
|
{ |
1038
|
1 |
|
return domnodes_to_string($this->nodes); |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
1 |
|
protected function newContext($context) |
1042
|
|
|
{ |
1043
|
1 |
|
return new FluidContext($this->document, $context); |
1044
|
|
|
} |
1045
|
|
|
|
1046
|
1 |
|
protected function handleOptionals($element, &$optionals) |
1047
|
|
|
{ |
1048
|
1 |
|
if (! \is_array($element)) { |
1049
|
1 |
|
$element = [ $element ]; |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
1 |
|
$switch_context = false; |
1053
|
1 |
|
$attributes = []; |
1054
|
|
|
|
1055
|
1 |
|
foreach ($optionals as $opt) { |
1056
|
1 |
|
if (\is_array($opt)) { |
1057
|
1 |
|
$attributes = $opt; |
1058
|
|
|
|
1059
|
1 |
|
} else if (\is_bool($opt)) { |
1060
|
1 |
|
$switch_context = $opt; |
1061
|
|
|
|
1062
|
1 |
|
} else if (\is_string($opt)) { |
1063
|
1 |
|
$e = \array_pop($element); |
1064
|
|
|
|
1065
|
1 |
|
$element[$e] = $opt; |
1066
|
|
|
|
1067
|
|
|
} else { |
1068
|
1 |
|
throw new \Exception("Optional argument '$opt' not recognized."); |
1069
|
|
|
} |
1070
|
|
|
} |
1071
|
|
|
|
1072
|
1 |
|
return [ $element, $attributes, $switch_context ]; |
1073
|
|
|
} |
1074
|
|
|
|
1075
|
1 |
|
protected function insertElement($element, &$optionals, $fn) |
1076
|
|
|
{ |
1077
|
1 |
|
list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals); |
1078
|
|
|
|
1079
|
1 |
|
$nodes = []; |
1080
|
|
|
|
1081
|
1 |
|
foreach ($this->nodes as $n) { |
1082
|
1 |
|
foreach ($element as $k => $v) { |
1083
|
|
|
// I give up, it's a too complex job for only one method like me. |
1084
|
1 |
|
$cx = $this->handleInsertion($n, $k, $v, $fn, $optionals); |
1085
|
|
|
|
1086
|
1 |
|
$nodes = \array_merge($nodes, $cx); |
1087
|
|
|
} |
1088
|
|
|
} |
1089
|
|
|
|
1090
|
1 |
|
$new_context = $this->newContext($nodes); |
1091
|
|
|
|
1092
|
|
|
// Setting the attributes is an help that the appendChild method |
1093
|
|
|
// offers to the user and is the same of: |
1094
|
|
|
// 1. appending a child switching the context |
1095
|
|
|
// 2. setting the attributes over the new context. |
1096
|
1 |
|
if (! empty($attributes)) { |
1097
|
1 |
|
$new_context->setAttribute($attributes); |
1098
|
|
|
} |
1099
|
|
|
|
1100
|
1 |
|
if ($switch_context) { |
1101
|
1 |
|
return $new_context; |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
1 |
|
return $this; |
1105
|
|
|
} |
1106
|
|
|
|
1107
|
1 |
|
protected function handleInsertion($parent, $k, $v, $fn, &$optionals) |
1108
|
|
|
{ |
1109
|
|
|
// This is an highly optimized method. |
1110
|
|
|
// Good code design would split this method in many different handlers |
1111
|
|
|
// each one with its own checks. But it is too much expensive in terms |
1112
|
|
|
// of performances for a core method like this, so this implementation |
1113
|
|
|
// is prefered to collapse many identical checks to one. |
1114
|
|
|
|
1115
|
|
|
////////////////////// |
1116
|
|
|
// Key is a string. // |
1117
|
|
|
////////////////////// |
1118
|
|
|
|
1119
|
|
|
/////////////////////////////////////////////////////// |
1120
|
1 |
|
$k_is_string = \is_string($k); |
1121
|
1 |
|
$v_is_string = \is_string($v); |
1122
|
1 |
|
$v_is_xml = $v_is_string && is_an_xml_string($v); |
1123
|
1 |
|
$k_is_special = $k_is_string && $k[0] === '@'; |
1124
|
1 |
|
$k_isnt_special = ! $k_is_special; |
1125
|
1 |
|
$v_isnt_string = ! $v_is_string; |
1126
|
1 |
|
$v_isnt_xml = ! $v_is_xml; |
1127
|
|
|
/////////////////////////////////////////////////////// |
1128
|
|
|
|
1129
|
1 |
|
if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) { |
1130
|
1 |
|
return $this->insertStringString($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1131
|
|
|
} |
1132
|
|
|
|
1133
|
1 |
|
if ($k_is_string && $k_isnt_special && $v_is_string && $v_is_xml) { |
|
|
|
|
1134
|
|
|
// TODO |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
|
|
////////////////////////////////////////////// |
1138
|
1 |
|
$k_is_special_c = $k_is_special && $k === '@'; |
1139
|
|
|
////////////////////////////////////////////// |
1140
|
|
|
|
1141
|
1 |
|
if ($k_is_special_c && $v_is_string) { |
1142
|
1 |
|
return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
///////////////////////////////////////////////////// |
1146
|
1 |
|
$k_is_special_a = $k_is_special && ! $k_is_special_c; |
1147
|
|
|
///////////////////////////////////////////////////// |
1148
|
|
|
|
1149
|
1 |
|
if ($k_is_special_a && $v_is_string) { |
1150
|
1 |
|
return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1151
|
|
|
} |
1152
|
|
|
|
1153
|
1 |
|
if ($k_is_string && $v_isnt_string) { |
1154
|
1 |
|
return $this->insertStringMixed($parent, $k, $v, $fn, $optionals); |
1155
|
|
|
} |
1156
|
|
|
|
1157
|
|
|
//////////////////////// |
1158
|
|
|
// Key is an integer. // |
1159
|
|
|
//////////////////////// |
1160
|
|
|
|
1161
|
|
|
//////////////////////////////// |
1162
|
1 |
|
$k_is_integer = \is_integer($k); |
1163
|
1 |
|
$v_is_array = \is_array($v); |
1164
|
|
|
//////////////////////////////// |
1165
|
|
|
|
1166
|
1 |
|
if ($k_is_integer && $v_is_array) { |
1167
|
1 |
|
return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals); |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
1 |
|
if ($k_is_integer && $v_is_string && $v_isnt_xml) { |
1171
|
1 |
|
return $this->insertIntegerString($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1172
|
|
|
} |
1173
|
|
|
|
1174
|
1 |
|
if ($k_is_integer && $v_is_string && $v_is_xml) { |
1175
|
1 |
|
return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1176
|
|
|
} |
1177
|
|
|
|
1178
|
|
|
////////////////////////////////////////// |
1179
|
1 |
|
$v_is_domdoc = $v instanceof \DOMDocument; |
1180
|
|
|
////////////////////////////////////////// |
1181
|
|
|
|
1182
|
1 |
|
if ($k_is_integer && $v_is_domdoc) { |
1183
|
1 |
|
return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1184
|
|
|
} |
1185
|
|
|
|
1186
|
|
|
/////////////////////////////////////////////// |
1187
|
1 |
|
$v_is_domnodelist = $v instanceof \DOMNodeList; |
1188
|
|
|
/////////////////////////////////////////////// |
1189
|
|
|
|
1190
|
1 |
|
if ($k_is_integer && $v_is_domnodelist) { |
1191
|
1 |
|
return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1192
|
|
|
} |
1193
|
|
|
|
1194
|
|
|
/////////////////////////////////////// |
1195
|
1 |
|
$v_is_domnode = $v instanceof \DOMNode; |
1196
|
|
|
/////////////////////////////////////// |
1197
|
|
|
|
1198
|
1 |
|
if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) { |
1199
|
1 |
|
return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1200
|
|
|
} |
1201
|
|
|
|
1202
|
|
|
////////////////////////////////////////////////// |
1203
|
1 |
|
$v_is_simplexml = $v instanceof \SimpleXMLElement; |
1204
|
|
|
////////////////////////////////////////////////// |
1205
|
|
|
|
1206
|
1 |
|
if ($k_is_integer && $v_is_simplexml) { |
1207
|
1 |
|
return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1208
|
|
|
} |
1209
|
|
|
|
1210
|
|
|
//////////////////////////////////////// |
1211
|
1 |
|
$v_is_fluidxml = $v instanceof FluidXml; |
1212
|
|
|
//////////////////////////////////////// |
1213
|
|
|
|
1214
|
1 |
|
if ($k_is_integer && $v_is_fluidxml) { |
1215
|
1 |
|
return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1216
|
|
|
} |
1217
|
|
|
|
1218
|
|
|
/////////////////////////////////////////// |
1219
|
1 |
|
$v_is_fluidcx = $v instanceof FluidContext; |
1220
|
|
|
/////////////////////////////////////////// |
1221
|
|
|
|
1222
|
1 |
|
if ($k_is_integer && $v_is_fluidcx) { |
1223
|
1 |
|
return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals); |
|
|
|
|
1224
|
|
|
} |
1225
|
|
|
|
1226
|
1 |
|
throw new \Exception('XML document not supported.'); |
1227
|
|
|
} |
1228
|
|
|
|
1229
|
1 |
|
protected function createElement($name, $value = null) |
1230
|
|
|
{ |
1231
|
|
|
// The DOMElement instance must be different for every node, |
1232
|
|
|
// otherwise only one element is attached to the DOM. |
1233
|
|
|
|
1234
|
1 |
|
$id = null; |
1235
|
1 |
|
$uri = null; |
1236
|
|
|
|
1237
|
|
|
// The node name can contain the namespace id prefix. |
1238
|
|
|
// Example: xsl:template |
1239
|
1 |
|
$colon_pos = \strpos($name, ':'); |
1240
|
|
|
|
1241
|
1 |
|
if ($colon_pos !== false) { |
1242
|
1 |
|
$id = \substr($name, 0, $colon_pos); |
1243
|
1 |
|
$name = \substr($name, $colon_pos + 1); |
1244
|
|
|
} |
1245
|
|
|
|
1246
|
1 |
|
if ($id) { |
|
|
|
|
1247
|
1 |
|
$ns = $this->document->namespaces[$id]; |
1248
|
1 |
|
$uri = $ns->uri(); |
1249
|
|
|
|
1250
|
1 |
|
if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) { |
1251
|
1 |
|
$name = "{$id}:{$name}"; |
1252
|
|
|
} |
1253
|
|
|
} |
1254
|
|
|
|
1255
|
|
|
// Algorithm 1: |
1256
|
1 |
|
$el = new \DOMElement($name, $value, $uri); |
1257
|
|
|
|
1258
|
|
|
// Algorithm 2: |
1259
|
|
|
// $el = $dom->createElement($name, $value); |
1260
|
|
|
|
1261
|
1 |
|
return $el; |
1262
|
|
|
} |
1263
|
|
|
|
1264
|
1 |
|
protected function attachNodes($parent, $nodes, $fn) |
1265
|
|
|
{ |
1266
|
1 |
|
if (! \is_array($nodes) && ! $nodes instanceof \Traversable) { |
1267
|
1 |
|
$nodes = [ $nodes ]; |
1268
|
|
|
} |
1269
|
|
|
|
1270
|
1 |
|
$context = []; |
1271
|
|
|
|
1272
|
1 |
|
foreach ($nodes as $el) { |
1273
|
1 |
|
$el = $this->document->dom->importNode($el, true); |
1274
|
1 |
|
$context[] = $fn( $parent, $el); |
1275
|
|
|
} |
1276
|
|
|
|
1277
|
1 |
|
return $context; |
1278
|
|
|
} |
1279
|
|
|
|
1280
|
1 |
|
protected function insertSpecialContent($parent, $k, $v) |
|
|
|
|
1281
|
|
|
{ |
1282
|
|
|
// The user has passed an element text content: |
1283
|
|
|
// [ '@' => 'Element content.' ] |
1284
|
|
|
|
1285
|
|
|
// Algorithm 1: |
1286
|
1 |
|
$this->newContext($parent)->appendText($v); |
1287
|
|
|
|
1288
|
|
|
// Algorithm 2: |
1289
|
|
|
// $this->setText($v); |
1290
|
|
|
|
1291
|
|
|
// The user can specify multiple '@' special elements |
1292
|
|
|
// so Algorithm 1 is the right choice. |
1293
|
|
|
|
1294
|
1 |
|
return []; |
1295
|
|
|
} |
1296
|
|
|
|
1297
|
1 |
|
protected function insertSpecialAttribute($parent, $k, $v) |
1298
|
|
|
{ |
1299
|
|
|
// The user has passed an attribute name and an attribute value: |
1300
|
|
|
// [ '@attribute' => 'Attribute content' ] |
1301
|
|
|
|
1302
|
1 |
|
$attr = \substr($k, 1); |
1303
|
1 |
|
$this->newContext($parent)->setAttribute($attr, $v); |
1304
|
|
|
|
1305
|
1 |
|
return []; |
1306
|
|
|
} |
1307
|
|
|
|
1308
|
1 |
View Code Duplication |
protected function insertStringString($parent, $k, $v, $fn) |
|
|
|
|
1309
|
|
|
{ |
1310
|
|
|
// The user has passed an element name and an element value: |
1311
|
|
|
// [ 'element' => 'Element content' ] |
1312
|
|
|
|
1313
|
1 |
|
$el = $this->createElement($k, $v); |
1314
|
1 |
|
$el = $fn($parent, $el); |
1315
|
|
|
|
1316
|
1 |
|
return [ $el ]; |
1317
|
|
|
} |
1318
|
|
|
|
1319
|
1 |
View Code Duplication |
protected function insertStringMixed($parent, $k, $v, $fn, &$optionals) |
|
|
|
|
1320
|
|
|
{ |
1321
|
|
|
// The user has passed one of these two cases: |
1322
|
|
|
// - [ 'element' => [...] ] |
1323
|
|
|
// - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ] |
1324
|
|
|
|
1325
|
1 |
|
$el = $this->createElement($k); |
1326
|
1 |
|
$el = $fn($parent, $el); |
1327
|
|
|
|
1328
|
|
|
// The new children elements must be created in the order |
1329
|
|
|
// they are supplied, so 'appendChild' is the perfect operation. |
1330
|
1 |
|
$this->newContext($el)->appendChild($v, ...$optionals); |
1331
|
|
|
|
1332
|
1 |
|
return [ $el ]; |
1333
|
|
|
} |
1334
|
|
|
|
1335
|
1 |
|
protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals) |
|
|
|
|
1336
|
|
|
{ |
1337
|
|
|
// The user has passed a wrapper array: |
1338
|
|
|
// [ [...], ... ] |
1339
|
|
|
|
1340
|
1 |
|
$context = []; |
1341
|
|
|
|
1342
|
1 |
|
foreach ($v as $kk => $vv) { |
1343
|
1 |
|
$cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals); |
1344
|
|
|
|
1345
|
1 |
|
$context = \array_merge($context, $cx); |
1346
|
|
|
} |
1347
|
|
|
|
1348
|
1 |
|
return $context; |
1349
|
|
|
} |
1350
|
|
|
|
1351
|
1 |
|
protected function insertIntegerString($parent, $k, $v, $fn) |
|
|
|
|
1352
|
|
|
{ |
1353
|
|
|
// The user has passed a node name without a node value: |
1354
|
|
|
// [ 'element', ... ] |
1355
|
|
|
|
1356
|
1 |
|
$el = $this->createElement($v); |
1357
|
1 |
|
$el = $fn($parent, $el); |
1358
|
|
|
|
1359
|
1 |
|
return [ $el ]; |
1360
|
|
|
} |
1361
|
|
|
|
1362
|
1 |
|
protected function insertIntegerXml($parent, $k, $v, $fn) |
|
|
|
|
1363
|
|
|
{ |
1364
|
|
|
// The user has passed an XML document instance: |
1365
|
|
|
// [ '<tag></tag>', DOMNode, SimpleXMLElement, FluidXml ] |
1366
|
|
|
|
1367
|
1 |
|
$wrapper = new \DOMDocument(); |
1368
|
1 |
|
$wrapper->formatOutput = true; |
1369
|
1 |
|
$wrapper->preserveWhiteSpace = false; |
1370
|
|
|
|
1371
|
1 |
|
$v = \ltrim($v); |
1372
|
|
|
|
1373
|
1 |
|
if ($v[1] === '?') { |
1374
|
1 |
|
$wrapper->loadXML($v); |
1375
|
1 |
|
$nodes = $wrapper->childNodes; |
1376
|
|
|
} else { |
1377
|
|
|
// A way to import strings with multiple root nodes. |
1378
|
1 |
|
$wrapper->loadXML("<root>$v</root>"); |
1379
|
|
|
|
1380
|
|
|
// Algorithm 1: |
1381
|
1 |
|
$nodes = $wrapper->documentElement->childNodes; |
1382
|
|
|
|
1383
|
|
|
// Algorithm 2: |
1384
|
|
|
// $dom_xp = new \DOMXPath($dom); |
1385
|
|
|
// $nodes = $dom_xp->query('/root/*'); |
1386
|
|
|
} |
1387
|
|
|
|
1388
|
1 |
|
return $this->attachNodes($parent, $nodes, $fn); |
1389
|
|
|
} |
1390
|
|
|
|
1391
|
1 |
|
protected function insertIntegerDomdocument($parent, $k, $v, $fn) |
|
|
|
|
1392
|
|
|
{ |
1393
|
|
|
// A DOMDocument can have multiple root nodes. |
1394
|
|
|
|
1395
|
|
|
// Algorithm 1: |
1396
|
1 |
|
return $this->attachNodes($parent, $v->childNodes, $fn); |
1397
|
|
|
|
1398
|
|
|
// Algorithm 2: |
1399
|
|
|
// return $this->attachNodes($parent, $v->documentElement, $fn); |
1400
|
|
|
} |
1401
|
|
|
|
1402
|
1 |
|
protected function insertIntegerDomnodelist($parent, $k, $v, $fn) |
|
|
|
|
1403
|
|
|
{ |
1404
|
1 |
|
return $this->attachNodes($parent, $v, $fn); |
1405
|
|
|
} |
1406
|
|
|
|
1407
|
1 |
|
protected function insertIntegerDomnode($parent, $k, $v, $fn) |
|
|
|
|
1408
|
|
|
{ |
1409
|
1 |
|
return $this->attachNodes($parent, $v, $fn); |
1410
|
|
|
} |
1411
|
|
|
|
1412
|
1 |
|
protected function insertIntegerSimplexml($parent, $k, $v, $fn) |
|
|
|
|
1413
|
|
|
{ |
1414
|
1 |
|
return $this->attachNodes($parent, \dom_import_simplexml($v), $fn); |
1415
|
|
|
} |
1416
|
|
|
|
1417
|
1 |
|
protected function insertIntegerFluidxml($parent, $k, $v, $fn) |
|
|
|
|
1418
|
|
|
{ |
1419
|
1 |
|
return $this->attachNodes($parent, $v->dom()->documentElement, $fn); |
1420
|
|
|
} |
1421
|
|
|
|
1422
|
1 |
|
protected function insertIntegerFluidcontext($parent, $k, $v, $fn) |
|
|
|
|
1423
|
|
|
{ |
1424
|
1 |
|
return $this->attachNodes($parent, $v->asArray(), $fn); |
1425
|
|
|
} |
1426
|
|
|
} |
1427
|
|
|
|
1428
|
|
|
} // END OF NAMESPACE FluidXml\Core |
1429
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.