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
Push — master ( 8c00b1...542f3f )
by Christoph
01:58
created

HtmlPageCrawler::wrapAll()   B

Complexity

Conditions 8
Paths 29

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8.0052

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 22
cts 23
cp 0.9565
rs 8.0675
c 0
b 0
f 0
cc 8
nc 29
nop 1
crap 8.0052
1
<?php
2
namespace Wa72\HtmlPageDom;
3
4
use Symfony\Component\DomCrawler\Crawler;
5
6
/**
7
 * Extends \Symfony\Component\DomCrawler\Crawler by adding tree manipulation functions
8
 * for HTML documents inspired by jQuery such as html(), css(), append(), prepend(), before(),
9
 * addClass(), removeClass()
10
 *
11
 * @author Christoph Singer
12
 * @license MIT
13
 *
14
 */
15
class HtmlPageCrawler extends Crawler
0 ignored issues
show
Complexity introduced by
This class has 1056 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 46 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 156 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...
16
{
17
    /**
18
     * the (internal) root element name used when importing html fragments
19
     * */
20
    const FRAGMENT_ROOT_TAGNAME = '_root';
21
22
    /**
23
     * Get an HtmlPageCrawler object from a HTML string, DOMNode, DOMNodeList or HtmlPageCrawler
24
     *
25
     * This is the equivalent to jQuery's $() function when used for wrapping DOMNodes or creating DOMElements from HTML code.
26
     *
27
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|array $content
28
     * @return HtmlPageCrawler
29
     * @api
30
     */
31 18
    public static function create($content)
32
    {
33 18
        if ($content instanceof HtmlPageCrawler) {
34 3
            return $content;
35
        } else {
36 18
            return new HtmlPageCrawler($content);
37
        }
38
    }
39
40
    /**
41
     * Adds the specified class(es) to each element in the set of matched elements.
42
     *
43
     * @param string $name One or more space-separated classes to be added to the class attribute of each matched element.
44
     * @return HtmlPageCrawler $this for chaining
45
     * @api
46
     */
47 1
    public function addClass($name)
48
    {
49 1
        foreach ($this as $node) {
50 1
            if ($node instanceof \DOMElement) {
51
                /** @var \DOMElement $node */
52 1
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
53 1
                $found = false;
54 1
                $count = count($classes);
55 1
                for ($i = 0; $i < $count; $i++) {
56 1
                    if ($classes[$i] == $name) {
57 1
                        $found = true;
58
                    }
59
                }
60 1
                if (!$found) {
61 1
                    $classes[] = $name;
62 1
                    $node->setAttribute('class', trim(join(' ', $classes)));
63
                }
64
            }
65
        }
66 1
        return $this;
67
    }
68
69
    /**
70
     * Insert content, specified by the parameter, after each element in the set of matched elements.
71
     *
72
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
73
     * @return HtmlPageCrawler $this for chaining
74
     * @api
75
     */
76 3
    public function after($content)
77
    {
78 3
        $content = self::create($content);
79 3
        $newnodes = array();
80 3
        foreach ($this as $i => $node) {
81
            /** @var \DOMNode $node */
82 3
            $refnode = $node->nextSibling;
83 3
            foreach ($content as $newnode) {
84
                /** @var \DOMNode $newnode */
85 3
                $newnode = static::importNewnode($newnode, $node, $i);
86 3
                if ($refnode === null) {
87 3
                    $node->parentNode->appendChild($newnode);
88
                } else {
89 1
                    $node->parentNode->insertBefore($newnode, $refnode);
90
                }
91 3
                $newnodes[] = $newnode;
92
            }
93
        }
94 3
        $content->clear();
95 3
        $content->add($newnodes);
96 3
        return $this;
97
    }
98
99
    /**
100
     * Insert HTML content as child nodes of each element after existing children
101
     *
102
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment or DOMNode to append
103
     * @return HtmlPageCrawler $this for chaining
104
     * @api
105
     */
106 2
    public function append($content)
107
    {
108 2
        $content = self::create($content);
109 2
        $newnodes = array();
110 2
        foreach ($this as $i => $node) {
111
            /** @var \DOMNode $node */
112 2
            foreach ($content as $newnode) {
113
                /** @var \DOMNode $newnode */
114 2
                $newnode = static::importNewnode($newnode, $node, $i);
115 2
                $node->appendChild($newnode);
116 2
                $newnodes[] = $newnode;
117
            }
118
        }
119 2
        $content->clear();
120 2
        $content->add($newnodes);
121 2
        return $this;
122
    }
123
124
    /**
125
     * Insert every element in the set of matched elements to the end of the target.
126
     *
127
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
128
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
129
     * @api
130
     */
131 2
    public function appendTo($element)
132
    {
133 2
        $e = self::create($element);
134 2
        $newnodes = array();
135 2
        foreach ($e as $i => $node) {
136
            /** @var \DOMNode $node */
137 2
            foreach ($this as $newnode) {
138
                /** @var \DOMNode $newnode */
139 2
                if ($node !== $newnode) {
140 2
                    $newnode = static::importNewnode($newnode, $node, $i);
141 2
                    $node->appendChild($newnode);
142
                }
143 2
                $newnodes[] = $newnode;
144
            }
145
        }
146 2
        return self::create($newnodes);
147
    }
148
149
    /**
150
     * Returns the attribute value of the first node of the list, or sets an attribute on each element
151
     *
152
     * @see HtmlPageCrawler::getAttribute()
153
     * @see HtmlPageCrawler::setAttribute
154
     *
155
     * @param string $name
156
     * @param null|string $value
157
     * @return null|string|HtmlPageCrawler
158
     * @api
159
     */
160 2
    public function attr($name, $value = null)
161
    {
162 2
        if ($value === null) {
163 2
            return $this->getAttribute($name);
164
        } else {
165 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...
166
        }
167
    }
168
169
    /**
170
     * Sets an attribute on each element
171
     *
172
     * @param string $name
173
     * @param string $value
174
     * @return HtmlPageCrawler $this for chaining
175
     */
176 3
    public function setAttribute($name, $value)
177
    {
178 3
        foreach ($this as $node) {
179 3
            if ($node instanceof \DOMElement) {
180
                /** @var \DOMElement $node */
181 3
                $node->setAttribute($name, $value);
182
            }
183
        }
184 3
        return $this;
185
    }
186
187
    /**
188
     * Returns the attribute value of the first node of the list.
189
     *
190
     * @param string $name The attribute name
191
     * @return string|null The attribute value or null if the attribute does not exist
192
     * @throws \InvalidArgumentException When current node is empty
193
     *
194
     */
195 2
    public function getAttribute($name)
196
    {
197 2
        if (!count($this)) {
198 1
            throw new \InvalidArgumentException('The current node list is empty.');
199
        }
200 1
        $node = $this->getNode(0);
201 1
        return $node->hasAttribute($name) ? $node->getAttribute($name) : null;
202
    }
203
204
    /**
205
     * Insert content, specified by the parameter, before each element in the set of matched elements.
206
     *
207
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
208
     * @return HtmlPageCrawler $this for chaining
209
     * @api
210
     */
211 2
    public function before($content)
212
    {
213 2
        $content = self::create($content);
214 2
        $newnodes = array();
215 2
        foreach ($this as $i => $node) {
216
            /** @var \DOMNode $node */
217 2
            foreach ($content as $newnode) {
218
                /** @var \DOMNode $newnode */
219 2
                if ($node !== $newnode) {
220 2
                    $newnode = static::importNewnode($newnode, $node, $i);
221 2
                    $node->parentNode->insertBefore($newnode, $node);
222 2
                    $newnodes[] = $newnode;
223
                }
224
            }
225
        }
226 2
        $content->clear();
227 2
        $content->add($newnodes);
228 2
        return $this;
229
    }
230
231
    /**
232
     * Create a deep copy of the set of matched elements.
233
     *
234
     * Equivalent to clone() in jQuery (clone is not a valid PHP function name)
235
     *
236
     * @return HtmlPageCrawler
237
     * @api
238
     */
239 1
    public function makeClone()
240
    {
241 1
        return clone $this;
242
    }
243
244 1
    public function __clone()
245
    {
246 1
        $newnodes = array();
247 1
        foreach ($this as $node) {
248
            /** @var \DOMNode $node */
249 1
            $newnodes[] = $node->cloneNode(true);
250
        }
251 1
        $this->clear();
252 1
        $this->add($newnodes);
253 1
    }
254
255
    /**
256
     * Get one CSS style property of the first element or set it for all elements in the list
257
     *
258
     * Function is here for compatibility with jQuery; it is the same as getStyle() and setStyle()
259
     *
260
     * @see HtmlPageCrawler::getStyle()
261
     * @see HtmlPageCrawler::setStyle()
262
     *
263
     * @param string $key The name of the style property
264
     * @param null|string $value The CSS value to set, or NULL to get the current value
265
     * @return HtmlPageCrawler|string If no param is provided, returns the CSS styles of the first element
266
     * @api
267
     */
268 1
    public function css($key, $value = null)
269
    {
270 1
        if (null === $value) {
271 1
            return $this->getStyle($key);
272
        } else {
273 1
            return $this->setStyle($key, $value);
274
        }
275
    }
276
277
    /**
278
     * get one CSS style property of the first element
279
     *
280
     * @param string $key name of the property
281
     * @return string|null value of the property
282
     */
283 1
    public function getStyle($key)
284
    {
285 1
        $styles = Helpers::cssStringToArray($this->getAttribute('style'));
286 1
        return (isset($styles[$key]) ? $styles[$key] : null);
287
    }
288
289
    /**
290
     * set one CSS style property for all elements in the list
291
     *
292
     * @param string $key name of the property
293
     * @param string $value value of the property
294
     * @return HtmlPageCrawler $this for chaining
295
     */
296 1
    public function setStyle($key, $value)
297
    {
298 1
        foreach ($this as $node) {
299 1
            if ($node instanceof \DOMElement) {
300
                /** @var \DOMElement $node */
301 1
                $styles = Helpers::cssStringToArray($node->getAttribute('style'));
302 1
                if ($value != '') {
303 1
                    $styles[$key] = $value;
304 1
                } elseif (isset($styles[$key])) {
305 1
                    unset($styles[$key]);
306
                }
307 1
                $node->setAttribute('style', Helpers::cssArrayToString($styles));
308
            }
309
        }
310 1
        return $this;
311
    }
312
313
    /**
314
     * Removes all child nodes and text from all nodes in set
315
     *
316
     * Equivalent to jQuery's empty() function which is not a valid function name in PHP
317
     * @return HtmlPageCrawler $this
318
     * @api
319
     */
320 1
    public function makeEmpty()
321
    {
322 1
        foreach ($this as $node) {
323 1
            $node->nodeValue = '';
324
        }
325 1
        return $this;
326
    }
327
328
    /**
329
     * Determine whether any of the matched elements are assigned the given class.
330
     *
331
     * @param string $name
332
     * @return bool
333
     * @api
334
     */
335 2
    public function hasClass($name)
336
    {
337 2
        foreach ($this as $node) {
338 2
            if ($node instanceof \DOMElement && $class = $node->getAttribute('class')) {
339 2
                $classes = preg_split('/\s+/s', $class);
340 2
                if (in_array($name, $classes)) {
341 2
                    return true;
342
                }
343
            }
344
        }
345 2
        return false;
346
    }
347
348
    /**
349
     * Set the HTML contents of each element
350
     *
351
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
352
     * @return HtmlPageCrawler $this for chaining
353
     */
354 3
    public function setInnerHtml($content)
355
    {
356 3
        $content = self::create($content);
357 3
        foreach ($this as $node) {
358 3
            $node->nodeValue = '';
359 3
            foreach ($content as $newnode) {
360
                /** @var \DOMNode $node */
361
                /** @var \DOMNode $newnode */
362 3
                $newnode = static::importNewnode($newnode, $node);
363 3
                $node->appendChild($newnode);
364
            }
365
        }
366 3
        return $this;
367
    }
368
369
    /**
370
     * Insert every element in the set of matched elements after the target.
371
     *
372
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
373
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
374
     * @api
375
     */
376 2
    public function insertAfter($element)
377
    {
378 2
        $e = self::create($element);
379 2
        $newnodes = array();
380 2
        foreach ($e as $i => $node) {
381
            /** @var \DOMNode $node */
382 2
            $refnode = $node->nextSibling;
383 2
            foreach ($this as $newnode) {
384
                /** @var \DOMNode $newnode */
385 2
                $newnode = static::importNewnode($newnode, $node, $i);
386 2
                if ($refnode === null) {
387 2
                    $node->parentNode->appendChild($newnode);
388
                } else {
389 1
                    $node->parentNode->insertBefore($newnode, $refnode);
390
                }
391 2
                $newnodes[] = $newnode;
392
            }
393
        }
394 2
        return self::create($newnodes);
395
    }
396
397
    /**
398
     * Insert every element in the set of matched elements before the target.
399
     *
400
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
401
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
402
     * @api
403
     */
404 2
    public function insertBefore($element)
405
    {
406 2
        $e = self::create($element);
407 2
        $newnodes = array();
408 2
        foreach ($e as $i => $node) {
409
            /** @var \DOMNode $node */
410 2
            foreach ($this as $newnode) {
411
                /** @var \DOMNode $newnode */
412 2
                $newnode = static::importNewnode($newnode, $node, $i);
413 2
                if ($newnode !== $node) {
414 2
                    $node->parentNode->insertBefore($newnode, $node);
415
                }
416 2
                $newnodes[] = $newnode;
417
            }
418
        }
419 2
        return self::create($newnodes);
420
    }
421
422
    /**
423
     * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements.
424
     *
425
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
426
     * @return HtmlPageCrawler $this for chaining
427
     * @api
428
     */
429 2
    public function prepend($content)
430
    {
431 2
        $content = self::create($content);
432 2
        $newnodes = array();
433 2
        foreach ($this as $i => $node) {
434 2
            $refnode = $node->firstChild;
435
            /** @var \DOMNode $node */
436 2
            foreach ($content as $newnode) {
437
                /** @var \DOMNode $newnode */
438 2
                $newnode = static::importNewnode($newnode, $node, $i);
439 2
                if ($refnode === null) {
440 1
                    $node->appendChild($newnode);
441 2
                } else if ($refnode !== $newnode) {
442 2
                    $node->insertBefore($newnode, $refnode);
443
                }
444 2
                $newnodes[] = $newnode;
445
            }
446
        }
447 2
        $content->clear();
448 2
        $content->add($newnodes);
449 2
        return $this;
450
    }
451
452
    /**
453
     * Insert every element in the set of matched elements to the beginning of the target.
454
     *
455
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
456
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements prepended to the target elements
457
     * @api
458
     */
459 1
    public function prependTo($element)
460
    {
461 1
        $e = self::create($element);
462 1
        $newnodes = array();
463 1
        foreach ($e as $i => $node) {
464 1
            $refnode = $node->firstChild;
465
            /** @var \DOMNode $node */
466 1
            foreach ($this as $newnode) {
467
                /** @var \DOMNode $newnode */
468 1
                $newnode = static::importNewnode($newnode, $node, $i);
469 1
                if ($newnode !== $node) {
470 1
                    if ($refnode === null) {
471 1
                        $node->appendChild($newnode);
472
                    } else {
473 1
                        $node->insertBefore($newnode, $refnode);
474
                    }
475
                }
476 1
                $newnodes[] = $newnode;
477
            }
478
        }
479 1
        return self::create($newnodes);
480
    }
481
482
    /**
483
     * Remove the set of matched elements from the DOM.
484
     *
485
     * (as opposed to Crawler::clear() which detaches the nodes only from Crawler
486
     * but leaves them in the DOM)
487
     *
488
     * @api
489
     */
490 2
    public function remove()
491
    {
492 2
        foreach ($this as $node) {
493
            /**
494
             * @var \DOMNode $node
495
             */
496 2
            if ($node->parentNode instanceof \DOMElement) {
497 2
                $node->parentNode->removeChild($node);
498
            }
499
        }
500 2
        $this->clear();
501 2
    }
502
503
    /**
504
     * Remove an attribute from each element in the set of matched elements.
505
     *
506
     * Alias for removeAttribute for compatibility with jQuery
507
     *
508
     * @param string $name
509
     * @return HtmlPageCrawler
510
     * @api
511
     */
512 1
    public function removeAttr($name)
513
    {
514 1
        return $this->removeAttribute($name);
515
    }
516
517
    /**
518
     * Remove an attribute from each element in the set of matched elements.
519
     *
520
     * @param string $name
521
     * @return HtmlPageCrawler
522
     */
523 1
    public function removeAttribute($name)
524
    {
525 1
        foreach ($this as $node) {
526 1
            if ($node instanceof \DOMElement) {
527
                /** @var \DOMElement $node */
528 1
                if ($node->hasAttribute($name)) {
529 1
                    $node->removeAttribute($name);
530
                }
531
            }
532
        }
533 1
        return $this;
534
    }
535
536
    /**
537
     * Remove a class from each element in the list
538
     *
539
     * @param string $name
540
     * @return HtmlPageCrawler $this for chaining
541
     * @api
542
     */
543 2
    public function removeClass($name)
544
    {
545 2
        foreach ($this as $node) {
546 2
            if ($node instanceof \DOMElement) {
547
                /** @var \DOMElement $node */
548 2
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
549 2
                $count = count($classes);
550 2
                for ($i = 0; $i < $count; $i++) {
551 2
                    if ($classes[$i] == $name) {
552 2
                        unset($classes[$i]);
553
                    }
554
                }
555 2
                $node->setAttribute('class', trim(join(' ', $classes)));
556
            }
557
        }
558 2
        return $this;
559
    }
560
561
    /**
562
     * Replace each target element with the set of matched elements.
563
     *
564
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
565
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
566
     * @api
567
     */
568 2
    public function replaceAll($element)
569
    {
570 2
        $e = self::create($element);
571 2
        $newnodes = array();
572 2
        foreach ($e as $i => $node) {
573
            /** @var \DOMNode $node */
574 2
            $parent = $node->parentNode;
575 2
            $refnode  = $node->nextSibling;
576 2
            foreach ($this as $j => $newnode) {
577
                /** @var \DOMNode $newnode */
578 2
                $newnode = static::importNewnode($newnode, $node, $i);
579 2
                if ($j == 0) {
580 2
                    $parent->replaceChild($newnode, $node);
581
                } else {
582 1
                    $parent->insertBefore($newnode, $refnode);
583
                }
584 2
                $newnodes[] = $newnode;
585
            }
586
        }
587 2
        return self::create($newnodes);
588
    }
589
590
    /**
591
     * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed.
592
     *
593
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
594
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
595
     * @api
596
     */
597 2
    public function replaceWith($content)
598
    {
599 2
        $content = self::create($content);
600 2
        $newnodes = array();
601 2
        foreach ($this as $i => $node) {
602
            /** @var \DOMNode $node */
603 2
            $parent = $node->parentNode;
604 2
            $refnode  = $node->nextSibling;
605 2
            foreach ($content as $j => $newnode) {
606
                /** @var \DOMNode $newnode */
607 2
                $newnode = static::importNewnode($newnode, $node, $i);
608 2
                if ($j == 0) {
609 2
                    $parent->replaceChild($newnode, $node);
610
                } else {
611 1
                    $parent->insertBefore($newnode, $refnode);
612
                }
613 2
                $newnodes[] = $newnode;
614
            }
615
        }
616 2
        $content->clear();
617 2
        $content->add($newnodes);
618 2
        return $this;
619
    }
620
621
    /**
622
     * Get the combined text contents of each element in the set of matched elements, including their descendants.
623
     * This is what the jQuery text() function does, contrary to the Crawler::text() method that returns only
624
     * the text of the first node.
625
     *
626
     * @return string
627
     * @api
628
     */
629 1
    public function getCombinedText()
630
    {
631 1
        $text = '';
632 1
        foreach ($this as $node) {
633
            /** @var \DOMNode $node */
634 1
            $text .= $node->nodeValue;
635
        }
636 1
        return $text;
637
    }
638
639
    /**
640
     * Set the text contents of the matched elements.
641
     *
642
     * @param string $text
643
     * @return HtmlPageCrawler
644
     * @api
645
     */
646 2
    public function setText($text)
647
    {
648 2
        $text = htmlspecialchars($text);
649 2
        foreach ($this as $node) {
650
            /** @var \DOMNode $node */
651 2
            $node->nodeValue = $text;
652
        }
653 2
        return $this;
654
    }
655
656
    /**
657
     * Add or remove one or more classes from each element in the set of matched elements, depending the class’s presence.
658
     *
659
     * @param string $classname One or more classnames separated by spaces
660
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
661
     * @api
662
     */
663 1
    public function toggleClass($classname)
664
    {
665 1
        $classes = explode(' ', $classname);
666 1
        foreach ($this as $i => $node) {
667 1
            $c = self::create($node);
668
            /** @var \DOMNode $node */
669 1
            foreach ($classes as $class) {
670 1
                if ($c->hasClass($class)) {
671 1
                    $c->removeClass($class);
672
                } else {
673 1
                    $c->addClass($class);
674
                }
675
            }
676
        }
677 1
        return $this;
678
    }
679
680
    /**
681
     * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
682
     *
683
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
684
     * @api
685
     */
686 1
    public function unwrap()
687
    {
688 1
        $parents = array();
689 1
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
690 1
            $parents[] = $node->parentNode;
691
        }
692
693 1
        self::create($parents)->unwrapInner();
694 1
        return $this;
695
    }
