GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#19)
by
unknown
03:19
created

HtmlPageCrawler::makeClone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Wa72\HtmlPageDom;
4
5
use Symfony\Component\DomCrawler\Crawler;
6
7
/**
8
 * Extends \Symfony\Component\DomCrawler\Crawler by adding tree manipulation functions
9
 * for HTML documents inspired by jQuery such as html(), css(), append(), prepend(), before(),
10
 * addClass(), removeClass()
11
 *
12
 * @author Christoph Singer
13
 * @license MIT
14
 *
15
 */
16
class HtmlPageCrawler extends Crawler
0 ignored issues
show
Complexity introduced by
This class has 1086 lines of code which exceeds the configured maximum of 1000.

Really long classes often contain too much logic and violate the single responsibility principle.

We suggest to take a look at the “Code” section for options on how to refactor this code.

Loading history...
Complexity introduced by
This class has 50 public methods and attributes which exceeds the configured maximum of 45.

The number of this metric differs depending on the chosen design (inheritance vs. composition). For inheritance, the number should generally be a bit lower.

A high number indicates a reusable class. It might also make the class harder to change without breaking other classes though.

Loading history...
Complexity introduced by
This class has a complexity of 159 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

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

Loading history...
17
{
18
    /**
19
     * the (internal) root element name used when importing html fragments
20
     * */
21
    const FRAGMENT_ROOT_TAGNAME = '_root';
22
23
    /**
24
     * Get an HtmlPageCrawler object from a HTML string, DOMNode, DOMNodeList or HtmlPageCrawler
25
     *
26
     * This is the equivalent to jQuery's $() function when used for wrapping DOMNodes or creating DOMElements from HTML code.
27
     *
28
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|array $content
29
     * @return HtmlPageCrawler
30
     * @api
31
     */
32 11
    public static function create($content)
33
    {
34 11
        if ($content instanceof HtmlPageCrawler) {
35 3
            return $content;
36
        } else {
37 11
            return new HtmlPageCrawler($content);
38
        }
39
    }
40
41
    /**
42
     * Adds the specified class(es) to each element in the set of matched elements.
43
     *
44
     * @param string $name One or more space-separated classes to be added to the class attribute of each matched element.
45
     * @return HtmlPageCrawler $this for chaining
46
     * @api
47
     */
48 1
    public function addClass($name)
49
    {
50 1
        foreach ($this as $node) {
51 1
            if ($node instanceof \DOMElement) {
52
                /** @var \DOMElement $node */
53 1
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
54 1
                $found = false;
55 1
                $count = count($classes);
56 1
                for ($i = 0; $i < $count; $i++) {
57 1
                    if ($classes[$i] == $name) {
58
                        $found = true;
59
                    }
60 1
                }
61 1
                if (!$found) {
62 1
                    $classes[] = $name;
63 1
                    $node->setAttribute('class', trim(join(' ', $classes)));
64 1
                }
65 1
            }
66 1
        }
67 1
        return $this;
68
    }
69
70
    /**
71
     * Insert content, specified by the parameter, after each element in the set of matched elements.
72
     *
73
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
74
     * @return HtmlPageCrawler $this for chaining
75
     * @api
76
     */
77 3
    public function after($content)
78
    {
79 3
        $content = self::create($content);
80 3
        $newnodes = array();
81 3
        foreach ($this as $i => $node) {
82
            /** @var \DOMNode $node */
83 3
            $refnode = $node->nextSibling;
84 3
            foreach ($content as $newnode) {
85
                /** @var \DOMNode $newnode */
86 3
                $newnode = static::importNewnode($newnode, $node, $i);
87 3
                if ($refnode === null) {
88 3
                    $node->parentNode->appendChild($newnode);
89 3
                } else {
90
                    $node->parentNode->insertBefore($newnode, $refnode);
91
                }
92 3
                $newnodes[] = $newnode;
93 3
            }
94 3
        }
95 3
        $content->clear();
96 3
        $content->add($newnodes);
97 3
        return $this;
98
    }
99
100
    /**
101
     * Insert HTML content as child nodes of each element after existing children
102
     *
103
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment or DOMNode to append
104
     * @return HtmlPageCrawler $this for chaining
105
     * @api
106
     */
107 2
    public function append($content)
108
    {
109 2
        $content = self::create($content);
110 2
        $newnodes = array();
111 2
        foreach ($this as $i => $node) {
112
            /** @var \DOMNode $node */
113 2
            foreach ($content as $newnode) {
114
                /** @var \DOMNode $newnode */
115 2
                $newnode = static::importNewnode($newnode, $node, $i);
116
//                if ($newnode->ownerDocument !== $node->ownerDocument) {
117
//                    $newnode = $node->ownerDocument->importNode($newnode, true);
118
//                } else {
119
//                    if ($i > 0) {
120
//                        $newnode = $newnode->cloneNode(true);
121
//                    }
122
//                }
123 2
                $node->appendChild($newnode);
124 2
                $newnodes[] = $newnode;
125 2
            }
126 2
        }
127 2
        $content->clear();
128 2
        $content->add($newnodes);
129 2
        return $this;
130
    }
131
132
    /**
133
     * Insert every element in the set of matched elements to the end of the target.
134
     *
135
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
136
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
137
     * @api
138
     */
139 1
    public function appendTo($element)
140
    {
141 1
        $e = self::create($element);
142 1
        $newnodes = array();
143 1
        foreach ($e as $i => $node) {
144
            /** @var \DOMNode $node */
145 1
            foreach ($this as $newnode) {
146
                /** @var \DOMNode $newnode */
147 1
                $newnode = static::importNewnode($newnode, $node, $i);
148 1
                $node->appendChild($newnode);
149 1
                $newnodes[] = $newnode;
150 1
            }
151 1
        }
152 1
        return self::create($newnodes);
153
    }
154
155
    /**
156
     * Returns the attribute value of the first node of the list, or sets an attribute on each element
157
     *
158
     * @see HtmlPageCrawler::getAttribute()
159
     * @see HtmlPageCrawler::setAttribute
160
     *
161
     * @param string $name
162
     * @param null|string $value
163
     * @return null|string|HtmlPageCrawler
164
     * @api
165
     */
166 1
    public function attr($name, $value = null)
167
    {
168 1
        if ($value === null) {
169 1
            return $this->getAttribute($name);
170
        } else {
171 1
            return $this->setAttribute($name, $value);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->setAttribute($name, $value); (Wa72\HtmlPageDom\HtmlPageCrawler) is incompatible with the return type of the parent method Symfony\Component\DomCrawler\Crawler::attr of type string|null.

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...
172
        }
173
    }
174
175
    /**
176
     * Sets an attribute on each element
177
     *
178
     * @param string $name
179
     * @param string $value
180
     * @return HtmlPageCrawler $this for chaining
181
     */
182 2
    public function setAttribute($name, $value)
183
    {
184 2
        foreach ($this as $node) {
185 2
            if ($node instanceof \DOMElement) {
186
                /** @var \DOMElement $node */
187 2
                $node->setAttribute($name, $value);
188 2
            }
189 2
        }
190 2
        return $this;
191
    }
192
193
    /**
194
     * Returns the attribute value of the first node of the list.
195
     *
196
     * @param string $name The attribute name
197
     * @return string|null The attribute value or null if the attribute does not exist
198
     * @throws \InvalidArgumentException When current node is empty
199
     *
200
     */
201 1
    public function getAttribute($name)
202
    {
203 1
        if (!count($this)) {
204
            throw new \InvalidArgumentException('The current node list is empty.');
205
        }
206 1
        $node = $this->getNode(0);
207 1
        return $node->hasAttribute($name) ? $node->getAttribute($name) : null;
208
    }
209
210
    /**
211
     * Insert content, specified by the parameter, before each element in the set of matched elements.
212
     *
213
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
214
     * @return HtmlPageCrawler $this for chaining
215
     * @api
216
     */
217 2
    public function before($content)
218
    {
219 2
        $content = self::create($content);
220 2
        $newnodes = array();
221 2
        foreach ($this as $i => $node) {
222
            /** @var \DOMNode $node */
223 2
            foreach ($content as $newnode) {
224
                /** @var \DOMNode $newnode */
225 2
                $newnode = static::importNewnode($newnode, $node, $i);
226 2
                $node->parentNode->insertBefore($newnode, $node);
227 2
                $newnodes[] = $newnode;
228 2
            }
229 2
        }
230 2
        $content->clear();
231 2
        $content->add($newnodes);
232 2
        return $this;
233
    }
234
235
    /**
236
     * Create a deep copy of the set of matched elements.
237
     *
238
     * Equivalent to clone() in jQuery (clone is not a valid PHP function name)
239
     *
240
     * @return HtmlPageCrawler
241
     * @api
242
     */
243 1
    public function makeClone()
244
    {
245 1
        return clone $this;
246
    }
247
248 1
    public function __clone()
249
    {
250 1
        $newnodes = array();
251 1
        foreach ($this as $node) {
252
            /** @var \DOMNode $node */
253 1
            $newnodes[] = $node->cloneNode(true);
254 1
        }
255 1
        $this->clear();
256 1
        $this->add($newnodes);
257 1
    }
258
259
    /**
260
     * Get one CSS style property of the first element or set it for all elements in the list
261
     *
262
     * Function is here for compatibility with jQuery; it is the same as getStyle() and setStyle()
263
     *
264
     * @see HtmlPageCrawler::getStyle()
265
     * @see HtmlPageCrawler::setStyle()
266
     *
267
     * @param string $key The name of the style property
268
     * @param null|string $value The CSS value to set, or NULL to get the current value
269
     * @return HtmlPageCrawler|string If no param is provided, returns the CSS styles of the first element
270
     * @api
271
     */
272 1
    public function css($key, $value = null)
273
    {
274 1
        if (null === $value) {
275 1
            return $this->getStyle($key);
276
        } else {
277 1
            return $this->setStyle($key, $value);
278
        }
279
    }
280
281
    /**
282
     * get one CSS style property of the first element
283
     *
284
     * @param string $key name of the property
285
     * @return string|null value of the property
286
     */
287 1
    public function getStyle($key)
288
    {
289 1
        $styles = Helpers::cssStringToArray($this->getAttribute('style'));
290 1
        return (isset($styles[$key]) ? $styles[$key] : null);
291
    }
292
293
    /**
294
     * set one CSS style property for all elements in the list
295
     *
296
     * @param string $key name of the property
297
     * @param string $value value of the property
298
     * @return HtmlPageCrawler $this for chaining
299
     */
300 1
    public function setStyle($key, $value)
301
    {
302 1
        foreach ($this as $node) {
303 1
            if ($node instanceof \DOMElement) {
304
                /** @var \DOMElement $node */
305 1
                $styles = Helpers::cssStringToArray($node->getAttribute('style'));
306 1
                if ($value != '') {
307 1
                    $styles[$key] = $value;
308 1
                } elseif (isset($styles[$key])) {
309 1
                    unset($styles[$key]);
310 1
                }
311 1
                $node->setAttribute('style', Helpers::cssArrayToString($styles));
312 1
            }
313 1
        }
314 1
        return $this;
315
    }
316
317
    /**
318
     * Removes all child nodes and text from all nodes in set
319
     *
320
     * Equivalent to jQuery's empty() function which is not a valid function name in PHP
321
     * @return HtmlPageCrawler $this
322
     * @api
323
     */
324
    public function makeEmpty()
325
    {
326
        foreach ($this as $node) {
327
            $node->nodeValue = '';
328
        }
329
        return $this;
330
    }
331
332
    /**
333
     * Determine whether any of the matched elements are assigned the given class.
334
     *
335
     * @param string $name
336
     * @return bool
337
     * @api
338
     */
339 2
    public function hasClass($name)
340
    {
341 2
        foreach ($this as $node) {
342 2
            if ($node instanceof \DOMElement && $class = $node->getAttribute('class')) {
343 2
                $classes = preg_split('/\s+/s', $class);
344 2
                if (in_array($name, $classes)) {
345 2
                    return true;
346
                }
347 1
            }
348 2
        }
349 2
        return false;
350
    }
351
352
    /**
353
     * Get the HTML contents of the first element in the set of matched elements
354
     * or set the HTML contents of every matched element.
355
     *
356
     * Function is here for compatibility with jQuery: When called with a parameter, it is
357
     * equivalent to setInnerHtml(), without parameter it is the same as getInnerHtml()
358
     *
359
     * @see HtmlPageCrawler::setInnerHtml()
360
     * @see HtmlPageCrawler::getInnerHtml()
361
     *
362
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|null $html The HTML content to set, or NULL to get the current content
363
     *
364
     * @return HtmlPageCrawler|string If no param is provided, returns the HTML content of the first element
365
     * @api
366
     */
367
    public function html($html = null)
368
    {
369
        if (null === $html) {
370
            return $this->getInnerHtml();
371
        } else {
372
            $this->setInnerHtml($html);
373
            return $this;
374
        }
375
    }
376
377
    /**
378
     * Get the innerHTML contents of the first element
379
     *
380
     * @return string HTML code fragment
381
     */
382 2
    public function getInnerHtml()
383
    {
384 2
        return parent::html();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (html() instead of getInnerHtml()). Are you sure this is correct? If so, you might want to change this to $this->html().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
385
    }
386
387
    /**
388
     * Set the HTML contents of each element
389
     *
390
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
391
     * @return HtmlPageCrawler $this for chaining
392
     */
393 1
    public function setInnerHtml($content)
394
    {
395 1
        $content = self::create($content);
396 1
        foreach ($this as $node) {
397 1
            $node->nodeValue = '';
398 1
            foreach ($content as $newnode) {
399
                /** @var \DOMNode $node */
400
                /** @var \DOMNode $newnode */
401 1
                $newnode = static::importNewnode($newnode, $node);
402 1
                $node->appendChild($newnode);
403 1
            }
404 1
        }
405 1
        return $this;
406
    }
407
408
    /**
409
     * Insert every element in the set of matched elements after the target.
410
     *
411
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
412
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
413
     * @api
414
     */
415 1
    public function insertAfter($element)
416
    {
417 1
        $e = self::create($element);
418 1
        $newnodes = array();
419 1
        foreach ($e as $i => $node) {
420
            /** @var \DOMNode $node */
421 1
            $refnode = $node->nextSibling;
422 1
            foreach ($this as $newnode) {
423
                /** @var \DOMNode $newnode */
424 1
                $newnode = static::importNewnode($newnode, $node, $i);
425 1
                if ($refnode === null) {
426 1
                    $node->parentNode->appendChild($newnode);
427 1
                } else {
428 1
                    $node->parentNode->insertBefore($newnode, $refnode);
429
                }
430 1
                $newnodes[] = $newnode;
431 1
            }
432 1
        }
433 1
        return self::create($newnodes);
434
    }
435
436
    /**
437
     * Insert every element in the set of matched elements before the target.
438
     *
439
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
440
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
441
     * @api
442
     */
443 1
    public function insertBefore($element)
444
    {
445 1
        $e = self::create($element);
446 1
        $newnodes = array();
447 1
        foreach ($e as $i => $node) {
448
            /** @var \DOMNode $node */
449 1
            foreach ($this as $newnode) {
450
                /** @var \DOMNode $newnode */
451 1
                $newnode = static::importNewnode($newnode, $node, $i);
452 1
                $node->parentNode->insertBefore($newnode, $node);
453 1
                $newnodes[] = $newnode;
454 1
            }
455 1
        }
456 1
        return self::create($newnodes);
457
    }
458
459
    /**
460
     * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements.
461
     *
462
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
463
     * @return HtmlPageCrawler $this for chaining
464
     * @api
465
     */
466 1
    public function prepend($content)
467
    {
468 1
        $content = self::create($content);
469 1
        $newnodes = array();
470 1
        foreach ($this as $i => $node) {
471 1
            $refnode = $node->firstChild;
472
            /** @var \DOMNode $node */
473 1
            foreach ($content as $newnode) {
474
                /** @var \DOMNode $newnode */
475 1
                $newnode = static::importNewnode($newnode, $node, $i);
476 1
                if ($refnode === null) {
477
                    $node->appendChild($newnode);
478
                } else {
479 1
                    $node->insertBefore($newnode, $refnode);
480
                }
481 1
                $newnodes[] = $newnode;
482 1
            }
483 1
        }
484 1
        $content->clear();
485 1
        $content->add($newnodes);
486 1
        return $this;
487
    }
488
489
    /**
490
     * Insert every element in the set of matched elements to the beginning of the target.
491
     *
492
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
493
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements prepended to the target elements
494
     * @api
495
     */
496
    public function prependTo($element)
497
    {
498
        $e = self::create($element);
499
        $newnodes = array();
500
        foreach ($e as $i => $node) {
501
            $refnode = $node->firstChild;
502
            /** @var \DOMNode $node */
503
            foreach ($this as $newnode) {
504
                /** @var \DOMNode $newnode */
505
                $newnode = static::importNewnode($newnode, $node, $i);
506
                if ($refnode === null) {
507
                    $node->appendChild($newnode);
508
                } else {
509
                    $node->insertBefore($newnode, $refnode);
510
                }
511
                $newnodes[] = $newnode;
512
            }
513
        }
514
        return self::create($newnodes);
515
    }
516
517
    /**
518
     * Remove the set of matched elements from the DOM.
519
     *
520
     * (as opposed to Crawler::clear() which detaches the nodes only from Crawler
521
     * but leaves them in the DOM)
522
     *
523
     * @api
524
     */
525 2
    public function remove()
526
    {
527 2
        foreach ($this as $node) {
528
            /**
529
             * @var \DOMNode $node
530
             */
531 2
            if ($node->parentNode instanceof \DOMElement) {
532 2
                $node->parentNode->removeChild($node);
533 2
            }
534 2
        }
535 2
        $this->clear();
536 2
    }
537
538
    /**
539
     * Remove an attribute from each element in the set of matched elements.
540
     *
541
     * Alias for removeAttribute for compatibility with jQuery
542
     *
543
     * @param string $name
544
     * @return HtmlPageCrawler
545
     * @api
546
     */
547 1
    public function removeAttr($name)
548
    {
549 1
        return $this->removeAttribute($name);
550
    }
551
552
    /**
553
     * Remove an attribute from each element in the set of matched elements.
554
     *
555
     * @param string $name
556
     * @return HtmlPageCrawler
557
     */
558 1
    public function removeAttribute($name)
559
    {
560 1
        foreach ($this as $node) {
561 1
            if ($node instanceof \DOMElement) {
562
                /** @var \DOMElement $node */
563 1
                if ($node->hasAttribute($name)) {
564 1
                    $node->removeAttribute($name);
565 1
                }
566 1
            }
567 1
        }
568 1
        return $this;
569
    }
570
571
    /**
572
     * Remove a class from each element in the list
573
     *
574
     * @param string $name
575
     * @return HtmlPageCrawler $this for chaining
576
     * @api
577
     */
578 2
    public function removeClass($name)
579
    {
580 2
        foreach ($this as $node) {
581 2
            if ($node instanceof \DOMElement) {
582
                /** @var \DOMElement $node */
583 2
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
584 2
                $count = count($classes);
585 2
                for ($i = 0; $i < $count; $i++) {
586 2
                    if ($classes[$i] == $name) {
587 2
                        unset($classes[$i]);
588 2
                    }
589 2
                }
590 2
                $node->setAttribute('class', trim(join(' ', $classes)));
591 2
            }
592 2
        }
593 2
        return $this;
594
    }
595
596
    /**
597
     * Replace each target element with the set of matched elements.
598
     *
599
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
600
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
601
     * @api
602
     */
603 2
    public function replaceAll($element)
604
    {
605 2
        $e = self::create($element);
606 2
        $newnodes = array();
607 2
        foreach ($e as $i => $node) {
608
            /** @var \DOMNode $node */
609 2
            $parent = $node->parentNode;
610 2
            $refnode  = $node->nextSibling;
611 2
            foreach ($this as $j => $newnode) {
612
                /** @var \DOMNode $newnode */
613 2
                $newnode = static::importNewnode($newnode, $node, $i);
614 2
                if ($j == 0) {
615 2
                    $parent->replaceChild($newnode, $node);
616 2
                } else {
617 1
                    $parent->insertBefore($newnode, $refnode);
618
                }
619 2
                $newnodes[] = $newnode;
620 2
            }
621 2
        }
622 2
        return self::create($newnodes);
623
    }
624
625
    /**
626
     * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed.
627
     *
628
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
629
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
630
     * @api
631
     */
632 2
    public function replaceWith($content)
633
    {
634 2
        $content = self::create($content);
635 2
        $newnodes = array();
636 2
        foreach ($this as $i => $node) {
637
            /** @var \DOMNode $node */
638 2
            $parent = $node->parentNode;
639 2
            $refnode  = $node->nextSibling;
640 2
            foreach ($content as $j => $newnode) {
641
                /** @var \DOMNode $newnode */
642 2
                $newnode = static::importNewnode($newnode, $node, $i);
643 2
                if ($j == 0) {
644 2
                    $parent->replaceChild($newnode, $node);
645 2
                } else {
646 1
                    $parent->insertBefore($newnode, $refnode);
647
                }
648 2
                $newnodes[] = $newnode;
649 2
            }
650 2
        }
651 2
        $content->clear();
652 2
        $content->add($newnodes);
653 2
        return $this;
654
    }
655
656
    /**
657
     * Get the combined text contents of each element in the set of matched elements, including their descendants,
658
     * or set the text contents of the matched elements.
659
     *
660
     * ATTENTION: Contrary to the parent Crawler class, which returns the text from the first element only,
661
     * this functions returns the combined text of all elements (as jQuery does). If this is not what you need you
662
     * must call ->first() before calling ->text(), e.g.
663
     *
664
     * in Symfony\DOMCrawler\Crawler: $c->filter('p')->text() returns the text of the first paragraph only
665
     * in HtmlPageCrawler you need to call: $c->filter('p')->first()->text()
666
     *
667
     * @param null|string $text
668
     * @return string|HtmlPageCrawler
669
     * @api
670
     */
671 1
    public function text($text = null)
672
    {
673 1
        if ($text === null) {
674 1
            $text = '';
675 1
            foreach ($this as $node) {
676
                /** @var \DOMNode $node */
677 1
                $text .= $node->nodeValue;
678 1
            }
679 1
            return $text;
680
        } else {
681 1
            foreach ($this as $node) {
682
                /** @var \DOMNode $node */
683 1
                $node->nodeValue = $text;
684 1
            }
685 1
            return $this;
686
        }
687
    }
688
689
690
    /**
691
     * Add or remove one or more classes from each element in the set of matched elements, depending the class’s presence.
692
     *
693
     * @param string $classname One or more classnames separated by spaces
694
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
695
     * @api
696
     */
697 1
    public function toggleClass($classname)
698
    {
699 1
        $classes = explode(' ', $classname);
700 1
        foreach ($this as $i => $node) {
701 1
            $c = self::create($node);
702
            /** @var \DOMNode $node */
703 1
            foreach ($classes as $class) {
704 1
                if ($c->hasClass($class)) {
705 1
                    $c->removeClass($class);
706 1
                } else {
707 1
                    $c->addClass($class);
708
                }
709 1
            }
710 1
        }
711 1
        return $this;
712
    }
713
714
    /**
715
     * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
716
     *
717
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
718
     * @api
719
     */
720 1
    public function unwrap()
721
    {
722 1
        $parents = array();
723 1
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
724 1
            $parents[] = $node->parentNode;
725 1
        }
726
727 1
        self::create($parents)->unwrapInner();
728 1
        return $this;
729
    }
