Completed
Push — master ( 05ef16...3f1b18 )
by Lars
02:53
created

SimpleHtmlDom::__get()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0131

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 17
ccs 13
cts 14
cp 0.9286
rs 8.8571
cc 6
eloc 14
nc 6
nop 1
crap 6.0131
1
<?php
2
3
namespace voku\helper;
4
5
use BadMethodCallException;
6
use DOMElement;
7
use DOMNode;
8
use RuntimeException;
9
10
/**
11
 * Class SimpleHtmlDom
12
 *
13
 * @package FastSimpleHTMLDom
14
 * @property string outertext Get dom node's outer html
15
 * @property string innertext Get dom node's inner html
16
 * @property string plaintext (read-only) Get dom node's plain text
17
 * @property string tag       (read-only) Get dom node name
18
 * @property string attr      (read-only) Get dom node attributes
19
 *
20
 * @method SimpleHtmlDomNode|SimpleHtmlDom|null children() children($idx = -1) Returns children of node
21
 * @method SimpleHtmlDom|null first_child() Returns the first child of node
22
 * @method SimpleHtmlDom|null last_child() Returns the last child of node
23
 * @method SimpleHtmlDom|null next_sibling() Returns the next sibling of node
24
 * @method SimpleHtmlDom|null prev_sibling() Returns the previous sibling of node
25
 * @method SimpleHtmlDom|null parent() Returns the parent of node
26
 * @method string outertext() Get dom node's outer html
27
 * @method string innertext() Get dom node's inner html
28
 */
