1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace FluidXml; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* @method FluidXml namespace(...$arguments) |
7
|
|
|
*/ |
8
|
|
|
class FluidXml implements FluidInterface |
9
|
|
|
{ |
10
|
|
|
use NewableTrait, |
11
|
|
|
FluidSaveTrait, |
12
|
|
|
ReservedCallTrait, // For compatibility with PHP 5.6. |
13
|
|
|
ReservedCallStaticTrait; // For compatibility with PHP 5.6. |
14
|
|
|
|
15
|
|
|
const ROOT_NODE = 'doc'; |
16
|
|
|
|
17
|
|
|
private $defaults = [ 'root' => self::ROOT_NODE, |
18
|
|
|
'version' => '1.0', |
19
|
|
|
'encoding' => 'UTF-8', |
20
|
|
|
'stylesheet' => null ]; |
21
|
|
|
|
22
|
|
|
private $document; |
23
|
|
|
private $handler; |
24
|
|
|
|
25
|
|
|
private $context; |
26
|
|
|
private $contextEl; |
27
|
|
|
|
28
|
|
|
public static function load($document) |
29
|
|
|
{ |
30
|
|
|
$file = $document; |
31
|
|
|
$document = \file_get_contents($file); |
32
|
|
|
|
33
|
|
|
// file_get_contents() returns false in case of error. |
34
|
|
|
if (! $document) { |
35
|
|
|
throw new \Exception("File '$file' not accessible."); |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
return (new FluidXml(null))->addChild($document); |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
public function __construct(...$arguments) |
42
|
|
|
{ |
43
|
|
|
// First, we parse the arguments detecting the options provided. |
44
|
|
|
// This options are needed to build the DOM, add the stylesheet |
45
|
|
|
// and to create the document root/structure. |
46
|
|
|
$options = $this->buildOptions($arguments); |
47
|
|
|
|
48
|
|
|
// Having the options set, we can build the FluidDocument model |
49
|
|
|
// which incapsulates the DOM and the corresponding XPath instance. |
50
|
|
|
$document = new FluidDocument(); |
51
|
|
|
$document->dom = $this->buildDom($options); |
52
|
|
|
$document->xpath = new \DOMXPath($document->dom); |
53
|
|
|
|
54
|
|
|
// After the FluidDocument model creation, we can proceed to build |
55
|
|
|
// the FluidInsertionHandler which requires the model to perform |
56
|
|
|
// its logics. |
57
|
|
|
$handler = new FluidInsertionHandler($document); |
58
|
|
|
|
59
|
|
|
// Ok, it's time to let them beeing visible along the instance. |
60
|
|
|
$this->document = $document; |
61
|
|
|
$this->handler = $handler; |
62
|
|
|
|
63
|
|
|
// Now, we can further populate the DOM with any stylesheet or child. |
64
|
|
|
$this->initStylesheet($options) |
65
|
|
|
->initRoot($options); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
protected function buildOptions(&$arguments) |
69
|
|
|
{ |
70
|
|
|
$custom = []; |
71
|
|
|
|
72
|
|
|
if (\count($arguments) > 0) { |
73
|
|
|
// The root option can be specified as first argument |
74
|
|
|
// because it is the most common. |
75
|
|
|
$this->defaults['root'] = $arguments[0]; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
if (\count($arguments) > 1) { |
79
|
|
|
// Custom options can be specified only as second argument, |
80
|
|
|
// to avoid confusion with array to XML construction style. |
81
|
|
|
$custom = $arguments[1]; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
return \array_merge($this->defaults, $custom); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
private function buildDom(&$options) |
88
|
|
|
{ |
89
|
|
|
$dom = new \DOMDocument($options['version'], $options['encoding']); |
90
|
|
|
$dom->formatOutput = true; |
91
|
|
|
$dom->preserveWhiteSpace = false; |
92
|
|
|
|
93
|
|
|
return $dom; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
private function initStylesheet(&$options) |
97
|
|
|
{ |
98
|
|
|
if (! empty($options['stylesheet'])) { |
99
|
|
|
$attrs = 'type="text/xsl" ' |
100
|
|
|
. "encoding=\"{$options['encoding']}\" " |
101
|
|
|
. 'indent="yes" ' |
102
|
|
|
. "href=\"{$options['stylesheet']}\""; |
103
|
|
|
|
104
|
|
|
$stylesheet = new \DOMProcessingInstruction('xml-stylesheet', $attrs); |
105
|
|
|
|
106
|
|
|
$this->addChild($stylesheet); |
107
|
|
|
|
108
|
|
|
// Algorithm 2: |
109
|
|
|
// Used in case the order of the stylesheet and root creation is reversed. |
110
|
|
|
// $this->document->dom->insertBefore($stylesheet, $this->document->dom->documentElement); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
return $this; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
private function initRoot(&$options) |
117
|
|
|
{ |
118
|
|
|
if (! empty($options['root'])) { |
119
|
|
|
$this->appendSibling($options['root']); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return $this; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
public function length() |
126
|
|
|
{ |
127
|
|
|
return 1; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
// Alias of ->length(). |
131
|
|
|
public function size() |
132
|
|
|
{ |
133
|
|
|
return $this->length(); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
public function dom() |
137
|
|
|
{ |
138
|
|
|
return $this->document->dom; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
// This method should be called 'array', |
142
|
|
|
// but for compatibility with PHP 5.6 |
143
|
|
|
// it is shadowed by the __call() method. |
144
|
|
|
public function array_() |
145
|
|
|
{ |
146
|
|
|
return [ $this->document->dom ]; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function __toString() |
150
|
|
|
{ |
151
|
|
|
return $this->xml(); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
public function xml($strip = false) |
155
|
|
|
{ |
156
|
|
|
if ($strip) { |
157
|
|
|
return FluidHelper::domdocumentToStringWithoutHeaders($this->document->dom); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
return $this->document->dom->saveXML(); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
public function html($strip = false) |
164
|
|
|
{ |
165
|
|
|
$header = "<!DOCTYPE html>\n"; |
166
|
|
|
|
167
|
|
|
if ($strip) { |
168
|
|
|
$header = ''; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$html = FluidHelper::domdocumentToStringWithoutHeaders($this->document->dom, true); |
172
|
|
|
|
173
|
|
|
return "{$header}{$html}"; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
public function namespaces() |
177
|
|
|
{ |
178
|
|
|
return $this->document->namespaces; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
// This method should be called 'namespace', |
182
|
|
|
// but for compatibility with PHP 5.6 |
183
|
|
|
// it is shadowed by the __call() method. |
184
|
|
|
protected function namespace_(...$arguments) |
185
|
|
|
{ |
186
|
|
|
$namespaces = []; |
187
|
|
|
|
188
|
|
|
if (\is_string($arguments[0])) { |
189
|
|
|
$args = [ $arguments[0], $arguments[1] ]; |
190
|
|
|
|
191
|
|
|
if (isset($arguments[2])) { |
192
|
|
|
$args[] = $arguments[2]; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
$namespaces[] = new FluidNamespace(...$args); |
196
|
|
|
} elseif (\is_array($arguments[0])) { |
197
|
|
|
$namespaces = $arguments[0]; |
198
|
|
|
} else { |
199
|
|
|
$namespaces = $arguments; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
foreach ($namespaces as $n) { |
203
|
|
|
$this->document->namespaces[$n->id()] = $n; |
204
|
|
|
$this->document->xpath->registerNamespace($n->id(), $n->uri()); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $this; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
public function query(...$xpath) |
211
|
|
|
{ |
212
|
|
|
return $this->context()->query(...$xpath); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
// Alias of ->query(). |
216
|
|
|
public function __invoke(...$xpath) |
217
|
|
|
{ |
218
|
|
|
return $this->query(...$xpath); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
public function times($times, callable $fn = null) |
222
|
|
|
{ |
223
|
|
|
return $this->context()->times($times, $fn); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
public function each(callable $fn) |
227
|
|
|
{ |
228
|
|
|
return $this->context()->each($fn); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
public function filter(callable $fn) |
232
|
|
|
{ |
233
|
|
|
return $this->context()->filter($fn); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
public function addChild($child, ...$optionals) |
237
|
|
|
{ |
238
|
|
|
// If the user has requested ['root' => null] at construction time |
239
|
|
|
// 'context()' promotes DOMDocument as root node. |
240
|
|
|
$context = $this->context(); |
241
|
|
|
$new_context = $context->addChild($child, ...$optionals); |
242
|
|
|
|
243
|
|
|
return $this->chooseContext($context, $new_context); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
// Alias of ->addChild(). |
247
|
|
|
public function add($child, ...$optionals) |
248
|
|
|
{ |
249
|
|
|
return $this->addChild($child, ...$optionals); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
View Code Duplication |
public function prependSibling($sibling, ...$optionals) |
|
|
|
|
253
|
|
|
{ |
254
|
|
|
if ($this->document->dom->documentElement === null) { |
255
|
|
|
// If the document doesn't have at least one root node, |
256
|
|
|
// the sibling creation fails. In this case we replace |
257
|
|
|
// the sibling creation with the creation of a generic node. |
258
|
|
|
return $this->addChild($sibling, ...$optionals); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$context = $this->context(); |
262
|
|
|
$new_context = $context->prependSibling($sibling, ...$optionals); |
263
|
|
|
|
264
|
|
|
return $this->chooseContext($context, $new_context); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
// Alias of ->prependSibling(). |
268
|
|
|
public function prepend($sibling, ...$optionals) |
269
|
|
|
{ |
270
|
|
|
return $this->prependSibling($sibling, ...$optionals); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
View Code Duplication |
public function appendSibling($sibling, ...$optionals) |
|
|
|
|
274
|
|
|
{ |
275
|
|
|
if ($this->document->dom->documentElement === null) { |
276
|
|
|
// If the document doesn't have at least one root node, |
277
|
|
|
// the sibling creation fails. In this case we replace |
278
|
|
|
// the sibling creation with the creation of a generic node. |
279
|
|
|
return $this->addChild($sibling, ...$optionals); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
$context = $this->context(); |
283
|
|
|
$new_context = $context->appendSibling($sibling, ...$optionals); |
284
|
|
|
|
285
|
|
|
return $this->chooseContext($context, $new_context); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
// Alias of ->appendSibling(). |
289
|
|
|
public function append($sibling, ...$optionals) |
290
|
|
|
{ |
291
|
|
|
return $this->appendSibling($sibling, ...$optionals); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
public function setAttribute(...$arguments) |
295
|
|
|
{ |
296
|
|
|
$this->context()->setAttribute(...$arguments); |
297
|
|
|
|
298
|
|
|
return $this; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
// Alias of ->setAttribute(). |
302
|
|
|
public function attr(...$arguments) |
303
|
|
|
{ |
304
|
|
|
return $this->setAttribute(...$arguments); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
public function setText($text) |
308
|
|
|
{ |
309
|
|
|
$this->context()->setText($text); |
310
|
|
|
|
311
|
|
|
return $this; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
// Alias of ->setText(). |
315
|
|
|
public function text($text) |
316
|
|
|
{ |
317
|
|
|
return $this->setText($text); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
public function addText($text) |
321
|
|
|
{ |
322
|
|
|
$this->context()->addText($text); |
323
|
|
|
|
324
|
|
|
return $this; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
public function setCdata($text) |
328
|
|
|
{ |
329
|
|
|
$this->context()->setCdata($text); |
330
|
|
|
|
331
|
|
|
return $this; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// Alias of ->setCdata(). |
335
|
|
|
public function cdata($text) |
336
|
|
|
{ |
337
|
|
|
return $this->setCdata($text); |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
public function addCdata($text) |
341
|
|
|
{ |
342
|
|
|
$this->context()->addCdata($text); |
343
|
|
|
|
344
|
|
|
return $this; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
public function setComment($text) |
348
|
|
|
{ |
349
|
|
|
$this->context()->setComment($text); |
350
|
|
|
|
351
|
|
|
return $this; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
// Alias of ->setComment(). |
355
|
|
|
public function comment($text) |
356
|
|
|
{ |
357
|
|
|
return $this->setComment($text); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
public function addComment($text) |
361
|
|
|
{ |
362
|
|
|
$this->context()->addComment($text); |
363
|
|
|
|
364
|
|
|
return $this; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
public function remove(...$xpath) |
368
|
|
|
{ |
369
|
|
|
$this->context()->remove(...$xpath); |
370
|
|
|
|
371
|
|
|
return $this; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
protected function context() |
375
|
|
|
{ |
376
|
|
|
$el = $this->document->dom->documentElement; |
377
|
|
|
|
378
|
|
|
if ($el === null) { |
379
|
|
|
// Whether there is not a root node |
380
|
|
|
// the DOMDocument is promoted as root node. |
381
|
|
|
$el = $this->document->dom; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
if ($this->context === null || $el !== $this->contextEl) { |
385
|
|
|
// The user can prepend a root node to the current root node. |
386
|
|
|
// In this case we have to update the context with the new first root node. |
387
|
|
|
$this->context = new FluidContext($this->document, $this->handler, $el); |
388
|
|
|
$this->contextEl = $el; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
return $this->context; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
protected function chooseContext($help_context, $new_context) |
395
|
|
|
{ |
396
|
|
|
// If the two contextes are diffent, the user has requested |
397
|
|
|
// a switch of the context and we have to return it. |
398
|
|
|
if ($help_context !== $new_context) { |
399
|
|
|
return $new_context; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
return $this; |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
|
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.