696
697
    /**
698
     * Remove the matched elements, but promote the children to take their place.
699
     *
700
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
701
     * @api
702
     */
703 2
    public function unwrapInner()
704
    {
705 2
        foreach($this as $i => $node) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
706 2
            if (!$node->parentNode instanceof \DOMElement) {
707 1
                throw new \InvalidArgumentException('DOMElement does not have a parent DOMElement node.');
708
            }
709
710
            /** @var \DOMNode[] $children */
711 2
            $children = iterator_to_array($node->childNodes);
712 2
            foreach ($children as $child) {
713 1
                $node->parentNode->insertBefore($child, $node);
714
            }
715
716 2
            $node->parentNode->removeChild($node);
717
        }
718 2
    }
719
720
721
    /**
722
     * Wrap an HTML structure around each element in the set of matched elements
723
     *
724
     * The HTML structure must contain only one root node, e.g.:
725
     * Works: <div><div></div></div>
726
     * Does not work: <div></div><div></div>
727
     *
728
     * @param string|HtmlPageCrawler|\DOMNode $wrappingElement
729
     * @return HtmlPageCrawler $this for chaining
730
     * @api
731
     */
732 1
    public function wrap($wrappingElement)
733
    {
734 1
        $content = self::create($wrappingElement);
735 1
        $newnodes = array();
736 1
        foreach ($this as $i => $node) {
737
            /** @var \DOMNode $node */
738 1
            $newnode = $content->getNode(0);
739
            /** @var \DOMNode $newnode */
740
//            $newnode = static::importNewnode($newnode, $node, $i);
741 1
            if ($newnode->ownerDocument !== $node->ownerDocument) {
742 1
                $newnode = $node->ownerDocument->importNode($newnode, true);
743
            } else {
744
                if ($i > 0) {
745
                    $newnode = $newnode->cloneNode(true);
746
                }
747
            }
748 1
            $oldnode = $node->parentNode->replaceChild($newnode, $node);
749 1
            while ($newnode->hasChildNodes()) {
750 1
                $elementFound = false;
751 1
                foreach ($newnode->childNodes as $child) {
752 1
                    if ($child instanceof \DOMElement) {
753 1
                        $newnode = $child;
754 1
                        $elementFound = true;
755 1
                        break;
756
                    }
757
                }
758 1
                if (!$elementFound) {
759 1
                    break;
760
                }
761
            }
762 1
            $newnode->appendChild($oldnode);
763 1
            $newnodes[] = $newnode;
764
        }
765 1
        $content->clear();
766 1
        $content->add($newnodes);
767 1
        return $this;
768
    }
