Issues (16)

src/PHPHtmlParser/Dom/Node/InnerNode.php (2 issues)

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