Passed
Push — master ( 12b94f...668c77 )
by Gilles
03:31
created

InnerNode::countChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $combination can also be of type false. However, the property $children is declared as type array. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $combination can also be of type false. However, the property $children is declared as type array. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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