769
770
    /**
771
     * Wrap an HTML structure around all elements in the set of matched elements.
772
     *
773
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
774
     * @throws \LogicException
775
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
776
     * @api
777
     */
778 1
    public function wrapAll($content)
779
    {
780 1
        $content = self::create($content);
781 1
        $parent = $this->getNode(0)->parentNode;
782 1
        foreach ($this as $i => $node) {
783
            /** @var \DOMNode $node */
784 1
            if ($node->parentNode !== $parent) {
785 1
                throw new \LogicException('Nodes to be wrapped with wrapAll() must all have the same parent');
786
            }
787
        }
788
789 1
        $newnode = $content->getNode(0);
790
        /** @var \DOMNode $newnode */
791 1
        $newnode = static::importNewnode($newnode, $parent);
792
793 1
        $newnode = $parent->insertBefore($newnode,$this->getNode(0));
794 1
        $content->clear();
795 1
        $content->add($newnode);
796
797 1
        while ($newnode->hasChildNodes()) {
798 1
            $elementFound = false;
799 1
            foreach ($newnode->childNodes as $child) {
800 1
                if ($child instanceof \DOMElement) {
801 1
                    $newnode = $child;
802 1
                    $elementFound = true;
803 1
                    break;
804
                }
805
            }
806 1
            if (!$elementFound) {
807
                break;
808
            }
809
        }
810 1
        foreach ($this as $i => $node) {
811
            /** @var \DOMNode $node */
812 1
            $newnode->appendChild($node);
813
        }
814 1
        return $this;
815
    }
