Passed
Push — master ( 668c77...c11634 )
by Gilles
02:19
created

InnerNode::replaceChild()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.031

Importance

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