730
731
    /**
732
     * Remove the matched elements, but promote the children to take their place.
733
     *
734
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
735
     * @api
736
     */
737 1
    public function unwrapInner()
738
    {
739 1
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
740 1
            if (!$node->parentNode instanceof \DOMElement) {
741
                throw new \InvalidArgumentException('DOMElement does not have a parent DOMElement node.');
742
            }
743
744
            /** @var \DOMNode[] $children */
745 1
            $children = iterator_to_array($node->childNodes);
746 1
            foreach ($children as $child) {
747 1
                $node->parentNode->insertBefore($child, $node);
748 1
            }
749
750 1
            $node->parentNode->removeChild($node);
751 1
        }
752 1
    }
753
754
755
    /**
756
     * Wrap an HTML structure around each element in the set of matched elements
757
     *
758
     * The HTML structure must contain only one root node, e.g.:
759
     * Works: <div><div></div></div>
760
     * Does not work: <div></div><div></div>
761
     *
762
     * @param string|HtmlPageCrawler|\DOMNode $wrappingElement
763
     * @return HtmlPageCrawler $this for chaining
764
     * @api
765
     */
766 1
    public function wrap($wrappingElement)
767
    {
768 1
        $content = self::create($wrappingElement);
769 1
        $newnodes = array();
770 1
        foreach ($this as $i => $node) {
771
            /** @var \DOMNode $node */
772 1
            $newnode = $content->getNode(0);
773
            /** @var \DOMNode $newnode */
774
//            $newnode = static::importNewnode($newnode, $node, $i);
775 1
            if ($newnode->ownerDocument !== $node->ownerDocument) {
776 1
                $newnode = $node->ownerDocument->importNode($newnode, true);
777 1
            } else {
778
                if ($i > 0) {
779
                    $newnode = $newnode->cloneNode(true);
780
                }
781
            }
782 1
            $oldnode = $node->parentNode->replaceChild($newnode, $node);
783 1
            while ($newnode->hasChildNodes()) {
784 1
                $elementFound = false;
785 1
                foreach ($newnode->childNodes as $child) {
786 1
                    if ($child instanceof \DOMElement) {
787 1
                        $newnode = $child;
788 1
                        $elementFound = true;
789 1
                        break;
790
                    }
791 1
                }
792 1
                if (!$elementFound) {
793
                    break;
794
                }
795 1
            }
796 1
            $newnode->appendChild($oldnode);
797 1
            $newnodes[] = $newnode;
798 1
        }
799 1
        $content->clear();
800 1
        $content->add($newnodes);
801 1
        return $this;
802
    }
