InnerNode::addChild()   B
last analyzed

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\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