816
817
    /**
818
     * Wrap an HTML structure around the content of each element in the set of matched elements.
819
     *
820
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
821
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
822
     * @api
823
     */
824 1
    public function wrapInner($content)
825
    {
826 1
        foreach ($this as $i => $node) {
827
            /** @var \DOMNode $node */
828 1
            self::create($node->childNodes)->wrapAll($content);
829
        }
830 1
        return $this;
831
    }
832
833
    /**
834
     * Get the HTML code fragment of all elements and their contents.
835
     *
836
     * If the first node contains a complete HTML document return only
837
     * the full code of this document.
838
     *
839
     * @return string HTML code (fragment)
840
     * @api
841
     */
842 8
    public function saveHTML()
843
    {
844 8
        if ($this->isHtmlDocument()) {
845 1
            return $this->getDOMDocument()->saveHTML();
846
        } else {
847 8
            $doc = new \DOMDocument('1.0', 'UTF-8');
848 8
            $root = $doc->appendChild($doc->createElement('_root'));
849 8
            foreach ($this as $node) {
850 8
                $root->appendChild($doc->importNode($node, true));
851
            }
852 8
            $html = trim($doc->saveHTML());
853 8
            return preg_replace('@^<'.self::FRAGMENT_ROOT_TAGNAME.'[^>]*>|</'.self::FRAGMENT_ROOT_TAGNAME.'>$@', '', $html);
854
        }
855
    }
