Skip to content

Commit a77e326

Browse files
committed
feature #33144 [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml() (lyrixx)
This PR was merged into the 4.4 branch. Discussion ---------- [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml() | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #18609 | License | MIT | Doc PR | - Commits ------- 9535f9e [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml()
2 parents 1981f06 + 9535f9e commit a77e326

File tree

3 files changed

+157
-0
lines changed

3 files changed

+157
-0
lines changed

src/Symfony/Component/DomCrawler/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ CHANGELOG
55
-----
66

77
* Added `Form::getName()` method.
8+
* Added `Crawler::matches()` method.
9+
* Added `Crawler::closest()` method.
10+
* Added `Crawler::outerHtml()` method.
811

912
4.3.0
1013
-----

src/Symfony/Component/DomCrawler/Crawler.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,45 @@ public function siblings()
427427
return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild));
428428
}
429429

430+
public function matches(string $selector): bool
431+
{
432+
if (!$this->nodes) {
433+
return false;
434+
}
435+
436+
$converter = $this->createCssSelectorConverter();
437+
$xpath = $converter->toXPath($selector, 'self::');
438+
439+
return 0 !== $this->filterRelativeXPath($xpath)->count();
440+
}
441+
442+
/**
443+
* Return first parents (heading toward the document root) of the Element that matches the provided selector.
444+
*
445+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
446+
*
447+
* @throws \InvalidArgumentException When current node is empty
448+
*/
449+
public function closest(string $selector): ?self
450+
{
451+
if (!$this->nodes) {
452+
throw new \InvalidArgumentException('The current node list is empty.');
453+
}
454+
455+
$domNode = $this->getNode(0);
456+
457+
while (XML_ELEMENT_NODE === $domNode->nodeType) {
458+
$node = $this->createSubCrawler($domNode);
459+
if ($node->matches($selector)) {
460+
return $node;
461+
}
462+
463+
$domNode = $node->getNode(0)->parentNode;
464+
}
465+
466+
return null;
467+
}
468+
430469
/**
431470
* Returns the next siblings nodes of the current selection.
432471
*
@@ -609,6 +648,22 @@ public function html(/* $default = null */)
609648
return $html;
610649
}
611650

651+
public function outerHtml(): string
652+
{
653+
if (!\count($this)) {
654+
throw new \InvalidArgumentException('The current node list is empty.');
655+
}
656+
657+
$node = $this->getNode(0);
658+
$owner = $node->ownerDocument;
659+
660+
if (null !== $this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) {
661+
$owner = $this->html5Parser;
662+
}
663+
664+
return $owner->saveHTML($node);
665+
}
666+
612667
/**
613668
* Evaluates an XPath expression.
614669
*

src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,105 @@ public function testSiblings()
880880
}
881881
}
882882

883+
public function provideMatchTests()
884+
{
885+
yield ['#foo', true, '#foo'];
886+
yield ['#foo', true, '.foo'];
887+
yield ['#foo', true, '.other'];
888+
yield ['#foo', false, '.bar'];
889+
890+
yield ['#bar', true, '#bar'];
891+
yield ['#bar', true, '.bar'];
892+
yield ['#bar', true, '.other'];
893+
yield ['#bar', false, '.foo'];
894+
}
895+
896+
/** @dataProvider provideMatchTests */
897+
public function testMatch(string $mainNodeSelector, bool $expected, string $selector)
898+
{
899+
$html = <<<'HTML'
900+
<html lang="en">
901+
<body>
902+
<div id="foo" class="foo other">
903+
<div>
904+
<div id="bar" class="bar other"></div>
905+
</div>
906+
</div>
907+
</body>
908+
</html>
909+
HTML;
910+
911+
$crawler = $this->createCrawler($this->getDoctype().$html);
912+
$node = $crawler->filter($mainNodeSelector);
913+
$this->assertSame($expected, $node->matches($selector));
914+
}
915+
916+
public function testClosest()
917+
{
918+
$html = <<<'HTML'
919+
<html lang="en">
920+
<body>
921+
<div class="lorem2 ok">
922+
<div>
923+
<div class="lorem3 ko"></div>
924+
</div>
925+
<div class="lorem1 ok">
926+
<div id="foo" class="newFoo ok">
927+
<div class="lorem1 ko"></div>
928+
</div>
929+
</div>
930+
</div>
931+
<div class="lorem2 ko">
932+
</div>
933+
</body>
934+
</html>
935+
HTML;
936+
937+
$crawler = $this->createCrawler($this->getDoctype().$html);
938+
$foo = $crawler->filter('#foo');
939+
940+
$newFoo = $foo->closest('#foo');
941+
$this->assertInstanceOf(Crawler::class, $newFoo);
942+
$this->assertSame('newFoo ok', $newFoo->attr('class'));
943+
944+
$lorem1 = $foo->closest('.lorem1');
945+
$this->assertInstanceOf(Crawler::class, $lorem1);
946+
$this->assertSame('lorem1 ok', $lorem1->attr('class'));
947+
948+
$lorem2 = $foo->closest('.lorem2');
949+
$this->assertInstanceOf(Crawler::class, $lorem2);
950+
$this->assertSame('lorem2 ok', $lorem2->attr('class'));
951+
952+
$lorem3 = $foo->closest('.lorem3');
953+
$this->assertNull($lorem3);
954+
955+
$notFound = $foo->closest('.not-found');
956+
$this->assertNull($notFound);
957+
}
958+
959+
public function testOuterHtml()
960+
{
961+
$html = <<<'HTML'
962+
<html lang="en">
963+
<body>
964+
<div class="foo">
965+
<ul>
966+
<li>1</li>
967+
<li>2</li>
968+
<li>3</li>
969+
</ul>
970+
</body>
971+
</html>
972+
HTML;
973+
974+
$crawler = $this->createCrawler($this->getDoctype().$html);
975+
$bar = $crawler->filter('ul');
976+
$output = $bar->outerHtml();
977+
$output = str_replace([' ', "\n"], '', $output);
978+
$expected = '<ul><li>1</li><li>2</li><li>3</li></ul>';
979+
$this->assertSame($expected, $output);
980+
}
981+
883982
public function testNextAll()
884983
{
885984
$crawler = $this->createTestCrawler()->filterXPath('//li')->eq(1);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy