Skip to content

Commit c7f96ae

Browse files
committed
[DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml()
1 parent 7897eff commit c7f96ae

File tree

3 files changed

+157
-0
lines changed

3 files changed

+157
-0
lines changed

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
-----

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
*

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)