856
857 4
    public function __toString()
858
    {
859 4
        return $this->saveHTML();
860
    }
861
862
    /**
863
     * checks whether the first node contains a complete html document
864
     * (as opposed to a document fragment)
865
     *
866
     * @return boolean
867
     */
868 8
    public function isHtmlDocument()
869
    {
870 8
        $node = $this->getNode(0);
871 8
        if ($node instanceof \DOMElement
872 8
            && $node->ownerDocument instanceof \DOMDocument
873 8
            && $node->ownerDocument->documentElement === $node
874 8
            && $node->nodeName == 'html'
875
        ) {
876 1
            return true;
877
        } else {
878 8
            return false;
879
        }
880
    }
881
882
    /**
883
     * get ownerDocument of the first element
884
     *
885
     * @return \DOMDocument|null
886
     */
887 1
    public function getDOMDocument()
888
    {
889 1
        $node = $this->getNode(0);
890 1
        $r = null;
891 1
        if ($node instanceof \DOMElement
892 1
            && $node->ownerDocument instanceof \DOMDocument
893
        ) {
894 1
            $r = $node->ownerDocument;
895
        }
896 1
        return $r;
897
    }
898
899
    /**
900
     * Filters the list of nodes with a CSS selector.
901
     *
902
     * @param string $selector
903
     * @return HtmlPageCrawler
904
     */
