Completed
Push — master ( 966347...8b759a )
by Gilles
08:12
created

AbstractNode::__get()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.0145

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 14
cts 15
cp 0.9333
rs 8.6506
c 0
b 0
f 0
cc 7
nc 7
nop 1
crap 7.0145
1
<?php
2
namespace PHPHtmlParser\Dom;
3
4
use PHPHtmlParser\Exceptions\CircularException;
5
use PHPHtmlParser\Exceptions\ParentNotFoundException;
6
use PHPHtmlParser\Exceptions\ChildNotFoundException;
7
use PHPHtmlParser\Selector;
8
use stringEncode\Encode;
9
use PHPHtmlParser\Finder;
10
11
/**
12
 * Dom node object.
13
 *
14
 * @property string outerhtml
15
 * @property string innerhtml
16
 * @property string text
17
 * @property \PHPHtmlParser\Dom\Tag tag
18
 * @property InnerNode parent
19
 */
20
abstract class AbstractNode
21
{
22
    private static $count = 0;
23
    /**
24
     * Contains the tag name/type
25
     *
26
     * @var \PHPHtmlParser\Dom\Tag
27
     */
28
    protected $tag;
29
30
    /**
31
     * Contains a list of attributes on this tag.
32
     *
33
     * @var array
34
     */
35
    protected $attr = [];
36
37
    /**
38
     * Contains the parent Node.
39
     *
40
     * @var InnerNode
41
     */
42
    protected $parent = null;
43
44
    /**
45
     * The unique id of the class. Given by PHP.
46
     *
47
     * @var string
48
     */
49
    protected $id;
50
51
    /**
52
     * The encoding class used to encode strings.
53
     *
54
     * @var mixed
55
     */
56
    protected $encode;
57
58
    /**
59
     * Creates a unique id for this node.
60
     */
61 453
    public function __construct()
62
    {
63 453
        $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...
64 453
        self::$count++;
65 453
    }
66
67
    /**
68
     * Magic get method for attributes and certain methods.
69
     *
70
     * @param string $key
71
     * @return mixed
72
     */
73 171
    public function __get(string $key)
74
    {
75
        // check attribute first
76 171
        if ( ! is_null($this->getAttribute($key))) {
77 39
            return $this->getAttribute($key);
78
        }
79 141
        switch (strtolower($key)) {
80 141
            case 'outerhtml':
81 33
                return $this->outerHtml();
82 108
            case 'innerhtml':
83 54
                return $this->innerHtml();
84 57
            case 'text':
85 51
                return $this->text();
86 6
            case 'tag':
87 3
                return $this->getTag();
88 3
            case 'parent':
89
                return $this->getParent();
90
        }
91
92 3
        return null;
93
    }
94
95
    /**
96
     * Attempts to clear out any object references.
97
     */
98 126
    public function __destruct()
99
    {
100 126
        $this->tag      = null;
101 126
        $this->attr     = [];
102 126
        $this->parent   = null;
103 126
        $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...
104 126
    }
105
106
    /**
107
     * Simply calls the outer text method.
108
     *
109
     * @return string
110
     */
111 6
    public function __toString()
112
    {
113 6
        return $this->outerHtml();
114
    }
115
116
    /**
117
     * Reset node counter
118
     *
119
     * @return void
120
     */
121 144
    public static function resetCount()
122
    {
123 144
        self::$count = 0;
124 144
    }
125
126
    /**
127
     * Returns the id of this object.
128
     *
129
     * @return int
130
     */
131 387
    public function id(): int
132
    {
133 387
        return $this->id;
134
    }
135
136
    /**
137
     * Returns the parent of node.
138
     *
139
     * @return AbstractNode
140
     */
141 198
    public function getParent()
142
    {
143 198
        return $this->parent;
144
    }
145
146
    /**
147
     * Sets the parent node.
148
     *
149
     * @param InnerNode $parent
150
     * @return AbstractNode
151
     * @throws CircularException
152
     * @chainable
153
     */
154 381
    public function setParent(InnerNode $parent): AbstractNode
155
    {
156
        // remove from old parent
157 381
        if ( ! is_null($this->parent)) {
158 30
            if ($this->parent->id() == $parent->id()) {
159
                // already the parent
160 27
                return $this;
161
            }
162
163 6
            $this->parent->removeChild($this->id);
164
        }
165
166 381
        $this->parent = $parent;
167
168
        // assign child to parent
169 381
        $this->parent->addChild($this);
170
171 381
        return $this;
172
    }
173
174
    /**
175
     * Removes this node and all its children from the
176
     * DOM tree.
177
     *
178
     * @return void
179
     */
180 3
    public function delete()
181
    {
182 3
        if ( ! is_null($this->parent)) {
183 3
            $this->parent->removeChild($this->id);
184
        }
185 3
        $this->parent->clear();
186 3
        $this->clear();
187 3
    }
188
189
    /**
190
     * Sets the encoding class to this node.
191
     *
192
     * @param Encode $encode
193
     * @return void
194
     */
195 186
    public function propagateEncoding(Encode $encode)
196
    {
197 186
        $this->encode = $encode;
198 186
        $this->tag->setEncoding($encode);
199 186
    }
200
201
    /**
202
     * Checks if the given node id is an ancestor of
203
     * the current node.
204
     *
205
     * @param int $id
206
     * @return bool
207
     */
208 384
    public function isAncestor(int $id): Bool
209
    {
210 384
        if ( ! is_null($this->getAncestor($id))) {
211 6
            return true;
212
        }
213
214 384
        return false;
215
    }
216
217
    /**
218
     * Attempts to get an ancestor node by the given id.
219
     *
220
     * @param int $id
221
     * @return null|AbstractNode
222
     */
223 387
    public function getAncestor(int $id)
224
    {
225 387
        if ( ! is_null($this->parent)) {
226 267
            if ($this->parent->id() == $id) {
227 12
                return $this->parent;
228
            }
229
230 258
            return $this->parent->getAncestor($id);
231
        }
232
233 387
        return null;
234
    }
235
236
    /**
237
     * Checks if the current node has a next sibling.
238
     *
239
     * @return bool
240
     */
241 9
    public function hasNextSibling(): bool
242
    {
243
        try
244
        {
245 9
            $sibling = $this->nextSibling();
0 ignored issues
show
Unused Code introduced by
$sibling is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
246
            // sibling found, return true;
247 9
            return true;
248
        }
249 9
        catch (ParentNotFoundException $e)
250
        {
251
            // no parent, no next sibling
252
            return false;
253
        }
254 9
        catch (ChildNotFoundException $e)
255
        {
256
            // no sibling found
257 9
            return false;
258
        }
259
    }
260
261
    /**
262
     * Attempts to get the next sibling.
263
     *
264
     * @return AbstractNode
265
     * @throws ParentNotFoundException
266
     */
267 39
    public function nextSibling(): AbstractNode
268
    {
269 39
        if (is_null($this->parent)) {
270 3
            throw new ParentNotFoundException('Parent is not set for this node.');
271
        }
272
273 36
        return $this->parent->nextChild($this->id);
274
    }
275
276
    /**
277
     * Attempts to get the previous sibling
278
     *
279
     * @return AbstractNode
280
     * @throws ParentNotFoundException
281
     */
282 9
    public function previousSibling(): AbstractNode
283
    {
284 9
        if (is_null($this->parent)) {
285 3
            throw new ParentNotFoundException('Parent is not set for this node.');
286
        }
287
288 6
        return $this->parent->previousChild($this->id);
289
    }
290
291
    /**
292
     * Gets the tag object of this node.
293
     *
294
     * @return Tag
295
     */
296 258
    public function getTag(): Tag
297
    {
298 258
        return $this->tag;
299
    }
300
301
    /**
302
     * A wrapper method that simply calls the getAttribute method
303
     * on the tag of this node.
304
     *
305
     * @return array
306
     */
307 6
    public function getAttributes(): array
308
    {
309 6
        $attributes = $this->tag->getAttributes();
310 6
        foreach ($attributes as $name => $info) {
311 3
            $attributes[$name] = $info['value'];
312
        }
313
314 6
        return $attributes;
315
    }
316
317
    /**
318
     * A wrapper method that simply calls the getAttribute method
319
     * on the tag of this node.
320
     *
321
     * @param string $key
322
     * @return mixed
323
     */
324 195
    public function getAttribute(string $key)
325
    {
326 195
        $attribute = $this->tag->getAttribute($key);
327 195
        if ( ! is_null($attribute)) {
328 99
            $attribute = $attribute['value'];
329
        }
330
331 195
        return $attribute;
332
    }
333
334
    /**
335
     * A wrapper method that simply calls the hasAttribute method
336
     * on the tag of this node.
337
     *
338
     * @param string $key
339
     * @return bool
340
     */
341 84
    public function hasAttribute(string $key): bool
342
    {
343 84
        return $this->tag->hasAttribute($key);
344
    }
345
346
    /**
347
     * A wrapper method that simply calls the setAttribute method
348
     * on the tag of this node.
349
     *
350
     * @param string $key
351
     * @param string|null $value
352
     * @return AbstractNode
353
     * @chainable
354
     */
355 21
    public function setAttribute(string $key, $value): AbstractNode
356
    {
357 21
        $this->tag->setAttribute($key, $value);
358
359
        //clear any cache
360 21
        $this->clear();
361
362 21
        return $this;
363
    }
364
365
    /**
366
     * A wrapper method that simply calls the removeAttribute method
367
     * on the tag of this node.
368
     *
369
     * @param string $key
370
     * @return void
371
     */
372 3
    public function removeAttribute(string $key): void
373
    {
374 3
        $this->tag->removeAttribute($key);
375
376
        //clear any cache
377 3
        $this->clear();
378 3
    }
379
380
    /**
381
     * A wrapper method that simply calls the removeAllAttributes
382
     * method on the tag of this node.
383
     *
384
     * @return void
385
     */
386 3
    public function removeAllAttributes(): void
387
    {
388 3
        $this->tag->removeAllAttributes();
389
390
        //clear any cache
391 3
        $this->clear();
392 3
    }
393
    /**
394
     * Function to locate a specific ancestor tag in the path to the root.
395
     *
396
     * @param  string $tag
397
     * @return AbstractNode
398
     * @throws ParentNotFoundException
399
     */
400 6
    public function ancestorByTag(string $tag): AbstractNode
401
    {
402
        // Start by including ourselves in the comparison.
403 6
        $node = $this;
404
405 6
        while ( ! is_null($node)) {
406 6
            if ($node->tag->name() == $tag) {
407 3
                return $node;
408
            }
409
410 3
            $node = $node->getParent();
411
        }
412
413 3
        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
414
    }
415
416
    /**
417
     * Find elements by css selector
418
     *
419
     * @param string $selector
420
     * @param int $nth
421
     * @return array|AbstractNode
422
     */
423 186
    public function find(string $selector, int $nth = null)
424
    {
425 186
        $selector = new Selector($selector);
426 186
        $nodes    = $selector->find($this);
427
428 186
        if ( ! is_null($nth)) {
429
            // return nth-element or array
430 186
            if (isset($nodes[$nth])) {
431 114
                return $nodes[$nth];
432
            }
433
434 156
            return null;
435
        }
436
437 51
        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...
438
    }
439
440
    /**
441
     * Find node by id
442
     *
443
     * @param int $id
444
     * @return bool|AbstractNode
445
     */
446 9
    public function findById(int $id)
447
    {
448 9
        $finder= new Finder($id);
449
450 9
        return $finder->find($this);
451
    }
452
453
454
    /**
455
     * Gets the inner html of this node.
456
     *
457
     * @return string
458
     */
459
    abstract public function innerHtml(): string;
460
461
    /**
462
     * Gets the html of this node, including it's own
463
     * tag.
464
     *
465
     * @return string
466
     */
467
    abstract public function outerHtml(): string;
468
469
    /**
470
     * Gets the text of this node (if there is any text).
471
     *
472
     * @return string
473
     */
474
    abstract public function text(): string;
475
476
    /**
477
     * Call this when something in the node tree has changed. Like a child has been added
478
     * or a parent has been changed.
479
     *
480
     * @return void
481
     */
482
    abstract protected function clear(): void;
483
484
    /**
485
     * Check is node type textNode
486
     *
487
     * @return boolean
488
     */
489 9
    public function isTextNode(): bool 
490
    {
491
492 9
        return false;
493
    }
494
}
495