Completed
Push — master ( 77e4a4...d10009 )
by Gilles
03:09
created

InnerNode::insertAfter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

402
        return $this->getChild(/** @scrutinizer ignore-type */ $key);
Loading history...
403
    }
404
405
    /**
406
     * Checks if the given node id is a descendant of the
407
     * current node.
408
     *
409
     * @param int $id
410
     *
411
     * @return bool
412
     */
413 405
    public function isDescendant(int $id): bool
414
    {
415 405
        if ($this->isChild($id)) {
416 6
            return true;
417
        }
418
419 405
        foreach ($this->children as $childId => $child) {
420
            /** @var InnerNode $node */
421 18
            $node = $child['node'];
422 18
            if ($node instanceof InnerNode
423 18
              && $node->hasChildren()
424 18
              && $node->isDescendant($id)
425
            ) {
426 13
                return true;
427
            }
428
        }
429
430 405
        return false;
431
    }
432
433
    /**
434
     * Sets the parent node.
435
     * @param InnerNode $parent
436
     * @return AbstractNode
437
     * @throws ChildNotFoundException
438
     * @throws CircularException
439
     */
440 405
    public function setParent(InnerNode $parent): AbstractNode
441
    {
442
        // check integrity
443 405
        if ($this->isDescendant($parent->id())) {
444 3
            throw new CircularException('Can not add descendant "'
445 3
              . $parent->id() . '" as my parent.');
446
        }
447
448
        // clear cache
449 405
        $this->clear();
450
451 405
        return parent::setParent($parent);
452
    }
453
}
454