Completed
Pull Request — master (#90)
by
unknown
06:27
created

AbstractNode   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 0
Metric Value
wmc 49
lcom 2
cbo 5
dl 0
loc 458
rs 8.5454
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B __get() 0 21 7
A __destruct() 0 7 1
A __toString() 0 4 1
A resetCount() 0 4 1
A id() 0 4 1
A getParent() 0 4 1
A setParent() 0 22 3
A delete() 0 8 2
A propagateEncoding() 0 5 1
A isAncestor() 0 8 2
A getAncestor() 0 12 3
A hasNextSibling() 0 8 3
A nextSibling() 0 8 2
A previousSibling() 0 8 2
A getTag() 0 4 1
A getAttributes() 0 9 2
A getAttribute() 0 9 2
A setAttribute() 0 6 1
A removeAttribute() 0 4 1
A removeAllAttributes() 0 4 1
A ancestorByTag() 0 15 3
A find() 0 16 3
A findById() 0 6 1
innerHtml() 0 1 ?
outerHtml() 0 1 ?
text() 0 1 ?
clear() 0 1 ?
A isTextNode() 0 4 1
A getAttributeArray() 0 15 2

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
2
namespace PHPHtmlParser\Dom;
3
4
use PHPHtmlParser\Exceptions\CircularException;
5
use PHPHtmlParser\Exceptions\ParentNotFoundException;
6
use PHPHtmlParser\Selector;
7
use stringEncode\Encode;
8
use PHPHtmlParser\Finder;
9
10
/**
11
 * Dom node object.
12
 *
13
 * @property string outerhtml
14
 * @property string innerhtml
15
 * @property string text
16
 * @property \PHPHtmlParser\Dom\Tag tag
17
 * @property InnerNode parent
18
 */
19
abstract class AbstractNode
20
{
21
    private static $count = 0;
22
    /**
23
     * Contains the tag name/type
24
     *
25
     * @var \PHPHtmlParser\Dom\Tag
26
     */
27
    protected $tag;
28
29
    /**
30
     * Contains a list of attributes on this tag.
31
     *
32
     * @var array
33
     */
34
    protected $attr = [];
35
36
    /**
37
     * Contains the parent Node.
38
     *
39
     * @var InnerNode
40
     */
41
    protected $parent = null;
42
43
    /**
44
     * The unique id of the class. Given by PHP.
45
     *
46
     * @var string
47
     */
48
    protected $id;
49
50
    /**
51
     * The encoding class used to encode strings.
52
     *
53
     * @var mixed
54
     */
55
    protected $encode;
56
57
    /**
58
     * Creates a unique id for this node.
59
     */
60
    public function __construct()
61
    {
62
        $this->id = self::$count;
0 ignored issues
show
Documentation Bug introduced by
The property $id was declared of type string, but self::$count is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
63
        self::$count++;
64
    }
65
66
    /**
67
     * Magic get method for attributes and certain methods.
68
     *
69
     * @param string $key
70
     * @return mixed
71
     */
72
    public function __get($key)
73
    {
74
        // check attribute first
75
        if ( ! is_null($this->getAttribute($key))) {
76
            return $this->getAttribute($key);
77
        }
78
        switch (strtolower($key)) {
79
            case 'outerhtml':
80
                return $this->outerHtml();
81
            case 'innerhtml':
82
                return $this->innerHtml();
83
            case 'text':
84
                return $this->text();
85
            case 'tag':
86
                return $this->getTag();
87
            case 'parent':
88
                $this->getParent();
0 ignored issues
show
Unused Code introduced by
The call to the method PHPHtmlParser\Dom\AbstractNode::getParent() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
89
        }
90
91
        return null;
92
    }
93
94
    /**
95
     * Attempts to clear out any object references.
96
     */
97
    public function __destruct()
98
    {
99
        $this->tag      = null;
100
        $this->attr     = [];
101
        $this->parent   = null;
102
        $this->children = [];
0 ignored issues
show
Bug introduced by
The property children does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
103
    }
104
105
    /**
106
     * Simply calls the outer text method.
107
     *
108
     * @return string
109
     */
110
    public function __toString()
111
    {
112
        return $this->outerHtml();
113
    }
114
115
    /**
116
     * Reset node counter
117
     */
118
    public static function resetCount()
119
    {
120
        self::$count = 0;
121
    }
122
123
    /**
124
     * Returns the id of this object.
125
     */
126
    public function id()
127
    {
128
        return $this->id;
129
    }
130
131
    /**
132
     * Returns the parent of node.
133
     *
134
     * @return AbstractNode
135
     */
136
    public function getParent()
137
    {
138
        return $this->parent;
139
    }
140
141
    /**
142
     * Sets the parent node.
143
     *
144
     * @param InnerNode $parent
145
     * @return $this
146
     * @throws CircularException
147
     */
148
    public function setParent(InnerNode $parent)
149
    {
150
        // remove from old parent
151
        if ( ! is_null($this->parent)) {
152
            if ($this->parent->id() == $parent->id()) {
153
                // already the parent
154
                return $this;
155
            }
156
157
            $this->parent->removeChild($this->id);
158
        }
159
160
        $this->parent = $parent;
161
162
        // assign child to parent
163
        $this->parent->addChild($this);
164
165
        //clear any cache
166
        $this->clear();
167
168
        return $this;
169
    }
170
171
    /**
172
     * Removes this node and all its children from the
173
     * DOM tree.
174
     *
175
     * @return void
176
     */
177
    public function delete()
178
    {
179
        if ( ! is_null($this->parent)) {
180
            $this->parent->removeChild($this->id);
181
        }
182
183
        $this->parent = null;
184
    }
185
186
    /**
187
     * Sets the encoding class to this node.
188
     *
189
     * @param Encode $encode
190
     * @return void
191
     */
192
    public function propagateEncoding(Encode $encode)
193
    {
194
        $this->encode = $encode;
195
        $this->tag->setEncoding($encode);
196
    }
197
198
    /**
199
     * Checks if the given node id is an ancestor of
200
     * the current node.
201
     *
202
     * @param int $id
203
     * @return bool
204
     */
205
    public function isAncestor($id)
206
    {
207
        if ( ! is_null($this->getAncestor($id))) {
208
            return true;
209
        }
210
211
        return false;
212
    }
213
214
    /**
215
     * Attempts to get an ancestor node by the given id.
216
     *
217
     * @param int $id
218
     * @return null|AbstractNode
219
     */
220
    public function getAncestor($id)
221
    {
222
        if ( ! is_null($this->parent)) {
223
            if ($this->parent->id() == $id) {
224
                return $this->parent;
225
            }
226
227
            return $this->parent->getAncestor($id);
228
        }
229
230
        return null;
231
    }
232
233
    public function hasNextSibling()
234
    {
235
        if (is_null($this->parent) || (!$this->parent->hasChildren())) {
236
            return false;
237
        }
238
239
        return $this->parent->hasNextChild($this->id());
240
    }
241
242
    /**
243
     * Attempts to get the next sibling.
244
     *
245
     * @return AbstractNode
246
     * @throws ParentNotFoundException
247
     */
248
    public function nextSibling()
249
    {
250
        if (is_null($this->parent)) {
251
            throw new ParentNotFoundException('Parent is not set for this node.');
252
        }
253
254
        return $this->parent->nextChild($this->id);
255
    }
256
257
    /**
258
     * Attempts to get the previous sibling
259
     *
260
     * @return AbstractNode
261
     * @throws ParentNotFoundException
262
     */
263
    public function previousSibling()
264
    {
265
        if (is_null($this->parent)) {
266
            throw new ParentNotFoundException('Parent is not set for this node.');
267
        }
268
269
        return $this->parent->previousChild($this->id);
270
    }
271
272
    /**
273
     * Gets the tag object of this node.
274
     *
275
     * @return Tag
276
     */
277
    public function getTag()
278
    {
279
        return $this->tag;
280
    }
281
282
    /**
283
     * A wrapper method that simply calls the getAttribute method
284
     * on the tag of this node.
285
     *
286
     * @return array
287
     */
288
    public function getAttributes()
289
    {
290
        $attributes = $this->tag->getAttributes();
291
        foreach ($attributes as $name => $info) {
292
            $attributes[$name] = $info['value'];
293
        }
294
295
        return $attributes;
296
    }
297
298
    /**
299
     * A wrapper method that simply calls the getAttribute method
300
     * on the tag of this node.
301
     *
302
     * @param string $key
303
     * @return mixed
304
     */
305
    public function getAttribute($key)
306
    {
307
        $attribute = $this->tag->getAttribute($key);
308
        if ( ! is_null($attribute)) {
309
            $attribute = $attribute['value'];
310
        }
311
312
        return $attribute;
313
    }
314
315
    /**
316
     * A wrapper method that simply calls the setAttribute method
317
     * on the tag of this node.
318
     *
319
     * @param string $key
320
     * @param string $value
321
     * @return $this
322
     */
323
    public function setAttribute($key, $value)
324
    {
325
        $this->tag->setAttribute($key, $value);
326
327
        return $this;
328
    }
329
330
    /**
331
     * A wrapper method that simply calls the removeAttribute method
332
     * on the tag of this node.
333
     *
334
     * @param string $key
335
     * @return void
336
     */
337
    public function removeAttribute($key)
338
    {
339
        $this->tag->removeAttribute($key);
340
    }
341
342
    /**
343
     * A wrapper method that simply calls the removeAllAttributes
344
     * method on the tag of this node.
345
     *
346
     * @return void
347
     */
348
    public function removeAllAttributes()
349
    {
350
        $this->tag->removeAllAttributes();
351
    }
352
353
    /**
354
     * Function to locate a specific ancestor tag in the path to the root.
355
     *
356
     * @param  string $tag
357
     * @return AbstractNode
358
     * @throws ParentNotFoundException
359
     */
360
    public function ancestorByTag($tag)
361
    {
362
        // Start by including ourselves in the comparison.
363
        $node = $this;
364
365
        while ( ! is_null($node)) {
366
            if ($node->tag->name() == $tag) {
367
                return $node;
368
            }
369
370
            $node = $node->getParent();
371
        }
372
373
        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
374
    }
375
376
    /**
377
     * Find elements by css selector
378
     *
379
     * @param string $selector
380
     * @param int $nth
381
     * @return array|AbstractNode
382
     */
383
    public function find($selector, $nth = null)
384
    {
385
        $selector = new Selector($selector);
386
        $nodes    = $selector->find($this);
387
388
        if ( ! is_null($nth)) {
389
            // return nth-element or array
390
            if (isset($nodes[$nth])) {
391
                return $nodes[$nth];
392
            }
393
394
            return null;
395
        }
396
397
        return $nodes;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $nodes; (PHPHtmlParser\Dom\Collection) is incompatible with the return type documented by PHPHtmlParser\Dom\AbstractNode::find of type array|PHPHtmlParser\Dom\AbstractNode.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
398
    }
399
400
    /**
401
     * Find node by id
402
     *
403
     * @param $id
404
     * @return bool|AbstractNode
405
     */
406
    public function findById($id)
407
    {
408
        $finder= new Finder($id);
409
410
        return $finder->find($this);
411
    }
412
413
414
    /**
415
     * Gets the inner html of this node.
416
     *
417
     * @return string
418
     */
419
    abstract public function innerHtml();
420
421
    /**
422
     * Gets the html of this node, including it's own
423
     * tag.
424
     *
425
     * @return string
426
     */
427
    abstract public function outerHtml();
428
429
    /**
430
     * Gets the text of this node (if there is any text).
431
     *
432
     * @return string
433
     */
434
    abstract public function text();
435
436
    /**
437
     * Call this when something in the node tree has changed. Like a child has been added
438
     * or a parent has been changed.
439
     *
440
     * @return void
441
     */
442
    abstract protected function clear();
443
444
    /**
445
     * Check is node type textNode
446
     *
447
     * @return boolean
448
     */
449
    public function isTextNode() {
450
451
        return false;
452
    }
453
454
     /**
455
     * Get attribute values in array
456
     *
457
     * @param $attributeValue
458
     * @param $delimiter
459
     * @return array
460
     */
461
    public function getAttributeArray($attributeValue, $delimiter)
462
    {
463
        $attributeValue = trim($attributeValue);
464
        $attributeValue = substr($attributeValue, 0, -1);
465
        $attributeValue = explode(';', $attributeValue);
466
467
        $result = [];
468
469
        foreach ($attributeValue as $attr) {
470
            $attr = explode($delimiter, $attr);
471
            $result[$attr[0]] = $attr[1];
472
        }
473
474
        return $result;
475
    }
476
}
477