Completed
Push — master ( 5ed2e8...45d18c )
by Gilles
02:48
created

AbstractNode::removeAllAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 1
Metric Value
c 1
b 1
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
namespace PHPHtmlParser\Dom;
3
4
use PHPHtmlParser\Selector;
5
use PHPHtmlParser\Exceptions\CircularException;
6
use PHPHtmlParser\Exceptions\ParentNotFoundException;
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': $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...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
86
        }
87
88
        return null;
89
    }
90
91
    /**
92
     * Attempts to clear out any object references.
93
     */
94
    public function __destruct()
95
    {
96
        $this->tag      = null;
97
        $this->attr     = [];
98
        $this->parent   = null;
99
        $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...
100
    }
101
102
    /**
103
     * Simply calls the outer text method.
104
     *
105
     * @return string
106
     */
107
    public function __toString()
108
    {
109
        return $this->outerHtml();
110
    }
111
112
    /**
113
     * Returns the id of this object.
114
     */
115
    public function id()
116
    {
117
        return $this->id;
118
    }
119
120
    /**
121
     * Returns the parent of node.
122
     *
123
     * @return AbstractNode
124
     */
125
    public function getParent()
126
    {
127
        return $this->parent;
128
    }
129
130
    /**
131
     * Sets the parent node.
132
     *
133
     * @param InnerNode $parent
134
     * @return $this
135
     * @throws CircularException
136
     */
137
    public function setParent(InnerNode $parent)
138
    {
139
        // remove from old parent
140
        if ( ! is_null($this->parent)) {
141
            if ($this->parent->id() == $parent->id()) {
142
                // already the parent
143
                return $this;
144
            }
145
146
            $this->parent->removeChild($this->id);
147
        }
148
149
        $this->parent = $parent;
150
151
        // assign child to parent
152
        $this->parent->addChild($this);
153
154
        //clear any cache
155
        $this->clear();
156
157
        return $this;
158
    }
159
160
    /**
161
     * Removes this node and all its children from the
162
     * DOM tree.
163
     *
164
     * @return void
165
     */
166
    public function delete()
167
    {
168
        if ( ! is_null($this->parent)) {
169
            $this->parent->removeChild($this->id);
170
        }
171
172
        $this->parent = null;
173
    }
174
175
    /**
176
     * Sets the encoding class to this node.
177
     *
178
     * @param Encode $encode
179
     * @return void
180
     */
181
    public function propagateEncoding(Encode $encode)
182
    {
183
        $this->encode = $encode;
184
        $this->tag->setEncoding($encode);
185
    }
186
187
    /**
188
     * Checks if the given node id is an ancestor of
189
     * the current node.
190
     *
191
     * @param int $id
192
     * @return bool
193
     */
194
    public function isAncestor($id)
195
    {
196
        if ( ! is_null($this->getAncestor($id))) {
197
            return true;
198
        }
199
200
        return false;
201
    }
202
203
    /**
204
     * Attempts to get an ancestor node by the given id.
205
     *
206
     * @param int $id
207
     * @return null|AbstractNode
208
     */
209
    public function getAncestor($id)
210
    {
211
        if ( ! is_null($this->parent)) {
212
            if ($this->parent->id() == $id) {
213
                return $this->parent;
214
            }
215
216
            return $this->parent->getAncestor($id);
217
        }
218
219
        return null;
220
    }
221
222
    /**
223
     * Attempts to get the next sibling.
224
     *
225
     * @return AbstractNode
226
     * @throws ParentNotFoundException
227
     */
228
    public function nextSibling()
229
    {
230
        if (is_null($this->parent)) {
231
            throw new ParentNotFoundException('Parent is not set for this node.');
232
        }
233
234
        return $this->parent->nextChild($this->id);
235
    }
236
237
    /**
238
     * Attempts to get the previous sibling
239
     *
240
     * @return AbstractNode
241
     * @throws ParentNotFoundException
242
     */
243
    public function previousSibling()
244
    {
245
        if (is_null($this->parent)) {
246
            throw new ParentNotFoundException('Parent is not set for this node.');
247
        }
248
249
        return $this->parent->previousChild($this->id);
250
    }
251
252
    /**
253
     * Gets the tag object of this node.
254
     *
255
     * @return Tag
256
     */
257
    public function getTag()
258
    {
259
        return $this->tag;
260
    }
261
262
    /**
263
     * A wrapper method that simply calls the getAttribute method
264
     * on the tag of this node.
265
     *
266
     * @return array
267
     */
268
    public function getAttributes()
269
    {
270
        $attributes = $this->tag->getAttributes();
271
        foreach ($attributes as $name => $info) {
272
            $attributes[$name] = $info['value'];
273
        }
274
275
        return $attributes;
276
    }
277
278
    /**
279
     * A wrapper method that simply calls the getAttribute method
280
     * on the tag of this node.
281
     *
282
     * @param string $key
283
     * @return mixed
284
     */
285
    public function getAttribute($key)
286
    {
287
        $attribute = $this->tag->getAttribute($key);
288
        if ( ! is_null($attribute)) {
289
            $attribute = $attribute['value'];
290
        }
291
292
        return $attribute;
293
    }
294
295
    /**
296
     * A wrapper method that simply calls the setAttribute method
297
     * on the tag of this node.
298
     *
299
     * @param string $key
300
     * @param string $value
301
     * @return $this
302
     */
303
    public function setAttribute($key, $value)
