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

AbstractNode::__get()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.0155

Importance

Changes 0
Metric Value
cc 8
eloc 15
c 0
b 0
f 0
nc 8
nop 1
dl 0
loc 19
ccs 15
cts 16
cp 0.9375
crap 8.0155
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PHPHtmlParser\Dom\Node;
6
7
use PHPHtmlParser\Contracts\Selector\SelectorInterface;
8
use PHPHtmlParser\Dom\Tag;
9
use PHPHtmlParser\Exceptions\ChildNotFoundException;
10
use PHPHtmlParser\Exceptions\CircularException;
11
use PHPHtmlParser\Exceptions\ParentNotFoundException;
12
use PHPHtmlParser\Exceptions\Tag\AttributeNotFoundException;
13
use PHPHtmlParser\Finder;
14
use PHPHtmlParser\Selector\Selector;
15
use stringEncode\Encode;
16
17
/**
18
 * Dom node object.
19
 *
20
 * @property string    $outerhtml
21
 * @property string    $innerhtml
22
 * @property string    $text
23
 * @property int       $prev
24
 * @property int       $next
25
 * @property Tag       $tag
26
 * @property InnerNode $parent
27
 */
28
abstract class AbstractNode
29
{
30
    /**
31
     * Contains the tag name/type.
32
     *
33
     * @var ?Tag
34
     */
35
    protected $tag;
36
37
    /**
38
     * Contains a list of attributes on this tag.
39
     *
40
     * @var array
41
     */
42
    protected $attr = [];
43
44
    /**
45
     * Contains the parent Node.
46
     *
47
     * @var ?InnerNode
48
     */
49
    protected $parent;
50
51
    /**
52
     * The unique id of the class. Given by PHP.
53
     *
54
     * @var int
55
     */
56
    protected $id;
57
58
    /**
59
     * The encoding class used to encode strings.
60
     *
61
     * @var mixed
62
     */
63
    protected $encode;
64
65
    /**
66
     * An array of all the children.
67
     *
68
     * @var array
69
     */
70
    protected $children = [];
71
72
    /**
73
     * @var bool
74
     */
75
    protected $htmlSpecialCharsDecode = false;
76
    /**
77
     * @var int
78
     */
79
    private static $count = 0;
80
81
    /**
82
     * Creates a unique id for this node.
83
     */
84 558
    public function __construct()
85
    {
86 558
        $this->id = self::$count;
87 558
        ++self::$count;
88 558
    }
89
90
    /**
91
     * Attempts to clear out any object references.
92
     */
93 132
    public function __destruct()
94
    {
95 132
        $this->tag = null;
96 132
        $this->parent = null;
97 132
        $this->attr = [];
98 132
        $this->children = [];
99 132
    }
100
101
    /**
102
     * Magic get method for attributes and certain methods.
103
     *
104
     * @return mixed
105
     */
106 321
    public function __get(string $key)
107
    {
108
        // check attribute first
109 321
        if ($this->getAttribute($key) !== null) {
110 18
            return $this->getAttribute($key);
111
        }
112 318
        switch (\strtolower($key)) {
113 318
            case 'outerhtml':
114 39
                return $this->outerHtml();
115 309
            case 'innerhtml':
116 78
                return $this->innerHtml();
117 306
            case 'innertext':
118 3
                return $this->innerText();
0 ignored issues
show
Bug introduced by
The method innerText() does not exist on PHPHtmlParser\Dom\Node\AbstractNode. It seems like you code against a sub-type of PHPHtmlParser\Dom\Node\AbstractNode such as PHPHtmlParser\Dom\Node\HtmlNode. ( Ignorable by Annotation )

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

118
                return $this->/** @scrutinizer ignore-call */ innerText();
Loading history...
119 306
            case 'text':
120 51
                return $this->text();
121 291
            case 'tag':
122 291
                return $this->getTag();
123 3
            case 'parent':
124
                return $this->getParent();
125
        }
126 3
    }
127
128
    /**
129
     * Simply calls the outer text method.
130
     *
131
     * @return string
132
     */
133 9
    public function __toString()
134
    {
135 9
        return $this->outerHtml();
136
    }
137
138
    /**
139
     * @param bool $htmlSpecialCharsDecode
140
     */
141 288
    public function setHtmlSpecialCharsDecode($htmlSpecialCharsDecode = false): void
142
    {
143 288
        $this->htmlSpecialCharsDecode = $htmlSpecialCharsDecode;
144 288
    }
145
146
    /**
147
     * Returns the id of this object.
148
     */
149 489
    public function id(): int
150
    {
151 489
        return $this->id;
152
    }
153
154
    /**
155
     * Returns the parent of node.
156
     *
157
     * @return AbstractNode
158
     */
159 261
    public function getParent()
160
    {
161 261
        return $this->parent;
162
    }
163
164
    /**
165
     * Sets the parent node.
166
     *
167
     * @throws ChildNotFoundException
168
     * @throws CircularException
169
     */
170 483
    public function setParent(InnerNode $parent): AbstractNode
171
    {
172
        // remove from old parent
173 483
        if ($this->parent !== null) {
174 33
            if ($this->parent->id() == $parent->id()) {
175
                // already the parent
176 27
                return $this;
177
            }
178
179 9
            $this->parent->removeChild($this->id);
180
        }
181
182 483
        $this->parent = $parent;
183
184
        // assign child to parent
185 483
        $this->parent->addChild($this);
186
187 483
        return $this;
188
    }
189
190
    /**
191
     * Removes this node and all its children from the
192
     * DOM tree.
193
     *
194
     * @return void
195
     */
196 3
    public function delete()
197
    {
198 3
        if ($this->parent !== null) {
199 3
            $this->parent->removeChild($this->id);
200
        }
201 3
        $this->parent->clear();
202 3
        $this->clear();
203 3
    }
204
205
    /**
206
     * Sets the encoding class to this node.
207
     *
208
     * @return void
209
     */
210 246
    public function propagateEncoding(Encode $encode)
211
    {
212 246
        $this->encode = $encode;
213 246
        $this->tag->setEncoding($encode);
214 246
    }
215
216
    /**
217
     * Checks if the given node id is an ancestor of
218
     * the current node.
219
     */
220 486
    public function isAncestor(int $id): bool
221
    {
222 486
        if ($this->getAncestor($id) !== null) {
223 6
            return true;
224
        }
225
226 486
        return false;
227
    }
228
229
    /**
230
     * Attempts to get an ancestor node by the given id.
231
     *
232
     * @return AbstractNode|null
233
     */
234 489
    public function getAncestor(int $id)
235
    {
236 489
        if ($this->parent !== null) {
237 330
            if ($this->parent->id() == $id) {
238 12
                return $this->parent;
239
            }
240
241 321
            return $this->parent->getAncestor($id);
242
        }
243 489
    }
244
245
    /**
246
     * Checks if the current node has a next sibling.
247
     */
248
    public function hasNextSibling(): bool
249
    {
250
        try {
251
            $this->nextSibling();
252
253
            // sibling found, return true;
254
            return true;
255
        } catch (ParentNotFoundException $e) {
256
            // no parent, no next sibling
257
            unset($e);
258
259
            return false;
260
        } catch (ChildNotFoundException $e) {
261
            // no sibling found
262
            unset($e);
263
264
            return false;
265
        }
266
    }
267
268
    /**
269
     * Attempts to get the next sibling.
270
     *
271
     * @throws ChildNotFoundException
272
     * @throws ParentNotFoundException
273
     */
274 30
    public function nextSibling(): AbstractNode
275
    {
276 30
        if ($this->parent === null) {
277 3
            throw new ParentNotFoundException('Parent is not set for this node.');
278
        }
279
280 27
        return $this->parent->nextChild($this->id);
281
    }
282
283
    /**
284
     * Attempts to get the previous sibling.
285
     *
286
     * @throws ChildNotFoundException
287
     * @throws ParentNotFoundException
288
     */
289 9
    public function previousSibling(): AbstractNode
290
    {
291 9
        if ($this->parent === null) {
292 3
            throw new ParentNotFoundException('Parent is not set for this node.');
293
        }
294
295 6
        return $this->parent->previousChild($this->id);
296
    }
297
298
    /**
299
     * Gets the tag object of this node.
300
     */
301 357
    public function getTag(): Tag
302
    {
303 357
        return $this->tag;
304
    }
305
306
    /**
307
     * Replaces the tag for this node.
308
     *
309
     * @param string|Tag $tag
310
     */
311 3
    public function setTag($tag): AbstractNode
312
    {
313 3
        if (\is_string($tag)) {
314 3
            $tag = new Tag($tag);
315
        }
316
317 3
        $this->tag = $tag;
318
319
        // clear any cache
320 3
        $this->clear();
321
322 3
        return $this;
323
    }
324
325
    /**
326
     * A wrapper method that simply calls the getAttribute method
327
     * on the tag of this node.
328
     */
329 6
    public function getAttributes(): array
330
    {
331 6
        $attributes = $this->tag->getAttributes();
332 6
        foreach ($attributes as $name => $attributeDTO) {
333 3
            $attributes[$name] = $attributeDTO->getValue();
334
        }
335
336 6
        return $attributes;
337
    }
338
339
    /**
340
     * A wrapper method that simply calls the getAttribute method
341
     * on the tag of this node.
342
     */
343 342
    public function getAttribute(string $key): ?string
344
    {
345
        try {
346 342
            $attributeDTO = $this->tag->getAttribute($key);
347 321
        } catch (AttributeNotFoundException $e) {
348
            // no attribute with this key exists, returning null.
349 321
            unset($e);
350
351 321
            return null;
352
        }
353
354 144
        return $attributeDTO->getValue();
355
    }
356
357
    /**
358
     * A wrapper method that simply calls the hasAttribute method
359
     * on the tag of this node.
360
     */
361 120
    public function hasAttribute(string $key): bool
362
    {
363 120
        return $this->tag->hasAttribute($key);
364
    }
365
366
    /**
367
     * A wrapper method that simply calls the setAttribute method
368
     * on the tag of this node.
369
     */
370 24
    public function setAttribute(string $key, ?string $value, bool $doubleQuote = true): AbstractNode
371
    {
372 24
        $this->tag->setAttribute($key, $value, $doubleQuote);
373
374
        //clear any cache
375 24
        $this->clear();
376
377 24
        return $this;
378
    }
379
380
    /**
381
     * A wrapper method that simply calls the removeAttribute method
382
     * on the tag of this node.
383
     */
384 3
    public function removeAttribute(string $key): void
385
    {
386 3
        $this->tag->removeAttribute($key);
387
388
        //clear any cache
389 3
        $this->clear();
390 3
    }
391
392
    /**
393
     * A wrapper method that simply calls the removeAllAttributes
394
     * method on the tag of this node.
395
     */
396 3
    public function removeAllAttributes(): void
397
    {
398 3
        $this->tag->removeAllAttributes();
399
400
        //clear any cache
401 3
        $this->clear();
402 3
    }
403
404
    /**
405
     * Function to locate a specific ancestor tag in the path to the root.
406
     *
407
     * @throws ParentNotFoundException
408
     */
409 6
    public function ancestorByTag(string $tag): AbstractNode
410
    {
411
        // Start by including ourselves in the comparison.
412 6
        $node = $this;
413
414
        do {
415 6
            if ($node->tag->name() == $tag) {
416 3
                return $node;
417
            }
418
419 3
            $node = $node->getParent();
420 3
        } while ($node !== null);
421
422 3
        throw new ParentNotFoundException('Could not find an ancestor with "' . $tag . '" tag');
423
    }
424
425
    /**
426
     * Find elements by css selector.
427
     *
428
     * @throws ChildNotFoundException
429
     *
430
     * @return mixed|Collection|null
431
     */
432 282
    public function find(string $selectorString, ?int $nth = null, ?SelectorInterface $selector = null)
433
    {
434 282
        if (\is_null($selector)) {
435 282
            $selector = new Selector($selectorString);
436
        }
437
438 282
        $nodes = $selector->find($this);
439
440 282
        if ($nth !== null) {
441
            // return nth-element or array
442 282
            if (isset($nodes[$nth])) {
443 129
                return $nodes[$nth];
444
            }
445
446 252
            return;
447
        }
448
449 111
        return $nodes;
450
    }
451
452
    /**
453
     * Find node by id.
454
     *
455
     * @throws ChildNotFoundException
456
     * @throws ParentNotFoundException
457
     *
458
     * @return bool|AbstractNode
459
     */
460
    public function findById(int $id)
461
    {
462
        $finder = new Finder($id);
463
464
        return $finder->find($this);
465
    }
466
467
    /**
468
     * Gets the inner html of this node.
469
     */
470
    abstract public function innerHtml(): string;
471
472
    /**
473
     * Gets the html of this node, including it's own
474
     * tag.
475
     */
476
    abstract public function outerHtml(): string;
477
478
    /**
479
     * Gets the text of this node (if there is any text).
480
     */
481
    abstract public function text(): string;
482
483
    /**
484
     * Check is node type textNode.
485
     */
486
    public function isTextNode(): bool
487
    {
488
        return false;
489
    }
490
491
    /**
492
     * Call this when something in the node tree has changed. Like a child has been added
493
     * or a parent has been changed.
494
     */
495
    abstract protected function clear(): void;
496
}
497