1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace FluidXml; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* @method array array() |
7
|
|
|
*/ |
8
|
|
|
class FluidContext implements FluidInterface, \ArrayAccess, \Iterator |
9
|
|
|
{ |
10
|
|
|
use FluidAliasesTrait, |
11
|
|
|
FluidSaveTrait, |
12
|
|
|
NewableTrait, |
13
|
|
|
ReservedCallTrait, // For compatibility with PHP 5.6. |
14
|
|
|
ReservedCallStaticTrait; // For compatibility with PHP 5.6. |
15
|
|
|
|
16
|
|
|
private $document; |
17
|
|
|
private $handler; |
18
|
|
|
private $nodes = []; |
19
|
|
|
private $seek = 0; |
20
|
|
|
|
21
|
1 |
|
public function __construct($document, $handler, $context) |
22
|
|
|
{ |
23
|
1 |
|
$this->document = $document; |
24
|
1 |
|
$this->handler = $handler; |
25
|
|
|
|
26
|
1 |
|
if (! \is_array($context) && ! $context instanceof \Traversable) { |
27
|
|
|
// DOMDocument, DOMElement and DOMNode are not iterable. |
28
|
|
|
// DOMNodeList and FluidContext are iterable. |
29
|
1 |
|
$context = [ $context ]; |
30
|
|
|
} |
31
|
|
|
|
32
|
1 |
|
foreach ($context as $n) { |
33
|
1 |
|
if (! $n instanceof \DOMNode) { |
34
|
1 |
|
throw new \Exception('Node type not recognized.'); |
35
|
|
|
} |
36
|
|
|
|
37
|
1 |
|
$this->nodes[] = $n; |
38
|
|
|
} |
39
|
1 |
|
} |
40
|
|
|
|
41
|
|
|
// \ArrayAccess interface. |
42
|
1 |
|
public function offsetSet($offset, $value) |
43
|
|
|
{ |
44
|
|
|
// if (\is_null($offset)) { |
45
|
|
|
// $this->nodes[] = $value; |
46
|
|
|
// } else { |
47
|
|
|
// $this->nodes[$offset] = $value; |
48
|
|
|
// } |
49
|
1 |
|
throw new \Exception('Setting a context element is not allowed.'); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
// \ArrayAccess interface. |
53
|
1 |
|
public function offsetExists($offset) |
54
|
|
|
{ |
55
|
1 |
|
return isset($this->nodes[$offset]); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
// \ArrayAccess interface. |
59
|
1 |
|
public function offsetUnset($offset) |
60
|
|
|
{ |
61
|
|
|
// unset($this->nodes[$offset]); |
62
|
1 |
|
\array_splice($this->nodes, $offset, 1); |
63
|
1 |
|
} |
64
|
|
|
|
65
|
|
|
// \ArrayAccess interface. |
66
|
1 |
|
public function offsetGet($offset) |
67
|
|
|
{ |
68
|
1 |
|
if (isset($this->nodes[$offset])) { |
69
|
1 |
|
return $this->nodes[$offset]; |
70
|
|
|
} |
71
|
|
|
|
72
|
1 |
|
return null; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
// \Iterator interface. |
76
|
1 |
|
public function rewind() |
77
|
|
|
{ |
78
|
1 |
|
$this->seek = 0; |
79
|
1 |
|
} |
80
|
|
|
|
81
|
|
|
// \Iterator interface. |
82
|
1 |
|
public function current() |
83
|
|
|
{ |
84
|
1 |
|
return $this->nodes[$this->seek]; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// \Iterator interface. |
88
|
1 |
|
public function key() |
89
|
|
|
{ |
90
|
1 |
|
return $this->seek; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// \Iterator interface. |
94
|
1 |
|
public function next() |
95
|
|
|
{ |
96
|
1 |
|
++$this->seek; |
97
|
1 |
|
} |
98
|
|
|
|
99
|
|
|
// \Iterator interface. |
100
|
1 |
|
public function valid() |
101
|
|
|
{ |
102
|
1 |
|
return isset($this->nodes[$this->seek]); |
103
|
|
|
} |
104
|
|
|
|
105
|
1 |
|
public function length() |
106
|
|
|
{ |
107
|
1 |
|
return \count($this->nodes); |
108
|
|
|
} |
109
|
|
|
|
110
|
1 |
|
public function query(...$query) |
111
|
|
|
{ |
112
|
1 |
|
if (\is_array($query[0])) { |
113
|
1 |
|
$query = $query[0]; |
114
|
|
|
} |
115
|
|
|
|
116
|
1 |
|
$results = []; |
117
|
|
|
|
118
|
1 |
|
$xp = $this->document->xpath; |
119
|
|
|
|
120
|
1 |
|
foreach ($this->nodes as $n) { |
121
|
1 |
|
foreach ($query as $q) { |
122
|
1 |
|
$q = $this->resolveQuery($q); |
123
|
|
|
|
124
|
|
|
// Returns a DOMNodeList. |
125
|
1 |
|
$res = $xp->query($q, $n); |
126
|
|
|
|
127
|
|
|
// Algorithm 1: |
128
|
|
|
// $results = \array_merge($results, \iterator_to_array($res)); |
129
|
|
|
|
130
|
|
|
// Algorithm 2: |
131
|
|
|
// It is faster than \iterator_to_array and a lot faster |
132
|
|
|
// than \iterator_to_array + \array_merge. |
133
|
1 |
|
foreach ($res as $r) { |
134
|
1 |
|
$results[] = $r; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// Algorithm 3: |
138
|
|
|
// for ($i = 0, $l = $res->length; $i < $l; ++$i) { |
139
|
|
|
// $results[] = $res->item($i); |
140
|
|
|
// } |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// Performing over multiple sibling nodes a query that ascends |
145
|
|
|
// the xpath, relative (../..) or absolute (//), returns identical |
146
|
|
|
// matching results that must be collapsed in an unique result |
147
|
|
|
// otherwise a subsequent operation is performed multiple times. |
148
|
1 |
|
$results = $this->filterQueryResults($results); |
149
|
|
|
|
150
|
1 |
|
return $this->newContext($results); |
151
|
|
|
} |
152
|
|
|
|
153
|
1 |
|
public function __invoke(...$query) |
154
|
|
|
{ |
155
|
1 |
|
return $this->query(...$query); |
156
|
|
|
} |
157
|
|
|
|
158
|
1 |
|
public function times($times, callable $fn = null) |
159
|
|
|
{ |
160
|
1 |
|
if ($fn === null) { |
161
|
1 |
|
return new FluidRepeater($this->document, $this->handler, $this, $times); |
162
|
|
|
} |
163
|
|
|
|
164
|
1 |
|
for ($i = 0; $i < $times; ++$i) { |
165
|
1 |
|
$this->callfn($fn, [$this, $i]); |
166
|
|
|
} |
167
|
|
|
|
168
|
1 |
|
return $this; |
169
|
|
|
} |
170
|
|
|
|
171
|
1 |
|
public function each(callable $fn) |
172
|
|
|
{ |
173
|
1 |
View Code Duplication |
foreach ($this->nodes as $i => $n) { |
|
|
|
|
174
|
1 |
|
$cx = $this->newContext($n); |
175
|
|
|
|
176
|
1 |
|
$this->callfn($fn, [$cx, $i, $n]); |
177
|
|
|
} |
178
|
|
|
|
179
|
1 |
|
return $this; |
180
|
|
|
} |
181
|
|
|
|
182
|
1 |
|
public function map(callable $fn) |
183
|
|
|
{ |
184
|
1 |
|
$result = []; |
185
|
|
|
|
186
|
1 |
View Code Duplication |
foreach ($this->nodes as $i => $n) { |
|
|
|
|
187
|
1 |
|
$cx = $this->newContext($n); |
188
|
|
|
|
189
|
1 |
|
$result[] = $this->callfn($fn, [$cx, $i, $n]); |
190
|
|
|
} |
191
|
|
|
|
192
|
1 |
|
return $result; |
193
|
|
|
} |
194
|
|
|
|
195
|
1 |
|
public function filter(callable $fn) |
196
|
|
|
{ |
197
|
1 |
|
$nodes = []; |
198
|
|
|
|
199
|
1 |
|
foreach ($this->nodes as $i => $n) { |
200
|
1 |
|
$cx = $this->newContext($n); |
201
|
|
|
|
202
|
1 |
|
$ret = $this->callfn($fn, [$cx, $i, $n]); |
203
|
|
|
|
204
|
1 |
|
if ($ret !== false) { |
205
|
1 |
|
$nodes[] = $n; |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
1 |
|
return $this->newContext($nodes); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// addChild($child, $value?, $attributes? = [], $switchContext? = false) |
213
|
1 |
|
public function addChild($child, ...$optionals) |
214
|
|
|
{ |
215
|
|
|
return $this->handler->insertElement($this->nodes, $child, $optionals, function ($parent, $element) { |
216
|
1 |
|
return $parent->appendChild($element); |
217
|
1 |
|
}, $this); |
218
|
|
|
} |
219
|
|
|
|
220
|
1 |
View Code Duplication |
public function prependSibling($sibling, ...$optionals) |
|
|
|
|
221
|
|
|
{ |
222
|
|
|
return $this->handler->insertElement($this->nodes, $sibling, $optionals, function ($sibling, $element) { |
223
|
1 |
|
if ($sibling->parentNode === null) { |
224
|
|
|
// If the node doesn't have at least one parent node, |
225
|
|
|
// the sibling creation fails. In this case we replace |
226
|
|
|
// the sibling creation with the creation of a child node. |
227
|
|
|
// Useful when dealing with a DOMDocument with a null |
228
|
|
|
// documentElement property. |
229
|
1 |
|
return $sibling->appendChild($element); |
230
|
|
|
} |
231
|
1 |
|
return $sibling->parentNode->insertBefore($element, $sibling); |
232
|
1 |
|
}, $this); |
233
|
|
|
} |
234
|
|
|
|
235
|
1 |
View Code Duplication |
public function appendSibling($sibling, ...$optionals) |
|
|
|
|
236
|
|
|
{ |
237
|
|
|
return $this->handler->insertElement($this->nodes, $sibling, $optionals, function ($sibling, $element) { |
238
|
1 |
|
if ($sibling->parentNode === null) { |
239
|
|
|
// If the node doesn't have at least one parent node, |
240
|
|
|
// the sibling creation fails. In this case we replace |
241
|
|
|
// the sibling creation with the creation of a child node. |
242
|
|
|
// Useful when dealing with a DOMDocument with a null |
243
|
|
|
// documentElement property. |
244
|
1 |
|
return $sibling->appendChild($element); |
245
|
|
|
} |
246
|
|
|
// If ->nextSibling is null, $element is simply appended as last sibling. |
247
|
1 |
|
return $sibling->parentNode->insertBefore($element, $sibling->nextSibling); |
248
|
1 |
|
}, $this); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
// setAttribute($name, $value) |
252
|
|
|
// setAttribute(['name' => 'value', ...]) |
253
|
1 |
|
public function setAttribute($name, $value = null) |
254
|
|
|
{ |
255
|
1 |
|
if (\is_array($name)) { |
256
|
1 |
|
$attrs = $name; |
257
|
|
|
} else { |
258
|
1 |
|
$attrs = [ $name => $value ]; |
259
|
|
|
} |
260
|
|
|
|
261
|
1 |
|
foreach ($this->nodes as $n) { |
262
|
1 |
|
foreach ($attrs as $k => $v) { |
263
|
1 |
|
if (\is_integer($k)) { |
264
|
1 |
|
$k = $v; |
265
|
1 |
|
$v = null; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
// Algorithm 1: |
269
|
1 |
|
$n->setAttribute($k, $v); |
270
|
|
|
|
271
|
|
|
// Algorithm 2: |
272
|
|
|
// $n->setAttributeNode(new \DOMAttr($k, $v)); |
273
|
|
|
|
274
|
|
|
// Algorithm 3: |
275
|
|
|
// $n->appendChild(new \DOMAttr($k, $v)); |
276
|
|
|
|
277
|
|
|
// Algorithm 2 and 3 have a different behaviour |
278
|
|
|
// from Algorithm 1. |
279
|
|
|
// The attribute is still created or setted, but |
280
|
|
|
// changing the value of an existing attribute |
281
|
|
|
// changes even the order of that attribute |
282
|
|
|
// in the attribute list. |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
286
|
1 |
|
return $this; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
public function getText($glue = PHP_EOL) |
290
|
|
|
{ |
291
|
|
|
return implode($glue, $this->map(function ($i, $n) { return $n->textContent; })); |
292
|
|
|
} |
293
|
|
|
|
294
|
1 |
|
public function setText($text) |
295
|
|
|
{ |
296
|
1 |
|
foreach ($this->nodes as $n) { |
297
|
|
|
// Algorithm 1: |
298
|
1 |
|
$n->nodeValue = $text; |
299
|
|
|
|
300
|
|
|
// Algorithm 2: |
301
|
|
|
// foreach ($n->childNodes as $c) { |
302
|
|
|
// $n->removeChild($c); |
303
|
|
|
// } |
304
|
|
|
// $n->appendChild(new \DOMText($text)); |
305
|
|
|
|
306
|
|
|
// Algorithm 3: |
307
|
|
|
// foreach ($n->childNodes as $c) { |
308
|
|
|
// $n->replaceChild(new \DOMText($text), $c); |
309
|
|
|
// } |
310
|
|
|
} |
311
|
|
|
|
312
|
1 |
|
return $this; |
313
|
|
|
} |
314
|
|
|
|
315
|
1 |
|
public function addText($text) |
316
|
|
|
{ |
317
|
1 |
|
foreach ($this->nodes as $n) { |
318
|
1 |
|
$n->appendChild(new \DOMText($text)); |
319
|
|
|
} |
320
|
|
|
|
321
|
1 |
|
return $this; |
322
|
|
|
} |
323
|
|
|
|
324
|
1 |
|
public function setCdata($text) |
325
|
|
|
{ |
326
|
1 |
|
foreach ($this->nodes as $n) { |
327
|
1 |
|
$n->nodeValue = ''; |
328
|
1 |
|
$n->appendChild(new \DOMCDATASection($text)); |
329
|
|
|
} |
330
|
|
|
|
331
|
1 |
|
return $this; |
332
|
|
|
} |
333
|
|
|
|
334
|
1 |
|
public function addCdata($text) |
335
|
|
|
{ |
336
|
1 |
|
foreach ($this->nodes as $n) { |
337
|
1 |
|
$n->appendChild(new \DOMCDATASection($text)); |
338
|
|
|
} |
339
|
|
|
|
340
|
1 |
|
return $this; |
341
|
|
|
} |
342
|
|
|
|
343
|
1 |
|
public function setComment($text) |
344
|
|
|
{ |
345
|
1 |
|
foreach ($this->nodes as $n) { |
346
|
1 |
|
$n->nodeValue = ''; |
347
|
1 |
|
$n->appendChild(new \DOMComment($text)); |
348
|
|
|
} |
349
|
|
|
|
350
|
1 |
|
return $this; |
351
|
|
|
} |
352
|
|
|
|
353
|
1 |
|
public function addComment($text) |
354
|
|
|
{ |
355
|
1 |
|
foreach ($this->nodes as $n) { |
356
|
1 |
|
$n->appendChild(new \DOMComment($text)); |
357
|
|
|
} |
358
|
|
|
|
359
|
1 |
|
return $this; |
360
|
|
|
} |
361
|
|
|
|
362
|
1 |
|
public function remove(...$query) |
363
|
|
|
{ |
364
|
|
|
// Arguments can be empty, a string or an array of strings. |
365
|
|
|
|
366
|
1 |
|
if (empty($query)) { |
367
|
|
|
// The user has requested to remove the nodes of this context. |
368
|
1 |
|
$targets = $this->nodes; |
369
|
|
|
} else { |
370
|
1 |
|
$targets = $this->query(...$query); |
371
|
|
|
} |
372
|
|
|
|
373
|
1 |
|
foreach ($targets as $t) { |
374
|
1 |
|
$t->parentNode->removeChild($t); |
375
|
|
|
} |
376
|
|
|
|
377
|
1 |
|
return $this; |
378
|
|
|
} |
379
|
|
|
|
380
|
1 |
|
public function dom() |
381
|
|
|
{ |
382
|
1 |
|
return $this->document->dom; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
// This method should be called 'array', |
386
|
|
|
// but for compatibility with PHP 5.6 |
387
|
|
|
// it is shadowed by the __call() method. |
388
|
1 |
|
public function array_() |
389
|
|
|
{ |
390
|
1 |
|
return $this->nodes; |
391
|
|
|
} |
392
|
|
|
|
393
|
1 |
|
public function __toString() |
394
|
|
|
{ |
395
|
1 |
|
return $this->xml(); |
396
|
|
|
} |
397
|
|
|
|
398
|
1 |
|
public function xml($strip = false) |
399
|
|
|
{ |
400
|
1 |
|
return FluidHelper::domnodesToString($this->nodes); |
401
|
|
|
} |
402
|
|
|
|
403
|
1 |
|
public function html($strip = false) |
404
|
|
|
{ |
405
|
1 |
|
return FluidHelper::domnodesToString($this->nodes, true); |
406
|
|
|
} |
407
|
|
|
|
408
|
1 |
|
protected function newContext(&$context) |
409
|
|
|
{ |
410
|
1 |
|
return new FluidContext($this->document, $this->handler, $context); |
411
|
|
|
} |
412
|
|
|
|
413
|
1 |
|
protected function resolveQuery($query) |
414
|
|
|
{ |
415
|
1 |
|
if ( $query === '.' |
416
|
1 |
|
|| $query[0] === '/' |
417
|
1 |
|
|| ( $query[0] === '.' && $query[1] === '/' ) |
418
|
1 |
|
|| ( $query[0] === '.' && $query[1] === '.' ) ) { |
419
|
1 |
|
return $query; |
420
|
|
|
} |
421
|
|
|
|
422
|
1 |
|
return CssTranslator::xpath($query); |
423
|
|
|
} |
424
|
|
|
|
425
|
1 |
|
protected function filterQueryResults(&$results) |
426
|
|
|
{ |
427
|
1 |
|
$set = []; |
428
|
|
|
|
429
|
1 |
|
foreach ($results as $r) { |
430
|
1 |
|
$found = false; |
431
|
|
|
|
432
|
1 |
|
foreach ($set as $u) { |
433
|
1 |
|
$found = ($r === $u) || $found; |
434
|
|
|
} |
435
|
|
|
|
436
|
1 |
|
if (! $found) { |
437
|
1 |
|
$set[] = $r; |
438
|
|
|
} |
439
|
|
|
} |
440
|
|
|
|
441
|
1 |
|
return $set; |
442
|
|
|
} |
443
|
|
|
|
444
|
1 |
|
protected function callfn($fn, $args) |
445
|
|
|
{ |
446
|
1 |
|
if ($fn instanceof \Closure) { |
447
|
1 |
|
$bind = \array_shift($args); |
448
|
|
|
|
449
|
1 |
|
$fn = $fn->bindTo($bind); |
450
|
|
|
|
451
|
|
|
// It is faster than \call_user_func. |
452
|
1 |
|
return $fn(...$args); |
453
|
|
|
} |
454
|
|
|
|
455
|
1 |
|
return \call_user_func($fn, ...$args); |
456
|
|
|
} |
457
|
|
|
} |
458
|
|
|
|
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.