803
804
    /**
805
     * Wrap an HTML structure around all elements in the set of matched elements.
806
     *
807
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
808
     * @throws \LogicException
809
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
810
     * @api
811
     */
812 1
    public function wrapAll($content)
813
    {
814 1
        $content = self::create($content);
815 1
        $parent = $this->getNode(0)->parentNode;
816 1
        foreach ($this as $i => $node) {
817
            /** @var \DOMNode $node */
818 1
            if ($node->parentNode !== $parent) {
819
                throw new \LogicException('Nodes to be wrapped with wrapAll() must all have the same parent');
820
            }
821 1
        }
822
823 1
        $newnode = $content->getNode(0);
824
        /** @var \DOMNode $newnode */
825 1
        $newnode = static::importNewnode($newnode, $parent);
826
827 1
        $newnode = $parent->insertBefore($newnode,$this->getNode(0));
828 1
        $content->clear();
829 1
        $content->add($newnode);
830
831 1
        while ($newnode->hasChildNodes()) {
832
            $elementFound = false;
833
            foreach ($newnode->childNodes as $child) {
834
                if ($child instanceof \DOMElement) {
835
                    $newnode = $child;
836
                    $elementFound = true;
837
                    break;
838
                }
839
            }
840
            if (!$elementFound) {
841
                break;
842
            }
843
        }
844 1
        foreach ($this as $i => $node) {
845
            /** @var \DOMNode $node */
846 1
            $newnode->appendChild($node);
847 1
        }
848 1
        return $this;
849
    }
