1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace voku\helper; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* @property-read string $plaintext |
9
|
|
|
* <p>Get dom node's plain text.</p> |
10
|
|
|
* |
11
|
|
|
* @method static XmlDomParser file_get_xml($xml, $libXMLExtraOptions = null) |
12
|
|
|
* <p>Load XML from file.</p> |
13
|
|
|
* @method static XmlDomParser str_get_xml($xml, $libXMLExtraOptions = null) |
14
|
|
|
* <p>Load XML from string.</p> |
15
|
|
|
*/ |
16
|
|
|
class XmlDomParser extends AbstractDomParser |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* @param \DOMNode|SimpleXmlDomInterface|string $element HTML code or SimpleXmlDomInterface, \DOMNode |
20
|
|
|
*/ |
21
|
3 |
|
public function __construct($element = null) |
22
|
|
|
{ |
23
|
3 |
|
$this->document = new \DOMDocument('1.0', $this->getEncoding()); |
24
|
|
|
|
25
|
|
|
// DOMDocument settings |
26
|
3 |
|
$this->document->preserveWhiteSpace = true; |
27
|
3 |
|
$this->document->formatOutput = true; |
28
|
|
|
|
29
|
3 |
|
if ($element instanceof SimpleXmlDomInterface) { |
30
|
|
|
$element = $element->getNode(); |
31
|
|
|
} |
32
|
|
|
|
33
|
3 |
|
if ($element instanceof \DOMNode) { |
34
|
|
|
$domNode = $this->document->importNode($element, true); |
35
|
|
|
|
36
|
|
|
if ($domNode instanceof \DOMNode) { |
37
|
|
|
/** @noinspection UnusedFunctionResultInspection */ |
38
|
|
|
$this->document->appendChild($domNode); |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
return; |
42
|
|
|
} |
43
|
|
|
|
44
|
3 |
|
if ($element !== null) { |
45
|
|
|
$this->loadXml($element); |
46
|
|
|
} |
47
|
3 |
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @param string $name |
51
|
|
|
* @param array $arguments |
52
|
|
|
* |
53
|
|
|
* @throws \BadMethodCallException |
54
|
|
|
* @throws \RuntimeException |
55
|
|
|
* |
56
|
|
|
* @return XmlDomParser |
57
|
|
|
*/ |
58
|
3 |
|
public static function __callStatic($name, $arguments) |
59
|
|
|
{ |
60
|
3 |
|
$arguments0 = $arguments[0] ?? ''; |
61
|
|
|
|
62
|
3 |
|
$arguments1 = $arguments[1] ?? null; |
63
|
|
|
|
64
|
3 |
|
if ($name === 'str_get_xml') { |
65
|
1 |
|
$parser = new static(); |
66
|
|
|
|
67
|
1 |
|
return $parser->loadXml($arguments0, $arguments1); |
68
|
|
|
} |
69
|
|
|
|
70
|
2 |
|
if ($name === 'file_get_xml') { |
71
|
2 |
|
$parser = new static(); |
72
|
|
|
|
73
|
2 |
|
return $parser->loadXmlFile($arguments0, $arguments1); |
|
|
|
|
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
throw new \BadMethodCallException('Method does not exist'); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** @noinspection MagicMethodsValidityInspection */ |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @param string $name |
83
|
|
|
* |
84
|
|
|
* @return string|null |
85
|
|
|
*/ |
86
|
|
|
public function __get($name) |
87
|
|
|
{ |
88
|
|
|
$name = \strtolower($name); |
89
|
|
|
|
90
|
|
|
if ($name === 'plaintext') { |
91
|
|
|
return $this->text(); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
return null; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @return string |
99
|
|
|
*/ |
100
|
2 |
|
public function __toString() |
101
|
|
|
{ |
102
|
2 |
|
return $this->xml(false, false, true, 0); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Create DOMDocument from XML. |
107
|
|
|
* |
108
|
|
|
* @param string $xml |
109
|
|
|
* @param int|null $libXMLExtraOptions |
110
|
|
|
* |
111
|
|
|
* @return \DOMDocument |
112
|
|
|
*/ |
113
|
3 |
|
protected function createDOMDocument(string $xml, $libXMLExtraOptions = null): \DOMDocument |
114
|
|
|
{ |
115
|
|
|
// set error level |
116
|
3 |
|
$internalErrors = \libxml_use_internal_errors(true); |
117
|
3 |
|
if (\PHP_VERSION_ID < 80000) { |
118
|
3 |
|
$disableEntityLoader = \libxml_disable_entity_loader(true); |
119
|
|
|
} |
120
|
3 |
|
\libxml_clear_errors(); |
121
|
|
|
|
122
|
3 |
|
$optionsXml = \LIBXML_DTDLOAD | \LIBXML_DTDATTR | \LIBXML_NONET; |
123
|
|
|
|
124
|
3 |
|
if (\defined('LIBXML_BIGLINES')) { |
125
|
3 |
|
$optionsXml |= \LIBXML_BIGLINES; |
126
|
|
|
} |
127
|
|
|
|
128
|
3 |
|
if (\defined('LIBXML_COMPACT')) { |
129
|
3 |
|
$optionsXml |= \LIBXML_COMPACT; |
130
|
|
|
} |
131
|
|
|
|
132
|
3 |
|
if ($libXMLExtraOptions !== null) { |
133
|
|
|
$optionsXml |= $libXMLExtraOptions; |
134
|
|
|
} |
135
|
|
|
|
136
|
3 |
|
$xml = self::replaceToPreserveHtmlEntities($xml); |
137
|
|
|
|
138
|
3 |
|
$documentFound = false; |
139
|
3 |
|
$sxe = \simplexml_load_string($xml, \SimpleXMLElement::class, $optionsXml); |
140
|
3 |
|
if ($sxe !== false && \count(\libxml_get_errors()) === 0) { |
141
|
3 |
|
$domElementTmp = \dom_import_simplexml($sxe); |
142
|
|
|
if ( |
143
|
3 |
|
$domElementTmp |
144
|
|
|
&& |
145
|
3 |
|
$domElementTmp->ownerDocument |
146
|
|
|
) { |
147
|
3 |
|
$documentFound = true; |
148
|
3 |
|
$this->document = $domElementTmp->ownerDocument; |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
3 |
|
if ($documentFound === false) { |
153
|
|
|
|
154
|
|
|
// UTF-8 hack: http://php.net/manual/en/domdocument.loadhtml.php#95251 |
155
|
|
|
$xmlHackUsed = false; |
156
|
|
|
/** @noinspection StringFragmentMisplacedInspection */ |
157
|
|
|
if (\stripos('<?xml', $xml) !== 0) { |
158
|
|
|
$xmlHackUsed = true; |
159
|
|
|
$xml = '<?xml encoding="' . $this->getEncoding() . '" ?>' . $xml; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$this->document->loadXML($xml, $optionsXml); |
163
|
|
|
|
164
|
|
|
// remove the "xml-encoding" hack |
165
|
|
|
if ($xmlHackUsed) { |
166
|
|
|
foreach ($this->document->childNodes as $child) { |
167
|
|
|
if ($child->nodeType === \XML_PI_NODE) { |
168
|
|
|
/** @noinspection UnusedFunctionResultInspection */ |
169
|
|
|
$this->document->removeChild($child); |
170
|
|
|
|
171
|
|
|
break; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// set encoding |
178
|
3 |
|
$this->document->encoding = $this->getEncoding(); |
179
|
|
|
|
180
|
|
|
// restore lib-xml settings |
181
|
3 |
|
\libxml_clear_errors(); |
182
|
3 |
|
\libxml_use_internal_errors($internalErrors); |
183
|
3 |
|
if (\PHP_VERSION_ID < 80000) { |
184
|
3 |
|
\libxml_disable_entity_loader($disableEntityLoader); |
|
|
|
|
185
|
|
|
} |
186
|
|
|
|
187
|
3 |
|
return $this->document; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Find list of nodes with a CSS selector. |
192
|
|
|
* |
193
|
|
|
* @param string $selector |
194
|
|
|
* @param int|null $idx |
195
|
|
|
* |
196
|
|
|
* @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
197
|
|
|
*/ |
198
|
1 |
|
public function find(string $selector, $idx = null) |
199
|
|
|
{ |
200
|
1 |
|
$xPathQuery = SelectorConverter::toXPath($selector); |
201
|
|
|
|
202
|
1 |
|
$xPath = new \DOMXPath($this->document); |
203
|
1 |
|
$nodesList = $xPath->query($xPathQuery); |
204
|
1 |
|
$elements = new SimpleXmlDomNode(); |
205
|
|
|
|
206
|
1 |
|
if ($nodesList) { |
207
|
1 |
|
foreach ($nodesList as $node) { |
208
|
1 |
|
$elements[] = new SimpleXmlDom($node); |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// return all elements |
213
|
1 |
|
if ($idx === null) { |
214
|
1 |
|
if (\count($elements) === 0) { |
215
|
1 |
|
return new SimpleXmlDomNodeBlank(); |
216
|
|
|
} |
217
|
|
|
|
218
|
1 |
|
return $elements; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
// handle negative values |
222
|
1 |
|
if ($idx < 0) { |
223
|
|
|
$idx = \count($elements) + $idx; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
// return one element |
227
|
1 |
|
return $elements[$idx] ?? new SimpleXmlDomBlank(); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Find nodes with a CSS selector. |
232
|
|
|
* |
233
|
|
|
* @param string $selector |
234
|
|
|
* |
235
|
|
|
* @return SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
236
|
|
|
*/ |
237
|
1 |
|
public function findMulti(string $selector): SimpleXmlDomNodeInterface |
238
|
|
|
{ |
239
|
1 |
|
return $this->find($selector, null); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Find nodes with a CSS selector or false, if no element is found. |
244
|
|
|
* |
245
|
|
|
* @param string $selector |
246
|
|
|
* |
247
|
|
|
* @return false|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
248
|
|
|
*/ |
249
|
1 |
|
public function findMultiOrFalse(string $selector) |
250
|
|
|
{ |
251
|
1 |
|
$return = $this->find($selector, null); |
252
|
|
|
|
253
|
1 |
|
if ($return instanceof SimpleXmlDomNodeBlank) { |
254
|
1 |
|
return false; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return $return; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Find one node with a CSS selector. |
262
|
|
|
* |
263
|
|
|
* @param string $selector |
264
|
|
|
* |
265
|
|
|
* @return SimpleXmlDomInterface |
266
|
|
|
*/ |
267
|
1 |
|
public function findOne(string $selector): SimpleXmlDomInterface |
268
|
|
|
{ |
269
|
1 |
|
return $this->find($selector, 0); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Find one node with a CSS selector or false, if no element is found. |
274
|
|
|
* |
275
|
|
|
* @param string $selector |
276
|
|
|
* |
277
|
|
|
* @return false|SimpleXmlDomInterface |
278
|
|
|
*/ |
279
|
1 |
|
public function findOneOrFalse(string $selector) |
280
|
|
|
{ |
281
|
1 |
|
$return = $this->find($selector, 0); |
282
|
|
|
|
283
|
1 |
|
if ($return instanceof SimpleXmlDomBlank) { |
284
|
1 |
|
return false; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return $return; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* @param string $content |
292
|
|
|
* @param bool $multiDecodeNewHtmlEntity |
293
|
|
|
* |
294
|
|
|
* @return string |
295
|
|
|
*/ |
296
|
|
|
public function fixHtmlOutput(string $content, bool $multiDecodeNewHtmlEntity = false): string |
297
|
|
|
{ |
298
|
|
|
$content = $this->decodeHtmlEntity($content, $multiDecodeNewHtmlEntity); |
299
|
|
|
|
300
|
|
|
return self::putReplacedBackToPreserveHtmlEntities($content); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Return elements by ".class". |
305
|
|
|
* |
306
|
|
|
* @param string $class |
307
|
|
|
* |
308
|
|
|
* @return SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
309
|
|
|
*/ |
310
|
|
|
public function getElementByClass(string $class): SimpleXmlDomNodeInterface |
311
|
|
|
{ |
312
|
|
|
return $this->findMulti(".${class}"); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Return element by #id. |
317
|
|
|
* |
318
|
|
|
* @param string $id |
319
|
|
|
* |
320
|
|
|
* @return SimpleXmlDomInterface |
321
|
|
|
*/ |
322
|
|
|
public function getElementById(string $id): SimpleXmlDomInterface |
323
|
|
|
{ |
324
|
|
|
return $this->findOne("#${id}"); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Return element by tag name. |
329
|
|
|
* |
330
|
|
|
* @param string $name |
331
|
|
|
* |
332
|
|
|
* @return SimpleXmlDomInterface |
333
|
|
|
*/ |
334
|
|
|
public function getElementByTagName(string $name): SimpleXmlDomInterface |
335
|
|
|
{ |
336
|
|
|
$node = $this->document->getElementsByTagName($name)->item(0); |
337
|
|
|
|
338
|
|
|
if ($node === null) { |
339
|
|
|
return new SimpleXmlDomBlank(); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
return new SimpleXmlDom($node); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Returns elements by "#id". |
347
|
|
|
* |
348
|
|
|
* @param string $id |
349
|
|
|
* @param int|null $idx |
350
|
|
|
* |
351
|
|
|
* @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
352
|
|
|
*/ |
353
|
|
|
public function getElementsById(string $id, $idx = null) |
354
|
|
|
{ |
355
|
|
|
return $this->find("#${id}", $idx); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Returns elements by tag name. |
360
|
|
|
* |
361
|
|
|
* @param string $name |
362
|
|
|
* @param int|null $idx |
363
|
|
|
* |
364
|
|
|
* @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
365
|
|
|
*/ |
366
|
|
View Code Duplication |
public function getElementsByTagName(string $name, $idx = null) |
|
|
|
|
367
|
|
|
{ |
368
|
|
|
$nodesList = $this->document->getElementsByTagName($name); |
369
|
|
|
|
370
|
|
|
$elements = new SimpleXmlDomNode(); |
371
|
|
|
|
372
|
|
|
foreach ($nodesList as $node) { |
373
|
|
|
$elements[] = new SimpleXmlDom($node); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
// return all elements |
377
|
|
|
if ($idx === null) { |
378
|
|
|
if (\count($elements) === 0) { |
379
|
|
|
return new SimpleXmlDomNodeBlank(); |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
return $elements; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
// handle negative values |
386
|
|
|
if ($idx < 0) { |
387
|
|
|
$idx = \count($elements) + $idx; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
// return one element |
391
|
|
|
return $elements[$idx] ?? new SimpleXmlDomNodeBlank(); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* Get dom node's outer html. |
396
|
|
|
* |
397
|
|
|
* @param bool $multiDecodeNewHtmlEntity |
398
|
|
|
* |
399
|
|
|
* @return string |
400
|
|
|
*/ |
401
|
|
|
public function html(bool $multiDecodeNewHtmlEntity = false): string |
402
|
|
|
{ |
403
|
|
|
if (static::$callback !== null) { |
404
|
|
|
\call_user_func(static::$callback, [$this]); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
$content = $this->document->saveHTML(); |
408
|
|
|
|
409
|
|
|
if ($content === false) { |
410
|
|
|
return ''; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
return $this->fixHtmlOutput($content, $multiDecodeNewHtmlEntity); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Load HTML from string. |
418
|
|
|
* |
419
|
|
|
* @param string $html |
420
|
|
|
* @param int|null $libXMLExtraOptions |
421
|
|
|
* |
422
|
|
|
* @return self |
423
|
|
|
*/ |
424
|
|
|
public function loadHtml(string $html, $libXMLExtraOptions = null): DomParserInterface |
425
|
|
|
{ |
426
|
|
|
$this->document = $this->createDOMDocument($html, $libXMLExtraOptions); |
427
|
|
|
|
428
|
|
|
return $this; |
|
|
|
|
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Load HTML from file. |
433
|
|
|
* |
434
|
|
|
* @param string $filePath |
435
|
|
|
* @param int|null $libXMLExtraOptions |
436
|
|
|
* |
437
|
|
|
* @throws \RuntimeException |
438
|
|
|
* |
439
|
|
|
* @return XmlDomParser |
440
|
|
|
*/ |
441
|
|
View Code Duplication |
public function loadHtmlFile(string $filePath, $libXMLExtraOptions = null): DomParserInterface |
|
|
|
|
442
|
|
|
{ |
443
|
|
|
if ( |
444
|
|
|
!\preg_match("/^https?:\/\//i", $filePath) |
445
|
|
|
&& |
446
|
|
|
!\file_exists($filePath) |
447
|
|
|
) { |
448
|
|
|
throw new \RuntimeException("File ${filePath} not found"); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
try { |
452
|
|
|
if (\class_exists('\voku\helper\UTF8')) { |
453
|
|
|
/** @noinspection PhpUndefinedClassInspection */ |
454
|
|
|
$html = UTF8::file_get_contents($filePath); |
455
|
|
|
} else { |
456
|
|
|
$html = \file_get_contents($filePath); |
457
|
|
|
} |
458
|
|
|
} catch (\Exception $e) { |
459
|
|
|
throw new \RuntimeException("Could not load file ${filePath}"); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
if ($html === false) { |
463
|
|
|
throw new \RuntimeException("Could not load file ${filePath}"); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
return $this->loadHtml($html, $libXMLExtraOptions); |
|
|
|
|
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* @param string $selector |
471
|
|
|
* @param int $idx |
472
|
|
|
* |
473
|
|
|
* @return SimpleXmlDomInterface|SimpleXmlDomInterface[]|SimpleXmlDomNodeInterface<SimpleXmlDomInterface> |
|
|
|
|
474
|
|
|
*/ |
475
|
|
|
public function __invoke($selector, $idx = null) |
476
|
|
|
{ |
477
|
|
|
return $this->find($selector, $idx); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* Load XML from string. |
482
|
|
|
* |
483
|
|
|
* @param string $xml |
484
|
|
|
* @param int|null $libXMLExtraOptions |
485
|
|
|
* |
486
|
|
|
* @return XmlDomParser |
487
|
|
|
*/ |
488
|
3 |
|
public function loadXml(string $xml, $libXMLExtraOptions = null): self |
489
|
|
|
{ |
490
|
3 |
|
$this->document = $this->createDOMDocument($xml, $libXMLExtraOptions); |
491
|
|
|
|
492
|
3 |
|
return $this; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Load XML from file. |
497
|
|
|
* |
498
|
|
|
* @param string $filePath |
499
|
|
|
* @param int|null $libXMLExtraOptions |
500
|
|
|
* |
501
|
|
|
* @throws \RuntimeException |
502
|
|
|
* |
503
|
|
|
* @return XmlDomParser |
504
|
|
|
*/ |
505
|
2 |
View Code Duplication |
public function loadXmlFile(string $filePath, $libXMLExtraOptions = null): self |
|
|
|
|
506
|
|
|
{ |
507
|
|
|
if ( |
508
|
2 |
|
!\preg_match("/^https?:\/\//i", $filePath) |
509
|
|
|
&& |
510
|
2 |
|
!\file_exists($filePath) |
511
|
|
|
) { |
512
|
|
|
throw new \RuntimeException("File ${filePath} not found"); |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
try { |
516
|
2 |
|
if (\class_exists('\voku\helper\UTF8')) { |
517
|
|
|
/** @noinspection PhpUndefinedClassInspection */ |
518
|
|
|
$xml = UTF8::file_get_contents($filePath); |
519
|
|
|
} else { |
520
|
2 |
|
$xml = \file_get_contents($filePath); |
521
|
|
|
} |
522
|
|
|
} catch (\Exception $e) { |
523
|
|
|
throw new \RuntimeException("Could not load file ${filePath}"); |
524
|
|
|
} |
525
|
|
|
|
526
|
2 |
|
if ($xml === false) { |
527
|
|
|
throw new \RuntimeException("Could not load file ${filePath}"); |
528
|
|
|
} |
529
|
|
|
|
530
|
2 |
|
return $this->loadXml($xml, $libXMLExtraOptions); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* @param callable $callback |
535
|
|
|
* @param \DOMNode|null $domNode |
536
|
|
|
* |
537
|
|
|
* @return void |
538
|
|
|
*/ |
539
|
1 |
|
public function replaceTextWithCallback($callback, \DOMNode $domNode = null) |
540
|
|
|
{ |
541
|
1 |
|
if ($domNode === null) { |
542
|
1 |
|
$domNode = $this->document; |
543
|
|
|
} |
544
|
|
|
|
545
|
1 |
|
if ($domNode->hasChildNodes()) { |
546
|
1 |
|
$children = []; |
547
|
|
|
|
548
|
|
|
// since looping through a DOM being modified is a bad idea we prepare an array: |
549
|
1 |
|
foreach ($domNode->childNodes as $child) { |
550
|
1 |
|
$children[] = $child; |
551
|
|
|
} |
552
|
|
|
|
553
|
1 |
|
foreach ($children as $child) { |
554
|
1 |
|
if ($child->nodeType === \XML_TEXT_NODE) { |
555
|
|
|
/** @noinspection PhpSillyAssignmentInspection */ |
556
|
|
|
/** @var \DOMText $child */ |
557
|
1 |
|
$child = $child; |
|
|
|
|
558
|
|
|
|
559
|
1 |
|
$oldText = self::putReplacedBackToPreserveHtmlEntities($child->wholeText); |
560
|
1 |
|
$newText = $callback($oldText); |
561
|
1 |
|
if ($domNode->ownerDocument) { |
562
|
1 |
|
$newTextNode = $domNode->ownerDocument->createTextNode(self::replaceToPreserveHtmlEntities($newText)); |
563
|
1 |
|
$domNode->replaceChild($newTextNode, $child); |
564
|
|
|
} |
565
|
|
|
} else { |
566
|
1 |
|
$this->replaceTextWithCallback($callback, $child); |
567
|
|
|
} |
568
|
|
|
} |
569
|
|
|
} |
570
|
1 |
|
} |
571
|
|
|
} |
572
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.