1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace PHPHtmlParser\Dom; |
6
|
|
|
|
7
|
|
|
use PHPHtmlParser\Exceptions\ChildNotFoundException; |
8
|
|
|
use PHPHtmlParser\Exceptions\CircularException; |
9
|
|
|
use PHPHtmlParser\Exceptions\LogicalException; |
10
|
|
|
use stringEncode\Encode; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Inner node of the html tree, might have children. |
14
|
|
|
* |
15
|
|
|
* @package PHPHtmlParser\Dom |
16
|
|
|
*/ |
17
|
|
|
abstract class InnerNode extends ArrayNode |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* An array of all the children. |
22
|
|
|
* |
23
|
|
|
* @var array |
24
|
|
|
*/ |
25
|
|
|
protected $children = []; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Sets the encoding class to this node and propagates it |
29
|
|
|
* to all its children. |
30
|
|
|
* |
31
|
|
|
* @param Encode $encode |
32
|
|
|
* |
33
|
|
|
* @return void |
34
|
|
|
*/ |
35
|
240 |
|
public function propagateEncoding(Encode $encode): void |
36
|
|
|
{ |
37
|
240 |
|
$this->encode = $encode; |
38
|
240 |
|
$this->tag->setEncoding($encode); |
39
|
|
|
// check children |
40
|
240 |
|
foreach ($this->children as $child) { |
41
|
|
|
/** @var AbstractNode $node */ |
42
|
240 |
|
$node = $child['node']; |
43
|
240 |
|
$node->propagateEncoding($encode); |
44
|
|
|
} |
45
|
240 |
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Checks if this node has children. |
49
|
|
|
* |
50
|
|
|
* @return bool |
51
|
|
|
*/ |
52
|
447 |
|
public function hasChildren(): bool |
53
|
|
|
{ |
54
|
447 |
|
return !empty($this->children); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Returns the child by id. |
59
|
|
|
* |
60
|
|
|
* @param int $id |
61
|
|
|
* |
62
|
|
|
* @return AbstractNode |
63
|
|
|
* @throws ChildNotFoundException |
64
|
|
|
*/ |
65
|
390 |
|
public function getChild(int $id): AbstractNode |
66
|
|
|
{ |
67
|
390 |
|
if (!isset($this->children[$id])) { |
68
|
3 |
|
throw new ChildNotFoundException("Child '$id' not found in this node."); |
69
|
|
|
} |
70
|
|
|
|
71
|
387 |
|
return $this->children[$id]['node']; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Returns a new array of child nodes |
76
|
|
|
* |
77
|
|
|
* @return array |
78
|
|
|
*/ |
79
|
15 |
|
public function getChildren(): array |
80
|
|
|
{ |
81
|
15 |
|
$nodes = []; |
82
|
15 |
|
$childrenIds = []; |
83
|
|
|
try { |
84
|
15 |
|
$child = $this->firstChild(); |
85
|
|
|
do { |
86
|
12 |
|
$nodes[] = $child; |
87
|
12 |
|
$childrenIds[] = $child->id; |
88
|
12 |
|
$child = $this->nextChild($child->id()); |
89
|
12 |
|
if (in_array($child->id, $childrenIds, true)) { |
90
|
|
|
throw new CircularException('Circular sibling referance found. Child with id '.$child->id().' found twice.'); |
91
|
|
|
} |
92
|
12 |
|
} while (true); |
93
|
15 |
|
} catch (ChildNotFoundException $e) { |
94
|
|
|
// we are done looking for children |
95
|
15 |
|
unset($e); |
96
|
|
|
} |
97
|
|
|
|
98
|
15 |
|
return $nodes; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Counts children |
103
|
|
|
* |
104
|
|
|
* @return int |
105
|
|
|
*/ |
106
|
6 |
|
public function countChildren(): int |
107
|
|
|
{ |
108
|
6 |
|
return count($this->children); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Adds a child node to this node and returns the id of the child for this |
113
|
|
|
* parent. |
114
|
|
|
* @param AbstractNode $child |
115
|
|
|
* @param int $before |
116
|
|
|
* @return bool |
117
|
|
|
* @throws ChildNotFoundException |
118
|
|
|
* @throws CircularException |
119
|
|
|
*/ |
120
|
441 |
|
public function addChild(AbstractNode $child, int $before = -1): bool |
121
|
|
|
{ |
122
|
441 |
|
$key = null; |
123
|
|
|
|
124
|
|
|
// check integrity |
125
|
441 |
|
if ($this->isAncestor($child->id())) { |
126
|
3 |
|
throw new CircularException('Can not add child. It is my ancestor.'); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
// check if child is itself |
130
|
441 |
|
if ($child->id() == $this->id) { |
131
|
3 |
|
throw new CircularException('Can not set itself as a child.'); |
132
|
|
|
} |
133
|
|
|
|
134
|
438 |
|
$next = null; |
135
|
|
|
|
136
|
438 |
|
if ($this->hasChildren()) { |
137
|
423 |
|
if (isset($this->children[$child->id()])) { |
138
|
|
|
// we already have this child |
139
|
411 |
|
return false; |
140
|
|
|
} |
141
|
|
|
|
142
|
330 |
|
if ($before >= 0) { |
143
|
9 |
|
if (!isset($this->children[$before])) { |
144
|
|
|
return false; |
145
|
|
|
} |
146
|
|
|
|
147
|
9 |
|
$key = $this->children[$before]['prev']; |
148
|
|
|
|
149
|
9 |
|
if ($key) { |
150
|
6 |
|
$this->children[$key]['next'] = $child->id(); |
151
|
|
|
} |
152
|
|
|
|
153
|
9 |
|
$this->children[$before]['prev'] = $child->id(); |
154
|
9 |
|
$next = $before; |
155
|
|
|
} else { |
156
|
330 |
|
$sibling = $this->lastChild(); |
157
|
330 |
|
$key = $sibling->id(); |
158
|
|
|
|
159
|
330 |
|
$this->children[$key]['next'] = $child->id(); |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
438 |
|
$keys = array_keys($this->children); |
164
|
|
|
|
165
|
|
|
$insert = [ |
166
|
438 |
|
'node' => $child, |
167
|
438 |
|
'next' => $next, |
168
|
438 |
|
'prev' => $key, |
169
|
|
|
]; |
170
|
|
|
|
171
|
438 |
|
$index = $key ? (int) (array_search($key, $keys, true) + 1) : 0; |
172
|
438 |
|
array_splice($keys, $index, 0, (string) $child->id()); |
173
|
|
|
|
174
|
438 |
|
$children = array_values($this->children); |
175
|
438 |
|
array_splice($children, $index, 0, [$insert]); |
176
|
|
|
|
177
|
|
|
// add the child |
178
|
438 |
|
$combination = array_combine($keys, $children); |
179
|
438 |
|
$this->children = $combination; |
|
|
|
|
180
|
|
|
|
181
|
|
|
// tell child I am the new parent |
182
|
438 |
|
$child->setParent($this); |
183
|
|
|
|
184
|
|
|
//clear any cache |
185
|
438 |
|
$this->clear(); |
186
|
|
|
|
187
|
438 |
|
return true; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Insert element before child with provided id |
192
|
|
|
* @param AbstractNode $child |
193
|
|
|
* @param int $id |
194
|
|
|
* @return bool |
195
|
|
|
* @throws ChildNotFoundException |
196
|
|
|
* @throws CircularException |
197
|
|
|
*/ |
198
|
6 |
|
public function insertBefore(AbstractNode $child, int $id): bool |
199
|
|
|
{ |
200
|
6 |
|
return $this->addChild($child, $id); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Insert element before after with provided id |
205
|
|
|
* @param AbstractNode $child |
206
|
|
|
* @param int $id |
207
|
|
|
* @return bool |
208
|
|
|
* @throws ChildNotFoundException |
209
|
|
|
* @throws CircularException |
210
|
|
|
*/ |
211
|
6 |
|
public function insertAfter(AbstractNode $child, int $id): bool |
212
|
|
|
{ |
213
|
6 |
|
if (!isset($this->children[$id])) { |
214
|
|
|
return false; |
215
|
|
|
} |
216
|
|
|
|
217
|
6 |
|
if (isset($this->children[$id]['next']) && is_int($this->children[$id]['next'])) { |
218
|
3 |
|
return $this->addChild($child, (int) $this->children[$id]['next']); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
// clear cache |
222
|
3 |
|
$this->clear(); |
223
|
|
|
|
224
|
3 |
|
return $this->addChild($child); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Removes the child by id. |
229
|
|
|
* |
230
|
|
|
* @param int $id |
231
|
|
|
* |
232
|
|
|
* @return InnerNode |
233
|
|
|
* @chainable |
234
|
|
|
*/ |
235
|
21 |
|
public function removeChild(int $id): InnerNode |
236
|
|
|
{ |
237
|
21 |
|
if (!isset($this->children[$id])) { |
238
|
3 |
|
return $this; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
// handle moving next and previous assignments. |
242
|
18 |
|
$next = $this->children[$id]['next']; |
243
|
18 |
|
$prev = $this->children[$id]['prev']; |
244
|
18 |
|
if (!is_null($next)) { |
245
|
9 |
|
$this->children[$next]['prev'] = $prev; |
246
|
|
|
} |
247
|
18 |
|
if (!is_null($prev)) { |
248
|
9 |
|
$this->children[$prev]['next'] = $next; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
// remove the child |
252
|
18 |
|
unset($this->children[$id]); |
253
|
|
|
|
254
|
|
|
//clear any cache |
255
|
18 |
|
$this->clear(); |
256
|
|
|
|
257
|
18 |
|
return $this; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Check if has next Child |
262
|
|
|
* |
263
|
|
|
* @param int $id |
264
|
|
|
* |
265
|
|
|
* @return mixed |
266
|
|
|
* @throws ChildNotFoundException |
267
|
|
|
*/ |
268
|
6 |
|
public function hasNextChild(int $id) |
269
|
|
|
{ |
270
|
6 |
|
$child = $this->getChild($id); |
271
|
3 |
|
return $this->children[$child->id()]['next']; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Attempts to get the next child. |
276
|
|
|
* |
277
|
|
|
* @param int $id |
278
|
|
|
* |
279
|
|
|
* @return AbstractNode |
280
|
|
|
* @throws ChildNotFoundException |
281
|
|
|
* @uses $this->getChild() |
282
|
|
|
*/ |
283
|
348 |
|
public function nextChild(int $id): AbstractNode |
284
|
|
|
{ |
285
|
348 |
|
$child = $this->getChild($id); |
286
|
348 |
|
$next = $this->children[$child->id()]['next']; |
287
|
348 |
|
if (is_null($next) || !is_int($next)) { |
288
|
327 |
|
throw new ChildNotFoundException("Child '$id' next sibling not found in this node."); |
289
|
|
|
} |
290
|
|
|
|
291
|
294 |
|
return $this->getChild($next); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Attempts to get the previous child. |
296
|
|
|
* |
297
|
|
|
* @param int $id |
298
|
|
|
* |
299
|
|
|
* @return AbstractNode |
300
|
|
|
* @throws ChildNotFoundException |
301
|
|
|
* @uses $this->getChild() |
302
|
|
|
*/ |
303
|
12 |
|
public function previousChild(int $id): AbstractNode |
304
|
|
|
{ |
305
|
12 |
|
$child = $this->getchild($id); |
306
|
12 |
|
$next = $this->children[$child->id()]['prev']; |
307
|
12 |
|
if (is_null($next) || !is_int($next)) { |
308
|
3 |
|
throw new ChildNotFoundException("Child '$id' previous not found in this node."); |
309
|
|
|
} |
310
|
|
|
|
311
|
9 |
|
return $this->getChild($next); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Checks if the given node id is a child of the |
316
|
|
|
* current node. |
317
|
|
|
* |
318
|
|
|
* @param int $id |
319
|
|
|
* |
320
|
|
|
* @return bool |
321
|
|
|
*/ |
322
|
423 |
|
public function isChild(int $id): bool |
323
|
|
|
{ |
324
|
423 |
|
foreach(array_keys($this->children) as $childId) { |
325
|
36 |
|
if ($id == $childId) { |
326
|
24 |
|
return true; |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
423 |
|
return false; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Removes the child with id $childId and replace it with the new child |
335
|
|
|
* $newChild. |
336
|
|
|
* |
337
|
|
|
* @param int $childId |
338
|
|
|
* @param AbstractNode $newChild |
339
|
|
|
* |
340
|
|
|
* @return void |
341
|
|
|
*/ |
342
|
6 |
|
public function replaceChild(int $childId, AbstractNode $newChild): void |
343
|
|
|
{ |
344
|
6 |
|
$oldChild = $this->children[$childId]; |
345
|
|
|
|
346
|
6 |
|
$newChild->prev = (int) $oldChild['prev']; |
347
|
6 |
|
$newChild->next = (int) $oldChild['next']; |
348
|
|
|
|
349
|
6 |
|
$keys = array_keys($this->children); |
350
|
6 |
|
$index = array_search($childId, $keys, true); |
351
|
6 |
|
$keys[$index] = $newChild->id(); |
352
|
6 |
|
$combination = array_combine($keys, $this->children); |
353
|
6 |
|
$this->children = $combination; |
|
|
|
|
354
|
6 |
|
$this->children[$newChild->id()] = [ |
355
|
6 |
|
'prev' => $oldChild['prev'], |
356
|
6 |
|
'node' => $newChild, |
357
|
6 |
|
'next' => $oldChild['next'] |
358
|
|
|
]; |
359
|
|
|
|
360
|
|
|
// change previous child id to new child |
361
|
6 |
|
if ($oldChild['prev'] && isset($this->children[$newChild->prev])) { |
362
|
|
|
$this->children[$oldChild['prev']]['next'] = $newChild->id(); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
// change next child id to new child |
366
|
6 |
|
if ($oldChild['next'] && isset($this->children[$newChild->next])) { |
367
|
3 |
|
$this->children[$oldChild['next']]['prev'] = $newChild->id(); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
// remove old child |
371
|
6 |
|
unset($this->children[$childId]); |
372
|
|
|
|
373
|
|
|
// clean out cache |
374
|
6 |
|
$this->clear(); |
375
|
6 |
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Shortcut to return the first child. |
379
|
|
|
* |
380
|
|
|
* @return AbstractNode |
381
|
|
|
* @throws ChildNotFoundException |
382
|
|
|
* @uses $this->getChild() |
383
|
|
|
*/ |
384
|
339 |
|
public function firstChild(): AbstractNode |
385
|
|
|
{ |
386
|
339 |
|
if (count($this->children) == 0) { |
387
|
|
|
// no children |
388
|
3 |
|
throw new ChildNotFoundException("No children found in node."); |
389
|
|
|
} |
390
|
|
|
|
391
|
339 |
|
reset($this->children); |
392
|
339 |
|
$key = (int)key($this->children); |
393
|
|
|
|
394
|
339 |
|
return $this->getChild($key); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Attempts to get the last child. |
399
|
|
|
* |
400
|
|
|
* @return AbstractNode |
401
|
|
|
* @throws ChildNotFoundException |
402
|
|
|
* @uses $this->getChild() |
403
|
|
|
*/ |
404
|
330 |
|
public function lastChild(): AbstractNode |
405
|
|
|
{ |
406
|
330 |
|
if (count($this->children) == 0) { |
407
|
|
|
// no children |
408
|
|
|
throw new ChildNotFoundException("No children found in node."); |
409
|
|
|
} |
410
|
|
|
|
411
|
330 |
|
end($this->children); |
412
|
330 |
|
$key = key($this->children); |
413
|
|
|
|
414
|
330 |
|
if (!is_int($key)) { |
415
|
|
|
throw new LogicalException("Children array contain child with a key that is not an int."); |
416
|
|
|
} |
417
|
|
|
|
418
|
330 |
|
return $this->getChild($key); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Checks if the given node id is a descendant of the |
423
|
|
|
* current node. |
424
|
|
|
* |
425
|
|
|
* @param int $id |
426
|
|
|
* |
427
|
|
|
* @return bool |
428
|
|
|
*/ |
429
|
423 |
|
public function isDescendant(int $id): bool |
430
|
|
|
{ |
431
|
423 |
|
if ($this->isChild($id)) { |
432
|
6 |
|
return true; |
433
|
|
|
} |
434
|
|
|
|
435
|
423 |
|
foreach ($this->children as $child) { |
436
|
|
|
/** @var InnerNode $node */ |
437
|
18 |
|
$node = $child['node']; |
438
|
18 |
|
if ($node instanceof InnerNode |
439
|
18 |
|
&& $node->hasChildren() |
440
|
18 |
|
&& $node->isDescendant($id) |
441
|
|
|
) { |
442
|
8 |
|
return true; |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
|
446
|
423 |
|
return false; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* Sets the parent node. |
451
|
|
|
* @param InnerNode $parent |
452
|
|
|
* @return AbstractNode |
453
|
|
|
* @throws ChildNotFoundException |
454
|
|
|
* @throws CircularException |
455
|
|
|
*/ |
456
|
423 |
|
public function setParent(InnerNode $parent): AbstractNode |
457
|
|
|
{ |
458
|
|
|
// check integrity |
459
|
423 |
|
if ($this->isDescendant($parent->id())) { |
460
|
3 |
|
throw new CircularException('Can not add descendant "' |
461
|
3 |
|
. $parent->id() . '" as my parent.'); |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
// clear cache |
465
|
423 |
|
$this->clear(); |
466
|
|
|
|
467
|
423 |
|
return parent::setParent($parent); |
468
|
|
|
} |
469
|
|
|
} |
470
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.