850
851
    /**
852
     * Wrap an HTML structure around the content of each element in the set of matched elements.
853
     *
854
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
855
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
856
     * @api
857
     */
858 1
    public function wrapInner($content)
859
    {
860 1
        foreach ($this as $i => $node) {
861
            /** @var \DOMNode $node */
862 1
            self::create($node->childNodes)->wrapAll($content);
863 1
        }
864 1
        return $this;
865
    }
866
867
    /**
868
     * Get the HTML code fragment of all elements and their contents.
869
     *
870
     * If the first node contains a complete HTML document return the
871
     * DocType if exists
872
     *
873
     * @return string HTML code (fragment)
874
     * @api
875
     */
876 3
    public function saveHTML()
877
    {
878 3
        $html = '';
879 3
        if ( $this->isHtmlDocument() ) {
0 ignored issues
show
Coding Style introduced by
Expected 0 spaces before closing bracket; 1 found
Loading history...
880
            /* Output DocType if exists */
881 1
            $documentHtml = $this->getDOMDocument()->saveHTML();
882 1
            $html .= preg_match("/<!DOCTYPE.*?>/is", $documentHtml, $match) ? $match[0]."\n" : '';
883 1
        }
884 3
        foreach ($this as $node) {
885 3
            $html .= trim($node->ownerDocument->saveHTML($node));
886 3
        }
887 3
        return $html;
888
    }