29
class SimpleHtmlDom implements \IteratorAggregate
30
{
31
  /**
32
   * @var array
33
   */
34
  protected static $functionAliases = array(
35
      'children'     => 'childNodes',
36
      'first_child'  => 'firstChild',
37
      'last_child'   => 'lastChild',
38
      'next_sibling' => 'nextSibling',
39
      'prev_sibling' => 'previousSibling',
40
      'parent'       => 'parentNode',
41
      'outertext'    => 'html',
42
      'innertext'    => 'innerHtml',
43
  );
44
  /**
45
   * @var DOMElement
46
   */
47
  protected $node;
48
49
  /**
50
   * SimpleHtmlDom constructor.
51
   *
52
   * @param DOMNode $node
53
   */
54 68
  public function __construct(DOMNode $node)
55
  {
56 68
    $this->node = $node;
0 ignored issues
show
Documentation Bug introduced by
$node is of type object<DOMNode>, but the property $node was declared to be of type object<DOMElement>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
57 68
  }
58
59
  /**
60
   * @param $name
61
   * @param $arguments
62
   *
63
   * @return null|string|SimpleHtmlDom
64
   *
65
   */
66 8 View Code Duplication
  public function __call($name, $arguments)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
67
  {
68 8
    if (isset(self::$functionAliases[$name])) {
69 8
      return call_user_func_array(array($this, self::$functionAliases[$name]), $arguments);
70
    }
71
    throw new BadMethodCallException('Method does not exist');
72
  }
73
74
  /**
75
   * @param $name
76
   *
77
   * @return array|null|string
78
   */
79 23
  public function __get($name)
80
  {
81
    switch ($name) {
82 23
      case 'outertext':
83 12
        return $this->html();
84 18
      case 'innertext':
85 3
        return $this->innerHtml();
86 17
      case 'plaintext':
87 9
        return $this->text();
88 9
      case 'tag'      :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

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

Loading history...
89 4
        return $this->node->nodeName;
90 8
      case 'attr'     :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

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

Loading history...
91
        return $this->getAllAttributes();
92 8
      default         :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
93 8
        return $this->getAttribute($name);
94 8
    }
95
  }
96
97
  /**
98
   * @param string $selector
99
   * @param int    $idx
100
   *
101
   * @return SimpleHtmlDom|SimpleHtmlDomNode|null
102
   */
103 12
  public function __invoke($selector, $idx = null)
104
  {
105 12
    return $this->find($selector, $idx);
106
  }
107
108
  /**
109
   * @param $name
110
   *
111
   * @return bool
112
   */
113 1
  public function __isset($name)
114
  {
115
    switch ($name) {
116 1
      case 'outertext':
117 1
      case 'innertext':
118 1
      case 'plaintext':
119 1
      case 'tag'      :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

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

Loading history...
120
        return true;
121 1
      default         :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
122 1
        return $this->hasAttribute($name);
123 1
    }
124
  }
125
126
  /**
127
   * @param $name
128
   * @param $value
129
   *
130
   * @return SimpleHtmlDom
131
   */
132 8
  public function __set($name, $value)
133
  {
134
    switch ($name) {
135 8
      case 'outertext':
136 2
        return $this->replaceNode($value);
137 6
      case 'innertext':
138 3
        return $this->replaceChild($value);
139 5
      default         :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
140 5
        return $this->setAttribute($name, $value);
141 5
    }
142
  }
143
144
  /**
145
   * @return mixed
146
   */
147 1
  public function __toString()
148
  {
149 1
    return $this->html();
150
  }
151
152
  /**
153
   * @param $name
154
   *
155
   * @return SimpleHtmlDom
156
   */
157 1
  public function __unset($name)
158
  {
159 1
    return $this->setAttribute($name, null);
160
  }
161
162
  /**
163
   * Returns children of node
164
   *
165
   * @param int $idx
166
   *
167
   * @return SimpleHtmlDomNode|SimpleHtmlDom|null
168
   */
169 2
  public function childNodes($idx = -1)
170
  {
171 2
    $nodeList = $this->getIterator();
172
173 2
    if ($idx === -1) {
174 2
      return $nodeList;
175
    }
176
177 2
    if (isset($nodeList[$idx])) {
178 2
      return $nodeList[$idx];
179
    }
180
181 1
    return null;
182
  }
183
184
  /**
185
   * Find list of nodes with a CSS selector
186
   *
187
   * @param string $selector
188
   * @param int    $idx
189
   *
190
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
191
   */
192 26
  public function find($selector, $idx = null)
193
  {
194 26
    return $this->getHtmlDomParser()->find($selector, $idx);
195
  }
196
197
  /**
198
   * Returns the first child of node
199
   *
200
   * @return SimpleHtmlDom|null
201
   */
202 4
  public function firstChild()
203
  {
204 4
    $node = $this->node->firstChild;
205
206 4
    if ($node === null) {
207 1
      return null;
208
    }
209
210 4
    return new SimpleHtmlDom($node);
211
  }
212
213
  /**
214
   * Returns array of attributes
215
   *
216
   * @return array|null
217
   */
218 1
  public function getAllAttributes()
219
  {
220 1
    if ($this->node->hasAttributes()) {
221 1
      $attributes = array();
222 1
      foreach ($this->node->attributes as $attr) {
223 1
        $attributes[$attr->name] = $attr->value;
224 1
      }
225
226 1
      return $attributes;
227
    }
228
229 1
    return null;
230
  }
231
232
  /**
233
   * Return attribute value
234
   *
235
   * @param string $name
236
   *
237
   * @return string
238
   */
239 9
  public function getAttribute($name)
240
  {
241 9
    return $this->node->getAttribute($name);
242
  }
243
244
  /**
245
   * Return SimpleHtmlDom by id.
246
   *
247
   * @param string $id
248
   *
249
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
250
   */
251 1
  public function getElementById($id)
252
  {
253 1
    return $this->find("#$id", 0);
254
  }
255
256
  /**
257
   * Return SimpleHtmlDom by tag name.
258
   *
259
   * @param string $name
260
   *
261
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
262
   */
263 1
  public function getElementByTagName($name)
264
  {
265 1
    return $this->find($name, 0);
266
  }
267
268
  /**
269
   * Returns Elements by id
270
   *
271
   * @param string   $id
272
   * @param null|int $idx
273
   *
274
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
275
   */
276
  public function getElementsById($id, $idx = null)
277
  {
278
    return $this->find("#$id", $idx);
279
  }
280
281
  /**
282
   * Returns Elements by tag name
283
   *
284
   * @param string   $name
285
   * @param null|int $idx
286
   *
287
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
288
   */
289 1
  public function getElementsByTagName($name, $idx = null)
290
  {
291 1
    return $this->find($name, $idx);
292
  }
293
294
  /**
295
   * Create a new "HtmlDomParser"-object from the current context.
296
   *
297
   * @return HtmlDomParser
298
   */
299 41
  public function getHtmlDomParser()
300
  {
301 41
    return new HtmlDomParser($this);
302
  }
303
304
  /**
305
   * Retrieve an external iterator
306
   *
307
   * @link  http://php.net/manual/en/iteratoraggregate.getiterator.php
308
   * @return SimpleHtmlDomNode An instance of an object implementing <b>Iterator</b> or
309
   * <b>Traversable</b>
310
   */
311 2
  public function getIterator()
312
  {
313 2
    $elements = new SimpleHtmlDomNode();
314 2
    if ($this->node->hasChildNodes()) {
315 2
      foreach ($this->node->childNodes as $node) {
316 2
        $elements[] = new SimpleHtmlDom($node);
317 2
      }
318 2
    }
319
320 2
    return $elements;
321
  }
322
323
  /**
324
   * @return DOMNode
325
   */
326 42
  public function getNode()
327
  {
328 42
    return $this->node;
329
  }
330
331
  /**
332
   * Determine if an attribute exists on the element.
333
   *
334
   * @param $name
335
   *
336
   * @return bool
337
   */
338 1
  public function hasAttribute($name)
339
  {
340 1
    return $this->node->hasAttribute($name);
341
  }
342
343
  /**
344
   * Get dom node's outer html
345
   *
346
   * @return string
347
   */
348 12
  public function html()
349
  {
350 12
    return $this->getHtmlDomParser()->html();
351
  }
352
353
  /**
354
   * Get dom node's inner html
355
   *
356
   * @return string
357
   */
358 3
  public function innerHtml()
359
  {
360 3
    return $this->getHtmlDomParser()->innerHtml();
361
  }
362
363
  /**
364
   * Returns the last child of node
365
   *
366
   * @return SimpleHtmlDom|null
367
   */
368 4
  public function lastChild()
369
  {
370 4
    $node = $this->node->lastChild;
371
372 4
    if ($node === null) {
373 1
      return null;
374
    }
375
376 4
    return new SimpleHtmlDom($node);
377
  }
378
379
  /**
380
   * Returns the next sibling of node
381
   *
382
   * @return SimpleHtmlDom|null
383
   */
384 1
  public function nextSibling()
385
  {
386 1
    $node = $this->node->nextSibling;
387
388 1
    if ($node === null) {
389 1
      return null;
390
    }
391
392 1
    return new SimpleHtmlDom($node);
393
  }
394
395
  /**
396
   * Returns the parent of node
397
   *
398
   * @return SimpleHtmlDom
399
   */
400 1
  public function parentNode()
401
  {
402 1
    return new SimpleHtmlDom($this->node->parentNode);
403
  }
404
405
  /**
406
   * Returns the previous sibling of node
407
   *
408
   * @return SimpleHtmlDom|null
409
   */
410 1
  public function previousSibling()
411
  {
412 1
    $node = $this->node->previousSibling;
413
414 1
    if ($node === null) {
415 1
      return null;
416
    }
417
418 1
    return new SimpleHtmlDom($node);
419
  }
420
421
  /**
422
   * Replace child node
423
   *
424
   * @param $string
425
   *
426
   * @return $this
427
   */
428 3
  protected function replaceChild($string)
429
  {
430 3
    if (!empty($string)) {
431 3
      $newDocument = new HtmlDomParser($string);
432
433 3
      if ($newDocument->outertext != $string) {
434
        throw new RuntimeException("Not valid HTML fragment");
435
      }
436 3
    }
437
438 3
    foreach ($this->node->childNodes as $node) {
439 3
      $this->node->removeChild($node);
440 3
    }
441
442 3
    if (!empty($newDocument)) {
443
444 3
      $newDocument = $this->cleanHtmlWrapper($newDocument);
445
446 3
      $newNode = $this->node->ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
447
448 3
      $this->node->appendChild($newNode);
449 3
    }
450
451 3
    return $this;
452
  }
453
454
  /**
455
   * Replace this node
456
   *
457
   * @param $string
458
   *
459
   * @return $this
460
   */
461 2
  protected function replaceNode($string)
462
  {
463 2
    if (empty($string)) {
464 1
      $this->node->parentNode->removeChild($this->node);
465
466 1
      return null;
467
    }
468
469 2
    $newDocument = new HtmlDomParser($string);
470
471 2
    if ($newDocument->outertext != $string) {
472
      throw new RuntimeException("Not valid HTML fragment");
473
    }
474
475 2
    $newDocument = $this->cleanHtmlWrapper($newDocument);
476
477 2
    $newNode = $this->node->ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
478
479 2
    $this->node->parentNode->replaceChild($newNode, $this->node);
480 2
    $this->node = $newNode;
481
482 2
    return $this;
483
  }
484
485
  /**
486
   * @param HtmlDomParser $newDocument
487
   *
488
   * @return HtmlDomParser
489
   */
490 5
  protected function cleanHtmlWrapper(HtmlDomParser $newDocument)
491
  {
492 5
    if ($newDocument->getIsDOMDocumentCreatedWithoutHtml() === true) {
493
494
      // Remove doc-type node.
495 3
      $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
496
497
      // Remove html element, preserving child nodes.
498 3
      $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
499 3
      $fragment = $newDocument->getDocument()->createDocumentFragment();
500 3
      while ($html->childNodes->length > 0) {
501 3
        $fragment->appendChild($html->childNodes->item(0));
502 3
      }
503 3
      $html->parentNode->replaceChild($fragment, $html);
504
505
      // Remove body element, preserving child nodes.
506 3
      $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
507 3
      $fragment = $newDocument->getDocument()->createDocumentFragment();
508 3
      while ($body->childNodes->length > 0) {
509 3
        $fragment->appendChild($body->childNodes->item(0));
510 3
      }
511 3
      $body->parentNode->replaceChild($fragment, $body);
512
513
      // At this point DOMDocument still added a "<p>"-wrapper around our string,
514
      // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
515 3
      $this->changeElementName($newDocument->getDocument()->getElementsByTagName('p')->item(0), 'simpleHtmlDomP');
516 3
    }
517
518 5
    return $newDocument;
519
  }
520
521
  /**
522
   * change the name of a tag in a "DOMNode"
523
   *
524
   * @param DOMNode $node
525
   * @param string  $name
526
   *
527
   * @return DOMElement
528
   */
529 3
  protected function changeElementName(\DOMNode $node, $name) {
530 3
    $newnode = $node->ownerDocument->createElement($name);
531 3
    foreach ($node->childNodes as $child){
532 3
      $child = $node->ownerDocument->importNode($child, true);
533 3
      $newnode->appendChild($child);
534 3
    }
535 3
    foreach ($node->attributes as $attrName => $attrNode) {
536
      $newnode->setAttribute($attrName, $attrNode);
537 3
    }
538 3
    $newnode->ownerDocument->replaceChild($newnode, $node);
539
540 3
    return $newnode;
541
  }
542
543
  /**
544
   * Set attribute value
545
   *
546
   * @param $name
547
   * @param $value
548
   *
549
   * @return $this
550
   */
551 5
  public function setAttribute($name, $value)
552
  {
553 5
    if (empty($value)) {
554 1
      $this->node->removeAttribute($name);
555 1
    } else {
556 5
      $this->node->setAttribute($name, $value);
557
    }
558
559 5
    return $this;
560
  }
561
562
  /**
563
   * Get dom node's plain text
564
   *
565
   * @return string
566
   */
567 9
  public function text()
568
  {
569 9
    return $this->node->textContent;
570
  }
571
}
572