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

InnerNode::addChild()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 72
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 10.017

Importance

Changes 0
Metric Value
cc 10
eloc 37
c 0
b 0
f 0
nc 20
nop 2
dl 0
loc 72
ccs 34
cts 36
cp 0.9444
crap 10.017
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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