889
890
    public function __toString()
891
    {
892
        return $this->saveHTML();
893
    }
894
895
    /**
896
     * checks whether the first node contains a complete html document
897
     * (as opposed to a document fragment)
898
     *
899
     * @return boolean
900
     */
901 3
    public function isHtmlDocument()
902
    {
903 3
        $node = $this->getNode(0);
904
        if ($node instanceof \DOMElement
905 3
            && $node->ownerDocument instanceof \DOMDocument
906 3
            && $node->ownerDocument->documentElement === $node
907 3
            && $node->nodeName == 'html'
908 3
        ) {
909 1
            return true;
910
        } else {
911 3
            return false;
912
        }
913
    }
914
915
    /**
916
     * get ownerDocument of the first element
917
     *
918
     * @return \DOMDocument|null
919
     */
920
    public function getDOMDocument()
921
    {
922
        $node = $this->getNode(0); 
923
        $r = null;
924
        if ($node instanceof \DOMElement
925
            && $node->ownerDocument instanceof \DOMDocument
926
        ) {
927
            $r = $node->ownerDocument;
928
        }
929
        return $r;
930
    }
931
932
    /**
933
     * Filters the list of nodes with a CSS selector.
934
     *
935
     * @param string $selector
936
     * @return HtmlPageCrawler
937
     */
