Completed
Push — master ( a85a91...44c366 )
by Lars
02:35
created

SimpleHtmlDom::changeElementName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 14
ccs 10
cts 11
cp 0.9091
rs 9.4285
cc 3
eloc 9
nc 4
nop 2
crap 3.0067
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 voku\helper
14
 *
15
 * @property string outertext Get dom node's outer html
16
 * @property string innertext Get dom node's inner html
17
 * @property string plaintext (read-only) Get dom node's plain text
18
 * @property string tag       (read-only) Get dom node name
19
 * @property string attr      (read-only) Get dom node attributes
20
 *
21
 * @method SimpleHtmlDomNode|SimpleHtmlDom|null children() children($idx = -1) Returns children of node
22
 * @method SimpleHtmlDom|null first_child() Returns the first child of node
23
 * @method SimpleHtmlDom|null last_child() Returns the last child of node
24
 * @method SimpleHtmlDom|null next_sibling() Returns the next sibling of node
25
 * @method SimpleHtmlDom|null prev_sibling() Returns the previous sibling of node
26
 * @method SimpleHtmlDom|null parent() Returns the parent of node
27
 * @method string outertext() Get dom node's outer html
28
 * @method string innertext() Get dom node's inner html
29
 */
30
class SimpleHtmlDom implements \IteratorAggregate
31
{
32
  /**
33
   * @var array
34
   */
35
  protected static $functionAliases = array(
36
      'children'     => 'childNodes',
37
      'first_child'  => 'firstChild',
38
      'last_child'   => 'lastChild',
39
      'next_sibling' => 'nextSibling',
40
      'prev_sibling' => 'previousSibling',
41
      'parent'       => 'parentNode',
42
      'outertext'    => 'html',
43
      'innertext'    => 'innerHtml',
44
  );
45
  /**
46
   * @var DOMElement
47
   */
48
  protected $node;
49
50
  /**
51
   * SimpleHtmlDom constructor.
52
   *
53
   * @param DOMNode $node
54
   */
55 69
  public function __construct(DOMNode $node)
56
  {
57 69
    $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...
58 69
  }
59
60
  /**
61
   * @param $name
62
   * @param $arguments
63
   *
64
   * @return null|string|SimpleHtmlDom
65
   *
66
   */
67 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...
68
  {
69 8
    if (isset(self::$functionAliases[$name])) {
70 8
      return call_user_func_array(array($this, self::$functionAliases[$name]), $arguments);
71
    }
72
    throw new BadMethodCallException('Method does not exist');
73
  }
74
75
  /**
76
   * @param $name
77
   *
78
   * @return array|null|string
79
   */
80 23
  public function __get($name)
81
  {
82
    switch ($name) {
83 23
      case 'outertext':
84 12
        return $this->html();
85 18
      case 'innertext':
86 3
        return $this->innerHtml();
87 17
      case 'plaintext':
88 9
        return $this->text();
89 9
      case 'tag':
90 4
        return $this->node->nodeName;
91 8
      case 'attr':
92
        return $this->getAllAttributes();
93 8
      default:
94 8
        return $this->getAttribute($name);
95 8
    }
96
  }
97
98
  /**
99
   * @param string $selector
100
   * @param int    $idx
101
   *
102
   * @return SimpleHtmlDom|SimpleHtmlDomNode|null
103
   */
104 12
  public function __invoke($selector, $idx = null)
105
  {
106 12
    return $this->find($selector, $idx);
107
  }
108
109
  /**
110
   * @param $name
111
   *
112
   * @return bool
113
   */
114 1 View Code Duplication
  public function __isset($name)
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...
115
  {
116
    switch ($name) {
117 1
      case 'outertext':
118 1
      case 'innertext':
119 1
      case 'plaintext':
120 1
      case 'tag':
121
        return true;
122 1
      default:
123 1
        return $this->hasAttribute($name);
124 1
    }
125
  }
126
127
  /**
128
   * @param $name
129
   * @param $value
130
   *
131
   * @return SimpleHtmlDom
132
   */
133 9
  public function __set($name, $value)
134
  {
135
    switch ($name) {
136 9
      case 'outertext':
137 3
        return $this->replaceNode($value);
138 6
      case 'innertext':
139 3
        return $this->replaceChild($value);
140 5
      default:
141 5
        return $this->setAttribute($name, $value);
142 5
    }
143
  }
144
145
  /**
146
   * @return string
147
   */
148 2
  public function __toString()
149
  {
150 2
    return $this->html();
151
  }
152
153
  /**
154
   * @param $name
155
   *
156
   * @return SimpleHtmlDom
157
   */
158 1
  public function __unset($name)
159
  {
160 1
    return $this->setAttribute($name, null);
161
  }
162
163
  /**
164
   * Returns children of node
165
   *
166
   * @param int $idx
167
   *
168
   * @return SimpleHtmlDomNode|SimpleHtmlDom|null
169
   */
170 2
  public function childNodes($idx = -1)
171
  {
172 2
    $nodeList = $this->getIterator();
173
174 2
    if ($idx === -1) {
175 2
      return $nodeList;
176
    }
177
178 2
    if (isset($nodeList[$idx])) {
179 2
      return $nodeList[$idx];
180
    }
181
182 1
    return null;
183
  }
184
185
  /**
186
   * Find list of nodes with a CSS selector
187
   *
188
   * @param string $selector
189
   * @param int    $idx
190
   *
191
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
192
   */
193 24
  public function find($selector, $idx = null)
194
  {
195 24
    return $this->getHtmlDomParser()->find($selector, $idx);
196
  }
197
198
  /**
199
   * Returns the first child of node
200
   *
201
   * @return SimpleHtmlDom|null
202
   */
203 4
  public function firstChild()
204
  {
205 4
    $node = $this->node->firstChild;
206
207 4
    if ($node === null) {
208 1
      return null;
209
    }
210
211 4
    return new self($node);
212
  }
213
214
  /**
215
   * Returns array of attributes
216
   *
217
   * @return array|null
218
   */
219 1
  public function getAllAttributes()
220
  {
221 1
    if ($this->node->hasAttributes()) {
222 1
      $attributes = array();
223 1
      foreach ($this->node->attributes as $attr) {
224 1
        $attributes[$attr->name] = $attr->value;
225 1
      }
226
227 1
      return $attributes;
228
    }
229
230 1
    return null;
231
  }
232
233
  /**
234
   * Return attribute value
235
   *
236
   * @param string $name
237
   *
238
   * @return string
239
   */
240 10
  public function getAttribute($name)
241
  {
242 10
    return $this->node->getAttribute($name);
243
  }
244
245
  /**
246
   * Return SimpleHtmlDom by id.
247
   *
248
   * @param string $id
249
   *
250
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
251
   */
252 1
  public function getElementById($id)
253
  {
254 1
    return $this->find("#$id", 0);
255
  }
256
257
  /**
258
   * Return SimpleHtmlDom by tag name.
259
   *
260
   * @param string $name
261
   *
262
   * @return SimpleHtmlDomNode|SimpleHtmlDomNodeBlank
263
   */
264 1
  public function getElementByTagName($name)
265
  {
266 1
    $node = $this->node->getElementsByTagName($name)->item(0);
267
268 1
    if ($node !== null) {
269 1
      return new self($node);
270
    } else {
271
      return new SimpleHtmlDomNodeBlank();
272
    }
273
  }
274
275
  /**
276
   * Returns Elements by id
277
   *
278
   * @param string   $id
279
   * @param null|int $idx
280
   *
281
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
282
   */
283
  public function getElementsById($id, $idx = null)
284
  {
285
    return $this->find("#$id", $idx);
286
  }
287
288
  /**
289
   * Returns Elements by tag name
290
   *
291
   * @param string   $name
292
   * @param null|int $idx
293
   *
294
   * @return SimpleHtmlDomNode|SimpleHtmlDomNode[]|SimpleHtmlDomNodeBlank
295
   */
296 1 View Code Duplication
  public function getElementsByTagName($name, $idx = null)
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...
297
  {
298 1
    $nodesList = $this->node->getElementsByTagName($name);
299
300 1
    $elements = new SimpleHtmlDomNode();
301
302 1
    foreach ($nodesList as $node) {
303 1
      $elements[] = new self($node);
304 1
    }
305
306 1
    if (null === $idx) {
307 1
      return $elements;
308
    } else {
309
      if ($idx < 0) {
310
        $idx = count($elements) + $idx;
311
      }
312
    }
313
314
    if (isset($elements[$idx])) {
315
      return $elements[$idx];
316
    } else {
317
      return new SimpleHtmlDomNodeBlank();
318
    }
319
  }
320
321
  /**
322
   * Create a new "HtmlDomParser"-object from the current context.
323
   *
324
   * @return HtmlDomParser
325
   */
326 40
  public function getHtmlDomParser()
327
  {
328 40
    return new HtmlDomParser($this);
329
  }
330
331
  /**
332
   * Retrieve an external iterator
333
   *
334
   * @link  http://php.net/manual/en/iteratoraggregate.getiterator.php
335
   * @return SimpleHtmlDomNode An instance of an object implementing <b>Iterator</b> or
336
   * <b>Traversable</b>
337
   */
338 2
  public function getIterator()
339
  {
340 2
    $elements = new SimpleHtmlDomNode();
341 2
    if ($this->node->hasChildNodes()) {
342 2
      foreach ($this->node->childNodes as $node) {
343 2
        $elements[] = new self($node);
344 2
      }
345 2
    }
346
347 2
    return $elements;
348
  }
349
350
  /**
351
   * @return DOMNode
352
   */
353 41
  public function getNode()
354
  {
355 41
    return $this->node;
356
  }
357
358
  /**
359
   * Determine if an attribute exists on the element.
360
   *
361
   * @param $name
362
   *
363
   * @return bool
364
   */
365 1
  public function hasAttribute($name)
366
  {
367 1
    return $this->node->hasAttribute($name);
368
  }
369
370
  /**
371
   * Get dom node's outer html
372
   *
373
   * @return string
374
   */
375 13
  public function html()
376
  {
377 13
    return $this->getHtmlDomParser()->html();
378
  }
379
380
  /**
381
   * Get dom node's inner html
382
   *
383
   * @return string
384
   */
385 3
  public function innerHtml()
386
  {
387 3
    return $this->getHtmlDomParser()->innerHtml();
388
  }
389
390
  /**
391
   * Returns the last child of node
392
   *
393
   * @return SimpleHtmlDom|null
394
   */
395 4
  public function lastChild()
396
  {
397 4
    $node = $this->node->lastChild;
398
399 4
    if ($node === null) {
400 1
      return null;
401
    }
402
403 4
    return new self($node);
404
  }
405
406
  /**
407
   * Returns the next sibling of node
408
   *
409
   * @return SimpleHtmlDom|null
410
   */
411 1
  public function nextSibling()
412
  {
413 1
    $node = $this->node->nextSibling;
414
415 1
    if ($node === null) {
416 1
      return null;
417
    }
418
419 1
    return new self($node);
420
  }
421
422
  /**
423
   * Returns the parent of node
424
   *
425
   * @return SimpleHtmlDom
426
   */
427 1
  public function parentNode()
428
  {
429 1
    return new self($this->node->parentNode);
430
  }
431
432
  /**
433
   * Returns the previous sibling of node
434
   *
435
   * @return SimpleHtmlDom|null
436
   */
437 1
  public function previousSibling()
438
  {
439 1
    $node = $this->node->previousSibling;
440
441 1
    if ($node === null) {
442 1
      return null;
443
    }
444
445 1
    return new self($node);
446
  }
447
448
  /**
449
   * Replace child node
450
   *
451
   * @param $string
452
   *
453
   * @return $this
454
   */
455 3
  protected function replaceChild($string)
456
  {
457 3
    if (!empty($string)) {
458 3
      $newDocument = new HtmlDomParser($string);
459
460 3
      if ($newDocument->outertext != $string) {
461
        throw new RuntimeException('Not valid HTML fragment');
462
      }
463 3
    }
464
465 3
    foreach ($this->node->childNodes as $node) {
466 3
      $this->node->removeChild($node);
467 3
    }
468
469 3
    if (!empty($newDocument)) {
470
471 3
      $newDocument = $this->cleanHtmlWrapper($newDocument);
472
473 3
      $newNode = $this->node->ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
474
475 3
      $this->node->appendChild($newNode);
476 3
    }
477
478 3
    return $this;
479
  }
480
481
  /**
482
   * Replace this node
483
   *
484
   * @param $string
485
   *
486
   * @return $this
487
   */
488 3
  protected function replaceNode($string)
489
  {
490 3
    if (empty($string)) {
491 1
      $this->node->parentNode->removeChild($this->node);
492
493 1
      return null;
494
    }
495
496 3
    $newDocument = new HtmlDomParser($string);
497
498 3
    if (str_replace(' ', '', $newDocument->outertext) !== str_replace(' ', '', $string)) {
499
      throw new RuntimeException('Not valid HTML fragment');
500
    }
501
502 3
    $newDocument = $this->cleanHtmlWrapper($newDocument);
503
504 3
    $newNode = $this->node->ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
505
506 3
    $this->node->parentNode->replaceChild($newNode, $this->node);
507 3
    $this->node = $newNode;
508
509 3
    return $this;
510
  }
511
512
  /**
513
   * @param HtmlDomParser $newDocument
514
   *
515
   * @return HtmlDomParser
516
   */
517 6
  protected function cleanHtmlWrapper(HtmlDomParser $newDocument)
518
  {
519 6
    if ($newDocument->getIsDOMDocumentCreatedWithoutHtml() === true) {
520
521
      // Remove doc-type node.
522 3
      $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
523
524
      // Remove html element, preserving child nodes.
525 3
      $html = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
526 3
      $fragment = $newDocument->getDocument()->createDocumentFragment();
527 3
      while ($html->childNodes->length > 0) {
528 3
        $fragment->appendChild($html->childNodes->item(0));
529 3
      }
530 3
      $html->parentNode->replaceChild($fragment, $html);
531
532
      // Remove body element, preserving child nodes.
533 3
      $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
534 3
      $fragment = $newDocument->getDocument()->createDocumentFragment();
535 3
      while ($body->childNodes->length > 0) {
536 3
        $fragment->appendChild($body->childNodes->item(0));
537 3
      }
538 3
      $body->parentNode->replaceChild($fragment, $body);
539
540
      // At this point DOMDocument still added a "<p>"-wrapper around our string,
541
      // so we replace it with "<simpleHtmlDomP>" and delete this at the ending ...
542 3
      $this->changeElementName($newDocument->getDocument()->getElementsByTagName('p')->item(0), 'simpleHtmlDomP');
543 3
    }
544
545 6
    return $newDocument;
546
  }
547
548
  /**
549
   * change the name of a tag in a "DOMNode"
550
   *
551
   * @param DOMNode $node
552
   * @param string  $name
553
   *
554
   * @return DOMElement
555
   */
556 3
  protected function changeElementName(\DOMNode $node, $name)
557
  {
558 3
    $newnode = $node->ownerDocument->createElement($name);
559 3
    foreach ($node->childNodes as $child) {
560 3
      $child = $node->ownerDocument->importNode($child, true);
561 3
      $newnode->appendChild($child);
562 3
    }
563 3
    foreach ($node->attributes as $attrName => $attrNode) {
564
      $newnode->setAttribute($attrName, $attrNode);
565 3
    }
566 3
    $newnode->ownerDocument->replaceChild($newnode, $node);
567
568 3
    return $newnode;
569
  }
570
571
  /**
572
   * Set attribute value
573
   *
574
   * @param $name
575
   * @param $value
576
   *
577
   * @return $this
578
   */
579 5
  public function setAttribute($name, $value)
580
  {
581 5
    if (empty($value)) {
582 1
      $this->node->removeAttribute($name);
583 1
    } else {
584 5
      $this->node->setAttribute($name, $value);
585
    }
586
587 5
    return $this;
588
  }
589
590
  /**
591
   * Get dom node's plain text
592
   *
593
   * @return string
594
   */
595 9
  public function text()
596
  {
597 9
    return $this->node->textContent;
598
  }
599
}
600