905 8
    public function filter($selector)
906
    {
907 8
        return parent::filter($selector);
908
    }
909
910
    /**
911
     * Filters the list of nodes with an XPath expression.
912
     *
913
     * @param string $xpath An XPath expression
914
     *
915
     * @return HtmlPageCrawler A new instance of Crawler with the filtered list of nodes
916
     *
917
     * @api
918
     */
919 2
    public function filterXPath($xpath)
920
    {
921 2
        return parent::filterXPath($xpath);
922
    }
923
924
    /**
925
     * Adds HTML/XML content to the HtmlPageCrawler object (but not to the DOM of an already attached node).
926
     *
927
     * Function overriden from Crawler because HTML fragments are always added as complete documents there
928
     *
929
     *
930
     * @param string      $content A string to parse as HTML/XML
931
     * @param null|string $type    The content type of the string
932
     *
933
     * @return null|void
934
     */
935 18
    public function addContent($content, $type = null)
936
    {
937 18
        if (empty($type)) {
938 18
            $type = 'text/html;charset=UTF-8';
939
        }
940 18
        if (substr($type, 0, 9) == 'text/html' && !preg_match('/<html\b[^>]*>/i', $content)) {
941
            // string contains no <html> Tag => no complete document but an HTML fragment!
942 17
            $this->addHtmlFragment($content);
943
        } else {
944 2
            parent::addContent($content, $type);
945
        }
946 18
    }