938 6
    public function filter($selector)
939
    {
940 6
        return parent::filter($selector);
941
    }
942
943
    /**
944
     * Filters the list of nodes with an XPath expression.
945
     *
946
     * @param string $xpath An XPath expression
947
     *
948
     * @return HtmlPageCrawler A new instance of Crawler with the filtered list of nodes
949
     *
950
     * @api
951
     */
952 1
    public function filterXPath($xpath)
953
    {
954 1
        return parent::filterXPath($xpath);
955
    }
956
957
    /**
958
     * Adds HTML/XML content to the HtmlPageCrawler object (but not to the DOM of an already attached node).
959
     *
960
     * Function overriden from Crawler because HTML fragments are always added as complete documents there
961
     *
962
     *
963
     * @param string      $content A string to parse as HTML/XML
964
     * @param null|string $type    The content type of the string
965
     *
966
     * @return null|void
967
     */
968 12
    public function addContent($content, $type = null)
969
    {
970 12
        if (empty($type)) {
971 12
            $type = 'text/html;charset=UTF-8';
972 12
        }
973 12
        if (substr($type, 0, 9) == 'text/html' && !preg_match('/<html\b[^>]*>/i', $content)) {
974
            // string contains no <html> Tag => no complete document but an HTML fragment!
975 10
            $this->addHtmlFragment($content);
976 10
        } else {
977 2
            parent::addContent($content, $type);
978
        }
979 12
    }