304
    {
305
        $this->tag->setAttribute($key, $value);
306
307
        return $this;
308
    }
309
310
    /**
311
     * A wrapper method that simply calls the removeAttribute method
312
     * on the tag of this node.
313
     *
314
     * @param string $key
315
     * @return void
316
     */
317
    public function removeAttribute($key)
318
    {
319
        $this->tag->removeAttribute($key);
320
    }
321
322
    /**
323
     * A wrapper method that simply calls the removeAllAttributes
324
     * method on the tag of this node.
325
     *
326
     * @return void
327
     */
328
    public function removeAllAttributes()
329
    {
330
        $this->tag->removeAllAttributes();
331
    }
332
333
    /**
334
     * Function to locate a specific ancestor tag in the path to the root.
335
     *
336
     * @param  string $tag
337
     * @return AbstractNode
338
     * @throws ParentNotFoundException
339
     */
340
    public function ancestorByTag($tag)
341
    {
342
        // Start by including ourselves in the comparison.
343
        $node = $this;
344
345
        while ( ! is_null($node)) {
346
            if ($node->tag->name() == $tag) {
347
                return $node;
348
            }
349
350
            $node = $node->getParent();
351
        }
352
353
        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
354
    }
355
356
    /**
357
     * Find elements by css selector
358
     *
359
     * @param string $selector
360
     * @param int $nth
361
     * @return array|AbstractNode
362
     */
363
    public function find($selector, $nth = null)
364
    {
365
        $selector = new Selector($selector);
366
        $nodes    = $selector->find($this);
367
368
        if ( ! is_null($nth)) {
369
            // return nth-element or array
370
            if (isset($nodes[$nth])) {
371
                return $nodes[$nth];
372
            }
373
374
            return null;
375
        }
376
377
        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...
378
    }
379
380
    /**
381
     * Function to try a few tricks to determine the displayed size of an img on the page.
382
     * NOTE: This will ONLY work on an IMG tag. Returns FALSE on all other tag types.
383
     *
384
     * Future enhancement:
385
     * Look in the tag to see if there is a class or id specified that has a height or width attribute to it.
386
     *
387
     * Far future enhancement
388
     * Look at all the parent tags of this image to see if they specify a class or id that has an img selector that specifies a height or width
389
     * Note that in this case, the class or id will have the img sub-selector for it to apply to the image.
390
     *
391
     * ridiculously far future development
392
     * If the class or id is specified in a SEPARATE css file that's not on the page, go get it and do what we were just doing for the ones on the page.
393
     *
394
     * @author John Schlick
395
     * @return array an array containing the 'height' and 'width' of the image on the page or -1 if we can't figure it out.
396
     */
397
    public function get_display_size()
398
    {
399
        $width  = -1;
400
        $height = -1;
401
402
        if ($this->tag->name() != 'img') {
403
            return false;
404
        }
405
406
        // See if there is a height or width attribute in the tag itself.
407
        if ( ! is_null($this->tag->getAttribute('width'))) {
408
            $width = $this->tag->getAttribute('width');
409
        }
410
411
        if ( ! is_null($this->tag->getAttribute('height'))) {
412
            $height = $this->tag->getAttribute('height');
413
        }
414
415
        // Now look for an inline style.
416
        if ( ! is_null($this->tag->getAttribute('style'))) {
417
            // Thanks to user 'gnarf' from stackoverflow for this regular expression.
418
            $attributes = [];
419
            preg_match_all("/([\w-]+)\s*:\s*([^;]+)\s*;?/", $this->tag->getAttribute('style'), $matches,
420
                PREG_SET_ORDER);
421
            foreach ($matches as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
422
                $attributes[$match[1]] = $match[2];
423
            }
424
425
            $width = $this->getLength($attributes, $width, 'width');
426
            $height = $this->getLength($attributes, $width, 'height');
427
        }
428
429
        $result = [
430
            'height' => $height,
431
            'width'  => $width,
432
        ];
433
434
        return $result;
435
    }
436
437
    /**
438
     * If there is a length in the style attributes use it.
439
     *
440
     * @param array $attributes
441
     * @param int $length
442
     * @param string $key
443
     * @return int
444
     */
445
    protected function getLength(array $attributes, $length, $key)
446
    {
447
        if (isset($attributes[$key]) && $length == -1) {
448
            // check that the last two characters are px (pixels)
449
            if (strtolower(substr($attributes[$key], -2)) == 'px') {
450
                $proposed_length = substr($attributes[$key], 0, -2);
451
                // Now make sure that it's an integer and not something stupid.
452
                if (filter_var($proposed_length, FILTER_VALIDATE_INT)) {
453
                    $length = $proposed_length;
454
                }
455
            }
456
        }
457
458
        return $length;
459
    }
460
461
    /**
462
     * Gets the inner html of this node.
463
     *
464
     * @return string
465
     */
466
    abstract public function innerHtml();
467
468
    /**
469
     * Gets the html of this node, including it's own
470
     * tag.
471
     *
472
     * @return string
473
     */
474
    abstract public function outerHtml();
475
476
    /**
477
     * Gets the text of this node (if there is any text).
478
     *
479
     * @return string
480
     */
481
    abstract public function text();
482
483
    /**
484
     * Call this when something in the node tree has changed. Like a child has been added
485
     * or a parent has been changed.
486
     *
487
     * @return void
488
     */
489
    abstract protected function clear();
490
}
491