Completed
Pull Request — master (#96)
by Oleg
07:14
created

AbstractNode   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 4
dl 0
loc 405
rs 8.295
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B __get() 0 21 7
A __destruct() 0 7 1
A __toString() 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 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 hasAttribute() 0 4 1
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
innerHtml() 0 1 ?
outerHtml() 0 1 ?
text() 0 1 ?
clear() 0 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. 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
9
/**
10
 * Dom node object.
11
 *
12
 * @property string outerhtml
13
 * @property string innerhtml
14
 * @property string text
15
 * @property \PHPHtmlParser\Dom\Tag tag
16
 * @property InnerNode parent
17
 */
18
abstract class AbstractNode
19
{
20
21
    /**
22
     * Contains the tag name/type
23
     *
24
     * @var \PHPHtmlParser\Dom\Tag
25
     */
26
    protected $tag;
27
28
    /**
29
     * Contains a list of attributes on this tag.
30
     *
31
     * @var array
32
     */
33
    protected $attr = [];
34
35
    /**
36
     * Contains the parent Node.
37
     *
38
     * @var InnerNode
39
     */
40
    protected $parent = null;
41
42
    /**
43
     * The unique id of the class. Given by PHP.
44
     *
45
     * @var string
46
     */
47
    protected $id;
48
49
    /**
50
     * The encoding class used to encode strings.
51
     *
52
     * @var mixed
53
     */
54
    protected $encode;
55
56
    /**
57
     * Creates a unique spl hash for this node.
58
     */
59
    public function __construct()
60
    {
61
        $this->id = spl_object_hash($this);
62
    }
63
64
    /**
65
     * Magic get method for attributes and certain methods.
66
     *
67
     * @param string $key
68
     * @return mixed
69
     */
70
    public function __get($key)
71
    {
72
        // check attribute first
73
        if ( ! is_null($this->getAttribute($key))) {
74
            return $this->getAttribute($key);
75
        }
76
        switch (strtolower($key)) {
77
            case 'outerhtml':
78
                return $this->outerHtml();
79
            case 'innerhtml':
80
                return $this->innerHtml();
81
            case 'text':
82
                return $this->text();
83
            case 'tag':
84
                return $this->getTag();
85
            case 'parent':
86
                $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...
87
        }
88
89
        return null;
90
    }
91
92
    /**
93
     * Attempts to clear out any object references.
94
     */
95
    public function __destruct()
96
    {
97
        $this->tag      = null;
98
        $this->attr     = [];
99
        $this->parent   = null;
100
        $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...
101
    }
102
103
    /**
104
     * Simply calls the outer text method.
105
     *
106
     * @return string
107
     */
108
    public function __toString()
109
    {
110
        return $this->outerHtml();
111
    }
112
113
    /**
114
     * Returns the id of this object.
115
     */
116
    public function id()
117
    {
118
        return $this->id;
119
    }
120
121
    /**
122
     * Returns the parent of node.
123
     *
124
     * @return AbstractNode
125
     */
126
    public function getParent()
127
    {
128
        return $this->parent;
129
    }
130
131
    /**
132
     * Sets the parent node.
133
     *
134
     * @param InnerNode $parent
135
     * @return $this
136
     * @throws CircularException
137
     */
138
    public function setParent(InnerNode $parent)
139
    {
140
        // remove from old parent
141
        if ( ! is_null($this->parent)) {
142
            if ($this->parent->id() == $parent->id()) {
143
                // already the parent
144
                return $this;
145
            }
146
147
            $this->parent->removeChild($this->id);
148
        }
149
150
        $this->parent = $parent;
151
152
        // assign child to parent
153
        $this->parent->addChild($this);
154
155
        //clear any cache
156
        $this->clear();
157
158
        return $this;
159
    }
160
161
    /**
162
     * Removes this node and all its children from the
163
     * DOM tree.
164
     *
165
     * @return void
166
     */
167
    public function delete()
168
    {
169
        if ( ! is_null($this->parent)) {
170
            $this->parent->removeChild($this->id);
171
        }
172
173
        $this->parent = null;
174
    }
175
176
    /**
177
     * Sets the encoding class to this node.
178
     *
179
     * @param Encode $encode
180
     * @return void
181
     */
182
    public function propagateEncoding(Encode $encode)
183
    {
184
        $this->encode = $encode;
185
        $this->tag->setEncoding($encode);
186
    }
187
188
    /**
189
     * Checks if the given node id is an ancestor of
190
     * the current node.
191
     *
192
     * @param int $id
193
     * @return bool
194
     */
195
    public function isAncestor($id)
196
    {
197
        if ( ! is_null($this->getAncestor($id))) {
198
            return true;
199
        }
200
201
        return false;
202
    }
203
204
    /**
205
     * Attempts to get an ancestor node by the given id.
206
     *
207
     * @param int $id
208
     * @return null|AbstractNode
209
     */
210
    public function getAncestor($id)
211
    {
212
        if ( ! is_null($this->parent)) {
213
            if ($this->parent->id() == $id) {
214
                return $this->parent;
215
            }
216
217
            return $this->parent->getAncestor($id);
218
        }
219
220
        return null;
221
    }
222
223
    /**
224
     * Attempts to get the next sibling.
225
     *
226
     * @return AbstractNode
227
     * @throws ParentNotFoundException
228
     */
229
    public function nextSibling()
230
    {
231
        if (is_null($this->parent)) {
232
            throw new ParentNotFoundException('Parent is not set for this node.');
233
        }
234
235
        return $this->parent->nextChild($this->id);
236
    }
237
238
    /**
239
     * Attempts to get the previous sibling
240
     *
241
     * @return AbstractNode
242
     * @throws ParentNotFoundException
243
     */
244
    public function previousSibling()
245
    {
246
        if (is_null($this->parent)) {
247
            throw new ParentNotFoundException('Parent is not set for this node.');
248
        }
249
250
        return $this->parent->previousChild($this->id);
251
    }
252
253
    /**
254
     * Gets the tag object of this node.
255
     *
256
     * @return Tag
257
     */
258
    public function getTag()
259
    {
260
        return $this->tag;
261
    }
262
263
    /**
264
     * A wrapper method that simply calls the getAttribute method
265
     * on the tag of this node.
266
     *
267
     * @return array
268
     */
269
    public function getAttributes()
270
    {
271
        $attributes = $this->tag->getAttributes();
272
        foreach ($attributes as $name => $info) {
273
            $attributes[$name] = $info['value'];
274
        }
275
276
        return $attributes;
277
    }
278
279
    /**
280
     * A wrapper method that simply calls the getAttribute method
281
     * on the tag of this node.
282
     *
283
     * @param string $key
284
     * @return mixed
285
     */
286
    public function getAttribute($key)
287
    {
288
        $attribute = $this->tag->getAttribute($key);
289
        if ( ! is_null($attribute)) {
290
            $attribute = $attribute['value'];
291
        }
292
293
        return $attribute;
294
    }
295
296
    /**
297
     * A wrapper method that simply calls the hasAttribute method
298
     * on the tag of this node.
299
     *
300
     * @param string $key
301
     * @return bool
302
     */
303
    public function hasAttribute($key)
304
    {
305
        return $this->tag->hasAttribute($key);
306
    }
307
308
    /**
309
     * A wrapper method that simply calls the setAttribute method
310
     * on the tag of this node.
311
     *
312
     * @param string $key
313
     * @param string $value
314
     * @return $this
315
     */
316
    public function setAttribute($key, $value)
317
    {
318
        $this->tag->setAttribute($key, $value);
319
320
        return $this;
321
    }
322
323
    /**
324
     * A wrapper method that simply calls the removeAttribute method
325
     * on the tag of this node.
326
     *
327
     * @param string $key
328
     * @return void
329
     */
330
    public function removeAttribute($key)
331
    {
332
        $this->tag->removeAttribute($key);
333
    }
334
335
    /**
336
     * A wrapper method that simply calls the removeAllAttributes
337
     * method on the tag of this node.
338
     *
339
     * @return void
340
     */
341
    public function removeAllAttributes()
342
    {
343
        $this->tag->removeAllAttributes();
344
    }
345
346
    /**
347
     * Function to locate a specific ancestor tag in the path to the root.
348
     *
349
     * @param  string $tag
350
     * @return AbstractNode
351
     * @throws ParentNotFoundException
352
     */
353
    public function ancestorByTag($tag)
354
    {
355
        // Start by including ourselves in the comparison.
356
        $node = $this;
357
358
        while ( ! is_null($node)) {
359
            if ($node->tag->name() == $tag) {
360
                return $node;
361
            }
362
363
            $node = $node->getParent();
364
        }
365
366
        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
367
    }
368
369
    /**
370
     * Find elements by css selector
371
     *
372
     * @param string $selector
373
     * @param int $nth
374
     * @return array|AbstractNode
375
     */
376
    public function find($selector, $nth = null)
377
    {
378
        $selector = new Selector($selector);
379
        $nodes    = $selector->find($this);
380
381
        if ( ! is_null($nth)) {
382
            // return nth-element or array
383
            if (isset($nodes[$nth])) {
384
                return $nodes[$nth];
385
            }
386
387
            return null;
388
        }
389
390
        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...
391
    }
392
393
    /**
394
     * Gets the inner html of this node.
395
     *
396
     * @return string
397
     */
398
    abstract public function innerHtml();
399
400
    /**
401
     * Gets the html of this node, including it's own
402
     * tag.
403
     *
404
     * @return string
405
     */
406
    abstract public function outerHtml();
407
408
    /**
409
     * Gets the text of this node (if there is any text).
410
     *
411
     * @return string
412
     */
413
    abstract public function text();
414
415
    /**
416
     * Call this when something in the node tree has changed. Like a child has been added
417
     * or a parent has been changed.
418
     *
419
     * @return void
420
     */
421
    abstract protected function clear();
422
}
423