980
981 10
    public function addHtmlFragment($content, $charset = 'UTF-8')
982
    {
983 10
        $d = new \DOMDocument('1.0', $charset);
984 10
        $root = $d->appendChild($d->createElement(self::FRAGMENT_ROOT_TAGNAME));
985 10
        $bodynode = Helpers::getBodyNodeFromHtmlFragment($content, $charset);
986 10
        foreach ($bodynode->childNodes as $child) {
987 10
            $inode = $root->appendChild($d->importNode($child, true));
988 10
            if ($inode) {
989 10
                $this->addNode($inode);
990 10
            }
991 10
        }
992 10
    }
993
994
    /**
995
     * returns the first node
996
     * deprecated, use getNode(0) instead
997
     *
998
     * @return \DOMNode|null
999
     * @deprecated
1000
     * @see Crawler::getNode
1001
     */
1002 1
    public function getFirstNode()
1003
    {
1004 1
        return $this->getNode(0);
1005
    }
1006
1007
    /**
1008
     * @param int $position
1009
     *
1010
     * overridden from Crawler because it is not public in Symfony 2.3
1011
     * TODO: throw away as soon as we don't need to support SF 2.3 any more
1012
     *
1013
     * @return \DOMElement|null
1014
     */
1015 5
    public function getNode($position)