947
948 16
    public function addHtmlFragment($content, $charset = 'UTF-8')
949
    {
950 16
        $d = new \DOMDocument('1.0', $charset);
951 16
        $d->preserveWhiteSpace = false;
952 16
        $root = $d->appendChild($d->createElement(self::FRAGMENT_ROOT_TAGNAME));
953 16
        $bodynode = Helpers::getBodyNodeFromHtmlFragment($content, $charset);
954 16
        foreach ($bodynode->childNodes as $child) {
955 16
            $inode = $root->appendChild($d->importNode($child, true));
956 16
            if ($inode) {
957 16
                $this->addNode($inode);
958
            }
959
        }
960 16
    }
961
962
//    /**
963
//     * returns the first node
964
//     * deprecated, use getNode(0) instead
965
//     *
966
//     * @return \DOMNode|null
967
//     * @deprecated
968
//     * @see Crawler::getNode
969
//     */
970
//    public function getFirstNode()
971
//    {
972
//        return $this->getNode(0);
973
//    }
974
975
//    /**
976
//     * @param int $position
977
//     *
978
//     * overridden from Crawler because it is not public in Symfony 2.3
979
//     * TODO: throw away as soon as we don't need to support SF 2.3 any more
980
//     *
981
//     * @return \DOMElement|null
982
//     */
983
//    public function getNode($position)
984
//    {
985
//        return parent::getNode($position);
986
//    }
987
//
988
//    /**
989
//     * Returns the node name of the first node of the list.
990
//     *
991
//     * in Crawler (parent), this function will be available starting with 2.6.0,
992
//     * therefore this method be removed from here as soon as we don't need to keep compatibility
993
//     * with Symfony < 2.6
994
//     *
995
//     * TODO: throw away as soon as we don't need to support SF 2.3 any more
996
//     *
997
//     * @return string The node name
998
//     *
999
//     * @throws \InvalidArgumentException When current node is empty
1000
//     */
1001
//    public function nodeName()
1002
//    {
1003
//        if (!count($this)) {
1004
//            throw new \InvalidArgumentException('The current node list is empty.');
1005
//        }
1006
//        return $this->getNode(0)->nodeName;
1007
//    }
1008
1009
    /**
1010
     * Adds a node to the current list of nodes.
1011
     *
1012
     * This method uses the appropriate specialized add*() method based
1013
     * on the type of the argument.
1014
     *
1015
     * Overwritten from parent to allow Crawler to be added
1016
     *
1017
     * @param null|\DOMNodeList|array|\DOMNode|Crawler $node A node
1018
     *
1019
     * @api
1020
     */
