Completed
Pull Request — master (#195)
by
unknown
15:08
created

AbstractNode   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Test Coverage

Coverage 98.43%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 107
dl 0
loc 518
ccs 125
cts 127
cp 0.9843
rs 7.92
c 3
b 0
f 0
wmc 51

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getTag() 0 3 1
A setHtmlSpecialCharsDecode() 0 3 1
A __destruct() 0 6 1
A resetCount() 0 3 1
A getParent() 0 3 1
A id() 0 3 1
A getAncestor() 0 11 3
A delete() 0 7 2
A isAncestor() 0 7 2
A hasNextSibling() 0 18 3
A setParent() 0 18 3
B __get() 0 20 7
A propagateEncoding() 0 4 1
A __construct() 0 4 1
A previousSibling() 0 7 2
A __toString() 0 3 1
A nextSibling() 0 7 2
A getAttribute() 0 8 2
A findById() 0 5 1
A getAttributes() 0 8 2
A find() 0 16 3
A isTextNode() 0 4 1
A hasAttribute() 0 3 1
A removeAttribute() 0 6 1
A ancestorByTag() 0 14 3
A removeAllAttributes() 0 6 1
A setTag() 0 12 2
A setAttribute() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractNode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractNode, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
namespace PHPHtmlParser\Dom;
3
4
use PHPHtmlParser\Exceptions\CircularException;
5
use PHPHtmlParser\Exceptions\ParentNotFoundException;
6
use PHPHtmlParser\Exceptions\ChildNotFoundException;
7
use PHPHtmlParser\Selector\Selector;
8
use PHPHtmlParser\Selector\Parser as SelectorParser;
9
use stringEncode\Encode;
10
use PHPHtmlParser\Finder;
11
12
/**
13
 * Dom node object.
14
 * @property string    outerhtml
15
 * @property string    innerhtml
16
 * @property string    text
17
 * @property int       prev
18
 * @property int       next
19
 * @property Tag       tag
20
 * @property InnerNode parent
21
 */
22
abstract class AbstractNode
23
{
24
    private static $count = 0;
25
    /**
26
     * Contains the tag name/type
27
     * @var Tag
28
     */
29
    protected $tag;
30
31
    /**
32
     * Contains a list of attributes on this tag.
33
     *
34
     * @var array
35
     */
36
    protected $attr = [];
37
38
    /**
39
     * Contains the parent Node.
40
     *
41
     * @var InnerNode
42
     */
43
    protected $parent = null;
44
45
    /**
46
     * The unique id of the class. Given by PHP.
47
     *
48
     * @var int
49
     */
50
    protected $id;
51
52
    /**
53
     * The encoding class used to encode strings.
54
     *
55
     * @var mixed
56
     */
57
    protected $encode;
58
59
    /**
60
     * An array of all the children.
61
     *
62
     * @var array
63
     */
64
    protected $children = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $htmlSpecialCharsDecode = false;
70
71
    /**
72
     * Creates a unique id for this node.
73
     */
74 492
    public function __construct()
75
    {
76 492
        $this->id = self::$count;
77 492
        self::$count++;
78 492
    }
79
80
    /**
81
     * Magic get method for attributes and certain methods.
82
     *
83
     * @param string $key
84
     * @return mixed
85
     */
86 192
    public function __get(string $key)
87
    {
88
        // check attribute first
89 192
        if ( ! is_null($this->getAttribute($key))) {
90 51
            return $this->getAttribute($key);
91
        }
92 153
        switch (strtolower($key)) {
93 153
            case 'outerhtml':
94 36
                return $this->outerHtml();
95 117
            case 'innerhtml':
96 63
                return $this->innerHtml();
97 57
            case 'text':
98 51
                return $this->text();
99 6
            case 'tag':
100 3
                return $this->getTag();
101 3
            case 'parent':
102
                return $this->getParent();
103
        }
104
105 3
        return null;
106
    }
107
108
    /**
109
     * Attempts to clear out any object references.
110
     */
111 126
    public function __destruct()
112
    {
113 126
        $this->tag      = null;
114 126
        $this->attr     = [];
115 126
        $this->parent   = null;
116 126
        $this->children = [];
117 126
    }
118
119
    /**
120
     * Simply calls the outer text method.
121
     *
122
     * @return string
123
     */
124 12
    public function __toString()
125
    {
126 12
        return $this->outerHtml();
127
    }
128
129
    /**
130
     * @param bool $htmlSpecialCharsDecode
131
     * @return void
132
     */
133 228
    public function setHtmlSpecialCharsDecode($htmlSpecialCharsDecode = false): void
134
    {
135 228
        $this->htmlSpecialCharsDecode = $htmlSpecialCharsDecode;
136 228
    }
137
138
139
    /**
140
     * Reset node counter
141
     *
142
     * @return void
143
     */
144 168
    public static function resetCount()
145
    {
146 168
        self::$count = 0;
147 168
    }
148
149
    /**
150
     * Returns the id of this object.
151
     *
152
     * @return int
153
     */
154 426
    public function id(): int
155
    {
156 426
        return $this->id;
157
    }
158
159
    /**
160
     * Returns the parent of node.
161
     *
162
     * @return AbstractNode
163
     */
164 228
    public function getParent()
165
    {
166 228
        return $this->parent;
167
    }
168
169
    /**
170
     * Sets the parent node.
171
     * @param InnerNode $parent
172
     * @return AbstractNode
173
     * @throws ChildNotFoundException
174
     * @throws CircularException
175
     */
176 420
    public function setParent(InnerNode $parent): AbstractNode
177
    {
178
        // remove from old parent
179 420
        if ( ! is_null($this->parent)) {
180 30
            if ($this->parent->id() == $parent->id()) {
181
                // already the parent
182 27
                return $this;
183
            }
184
185 6
            $this->parent->removeChild($this->id);
186
        }
187
188 420
        $this->parent = $parent;
189
190
        // assign child to parent
191 420
        $this->parent->addChild($this);
192
193 420
        return $this;
194
    }
195
196
    /**
197
     * Removes this node and all its children from the
198
     * DOM tree.
199
     *
200
     * @return void
201
     */
202 3
    public function delete()
203
    {
204 3
        if ( ! is_null($this->parent)) {
205 3
            $this->parent->removeChild($this->id);
206
        }
207 3
        $this->parent->clear();
208 3
        $this->clear();
209 3
    }
210
211
    /**
212
     * Sets the encoding class to this node.
213
     *
214
     * @param Encode $encode
215
     * @return void
216
     */
217 216
    public function propagateEncoding(Encode $encode)
218
    {
219 216
        $this->encode = $encode;
220 216
        $this->tag->setEncoding($encode);
221 216
    }
222
223
    /**
224
     * Checks if the given node id is an ancestor of
225
     * the current node.
226
     *
227
     * @param int $id
228
     * @return bool
229
     */
230 423
    public function isAncestor(int $id): Bool
231
    {
232 423
        if ( ! is_null($this->getAncestor($id))) {
233 6
            return true;
234
        }
235
236 423
        return false;
237
    }
238
239
    /**
240
     * Attempts to get an ancestor node by the given id.
241
     *
242
     * @param int $id
243
     * @return null|AbstractNode
244
     */
245 426
    public function getAncestor(int $id)
246
    {
247 426
        if ( ! is_null($this->parent)) {
248 297
            if ($this->parent->id() == $id) {
249 12
                return $this->parent;
250
            }
251
252 288
            return $this->parent->getAncestor($id);
253
        }
254
255 426
        return null;
256
    }
257
258
    /**
259
     * Checks if the current node has a next sibling.
260
     *
261
     * @return bool
262
     */
263 9
    public function hasNextSibling(): bool
264
    {
265
        try
266
        {
267 9
            $this->nextSibling();
268
269
            // sibling found, return true;
270 9
            return true;
271
        }
272 9
        catch (ParentNotFoundException $e)
273
        {
274
            // no parent, no next sibling
275
            return false;
276
        }
277 9
        catch (ChildNotFoundException $e)
278
        {
279
            // no sibling found
280 9
            return false;
281
        }
282
    }
283
284
    /**
285
     * Attempts to get the next sibling.
286
     * @return AbstractNode
287
     * @throws ChildNotFoundException
288
     * @throws ParentNotFoundException
289
     */
290 39
    public function nextSibling(): AbstractNode
291
    {
292 39
        if (is_null($this->parent)) {
293 3
            throw new ParentNotFoundException('Parent is not set for this node.');
294
        }
295
296 36
        return $this->parent->nextChild($this->id);
297
    }
298
299
    /**
300
     * Attempts to get the previous sibling.
301
     * @return AbstractNode
302
     * @throws ChildNotFoundException
303
     * @throws ParentNotFoundException
304
     */
305 9
    public function previousSibling(): AbstractNode
306
    {
307 9
        if (is_null($this->parent)) {
308 3
            throw new ParentNotFoundException('Parent is not set for this node.');
309
        }
310
311 6
        return $this->parent->previousChild($this->id);
312
    }
313
314
    /**
315
     * Gets the tag object of this node.
316
     *
317
     * @return Tag
318
     */
319 297
    public function getTag(): Tag
320
    {
321 297
        return $this->tag;
322
    }
323
324
    /**
325
     * Replaces the tag for this node
326
     *
327
     * @param string|Tag $tag
328
     * @return AbstractNode
329
     * @chainable
330 6
     */
331
    public function setTag($tag): AbstractNode
332 6
    {
333 6
        if (is_string($tag)) {
334 3
            $tag = new Tag($tag);
335
        }
336
337 6
        $this->tag = $tag;
338
339
        // clear any cache
340
        $this->clear();
341
342
        return $this;
343
    }
344
345
    /**
346
     * A wrapper method that simply calls the getAttribute method
347 222
     * on the tag of this node.
348
     *
349 222
     * @return array
350 222
     */
351 117
    public function getAttributes(): array
352
    {
353
        $attributes = $this->tag->getAttributes();
354 222
        foreach ($attributes as $name => $info) {
355
            $attributes[$name] = $info['value'];
356
        }
357
358
        return $attributes;
359
    }
360
361
    /**
362
     * A wrapper method that simply calls the getAttribute method
363
     * on the tag of this node.
364 90
     *
365
     * @param string $key
366 90
     * @return mixed
367
     */
368
    public function getAttribute(string $key)
369
    {
370
        $attribute = $this->tag->getAttribute($key);
371
        if ( ! is_null($attribute)) {
372
            $attribute = $attribute['value'];
373
        }
374
375
        return $attribute;
376
    }
377
378 24
    /**
379
     * A wrapper method that simply calls the hasAttribute method
380 24
     * on the tag of this node.
381
     *
382
     * @param string $key
383 24
     * @return bool
384
     */
385 24
    public function hasAttribute(string $key): bool
386
    {
387
        return $this->tag->hasAttribute($key);
388
    }
389
390
    /**
391
     * A wrapper method that simply calls the setAttribute method
392
     * on the tag of this node.
393
     *
394
     * @param string $key
395 3
     * @param string|null $value
396
     * @return AbstractNode
397 3
     * @chainable
398
     */
399
    public function setAttribute(string $key, $value): AbstractNode
400 3
    {
401 3
        $this->tag->setAttribute($key, $value);
402
403
        //clear any cache
404
        $this->clear();
405
406
        return $this;
407
    }
408
409 3
    /**
410
     * A wrapper method that simply calls the removeAttribute method
411 3
     * on the tag of this node.
412
     *
413
     * @param string $key
414 3
     * @return void
415 3
     */
416
    public function removeAttribute(string $key): void
417
    {
418
        $this->tag->removeAttribute($key);
419
420
        //clear any cache
421
        $this->clear();
422
    }
423 6
424
    /**
425
     * A wrapper method that simply calls the removeAllAttributes
426 6
     * method on the tag of this node.
427
     *
428 6
     * @return void
429 6
     */
430 3
    public function removeAllAttributes(): void
431
    {
432
        $this->tag->removeAllAttributes();
433 3
434
        //clear any cache
435
        $this->clear();
436 3
    }
437
    /**
438
     * Function to locate a specific ancestor tag in the path to the root.
439
     *
440
     * @param  string $tag
441
     * @return AbstractNode
442
     * @throws ParentNotFoundException
443
     */
444
    public function ancestorByTag(string $tag): AbstractNode
445
    {
446
        // Start by including ourselves in the comparison.
447 222
        $node = $this;
448
449 222
        while ( ! is_null($node)) {
450 222
            if ($node->tag->name() == $tag) {
451 222
                return $node;
452
            }
453 222
454
            $node = $node->getParent();
455 222
        }
456 120
457
        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
458
    }
459 192
460
    /**
461
     * Find elements by css selector
462 75
     * @param string   $selector
463
     * @param int|null $nth
464
     * @param bool     $depthFirst
465
     * @return mixed|Collection|null
466
     * @throws ChildNotFoundException
467
     */
468
    public function find(string $selector, int $nth = null, bool $depthFirst = false)
469
    {
470
        $selector = new Selector($selector, new SelectorParser());
471
        $selector->setDepthFirstFind($depthFirst);
472 9
        $nodes    = $selector->find($this);
473
474 9
        if ( ! is_null($nth)) {
475
            // return nth-element or array
476 9
            if (isset($nodes[$nth])) {
477
                return $nodes[$nth];
478
            }
479
480
            return null;
481
        }
482
483
        return $nodes;
484
    }
485
486
    /**
487
     * Find node by id
488
     * @param int $id
489
     * @return bool|AbstractNode
490
     * @throws ChildNotFoundException
491
     * @throws ParentNotFoundException
492
     */
493
    public function findById(int $id)
494
    {
495
        $finder= new Finder($id);
496
497
        return $finder->find($this);
498
    }
499
500
501
    /**
502
     * Gets the inner html of this node.
503
     *
504
     * @return string
505
     */
506
    abstract public function innerHtml(): string;
507
508
    /**
509
     * Gets the html of this node, including it's own
510
     * tag.
511
     *
512
     * @return string
513
     */
514
    abstract public function outerHtml(): string;
515 9
516
    /**
517
     * Gets the text of this node (if there is any text).
518 9
     *
519
     * @return string
520
     */
521
    abstract public function text(): string;
522
523
    /**
524
     * Call this when something in the node tree has changed. Like a child has been added
525
     * or a parent has been changed.
526
     *
527
     * @return void
528
     */
529
    abstract protected function clear(): void;
530
531
    /**
532
     * Check is node type textNode
533
     *
534
     * @return boolean
535
     */
536
    public function isTextNode(): bool
537
    {
538
539
        return false;
540
    }
541
}
542