1016
    {
1017 5
        return parent::getNode($position);
1018
    }
1019
1020
    /**
1021
     * Returns the node name of the first node of the list.
1022
     *
1023
     * in Crawler (parent), this function will be available starting with 2.6.0,
1024
     * therefore this method be removed from here as soon as we don't need to keep compatibility
1025
     * with Symfony < 2.6
1026
     *
1027
     * TODO: throw away as soon as we don't need to support SF 2.3 any more
1028
     *
1029
     * @return string The node name
1030
     *
1031
     * @throws \InvalidArgumentException When current node is empty
1032
     */
1033 1
    public function nodeName()
1034
    {
1035 1
        if (!count($this)) {
1036
            throw new \InvalidArgumentException('The current node list is empty.');
1037
        }
1038 1
        return $this->getNode(0)->nodeName;
1039
    }
1040
1041
    /**
1042
     * Adds a node to the current list of nodes.
1043
     *
1044
     * This method uses the appropriate specialized add*() method based
1045
     * on the type of the argument.
1046
     *
1047
     * Overwritten from parent to allow Crawler to be added
1048
     *
1049
     * @param null|\DOMNodeList|array|\DOMNode|Crawler $node A node
1050
     *
1051
     * @api
1052
     */
1053 14
    public function add($node)
1054
    {
1055 14
        if ($node instanceof Crawler) {
1056
            foreach ($node as $childnode) {
1057
                $this->addNode($childnode);
1058
            }
1059
        } else {
1060 14
            parent::add($node);
1061
        }
1062 14
    }
1063
1064
    /**
1065
     * @param \DOMNode $newnode
1066
     * @param \DOMNode $referencenode
1067
     * @param int $clone
1068
     * @return \DOMNode
1069
     */
1070 5
    protected static function importNewnode(\DOMNode $newnode, \DOMNode $referencenode, $clone = 0) {
1071 5
        if ($newnode->ownerDocument !== $referencenode->ownerDocument) {
1072 4
            $newnode = $referencenode->ownerDocument->importNode($newnode, true);
1073 4
        } else {
1074 2
            if ($clone > 0) {
1075
                $newnode = $newnode->cloneNode(true);
1076
            }
1077
        }
1078 5
        return $newnode;
1079
    }
1080
1081
    /**
1082
     * Checks whether the first node in the set is disconnected (has no parent node)
1083
     *
1084
     * @return bool
1085
     */
1086 1
    public function isDisconnected()
1087
    {
1088 1
        $parent = $this->getNode(0)->parentNode;
1089 1
        return ($parent == null || $parent->tagName == self::FRAGMENT_ROOT_TAGNAME);
1090
    }
1091
1092 1
    public function __get($name)
1093
    {
1094
        switch ($name) {
1095 1
            case 'count':
1096 1
            case 'length':
1097 1
                return count($this);
1098
        }
1099 1
        throw new \Exception('No such property ' . $name);
1100
    }
1101
}
1102