1021 30
    public function add($node)
1022
    {
1023 30
        if ($node instanceof Crawler) {
1024 1
            foreach ($node as $childnode) {
1025 1
                $this->addNode($childnode);
1026
            }
1027
        } else {
1028 30
            parent::add($node);
1029
        }
1030 30
    }
1031
1032
    /**
1033
     * @param \DOMNode $newnode
1034
     * @param \DOMNode $referencenode
1035
     * @param int $clone
1036
     * @return \DOMNode
1037
     */
1038 6
    protected static function importNewnode(\DOMNode $newnode, \DOMNode $referencenode, $clone = 0) {
1039 6
        if ($newnode->ownerDocument !== $referencenode->ownerDocument) {
1040 5
            $referencenode->ownerDocument->preserveWhiteSpace = false;
1041 5
            $newnode = $referencenode->ownerDocument->importNode($newnode, true);
1042
        } else {
1043 2
            if ($clone > 0) {
1044
                $newnode = $newnode->cloneNode(true);
1045
            }
1046
        }
1047 6
        return $newnode;
1048
    }
1049
1050
    /**
1051
     * Checks whether the first node in the set is disconnected (has no parent node)
1052
     *
1053
     * @return bool
1054
     */
1055 1
    public function isDisconnected()
1056
    {
1057 1
        $parent = $this->getNode(0)->parentNode;
1058 1
        return ($parent == null || $parent->tagName == self::FRAGMENT_ROOT_TAGNAME);
1059
    }
1060
1061 1
    public function __get($name)
1062
    {
1063
        switch ($name) {
1064 1
            case 'count':
1065 1
            case 'length':
1066 1
                return count($this);
1067
        }
1068 1
        throw new \Exception('No such property ' . $name);
1069
    }
1070
}
1071