Skip to content

Commit ba471f2

Browse files
Merge branch '4.4'
* 4.4: Add return types to internal & magic methods when possible fixed CSC Add Address::fromString [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml()
2 parents ef0d51b + c7f96ae commit ba471f2

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
@@ -11,6 +11,9 @@ CHANGELOG
1111
-----
1212

1313
* Added `Form::getName()` method.
14+
* Added `Crawler::matches()` method.
15+
* Added `Crawler::closest()` method.
16+
* Added `Crawler::outerHtml()` method.
1417

1518
4.3.0
1619
-----

Crawler.php

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

415+
public function matches(string $selector): bool
416+
{
417+
if (!$this->nodes) {
418+
return false;
419+
}
420+
421+
$converter = $this->createCssSelectorConverter();
422+
$xpath = $converter->toXPath($selector, 'self::');
423+
424+
return 0 !== $this->filterRelativeXPath($xpath)->count();
425+
}
426+
427+
/**
428+
* Return first parents (heading toward the document root) of the Element that matches the provided selector.
429+
*
430+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
431+
*
432+
* @throws \InvalidArgumentException When current node is empty
433+
*/
434+
public function closest(string $selector): ?self
435+
{
436+
if (!$this->nodes) {
437+
throw new \InvalidArgumentException('The current node list is empty.');
438+
}
439+
440+
$domNode = $this->getNode(0);
441+
442+
while (XML_ELEMENT_NODE === $domNode->nodeType) {
443+
$node = $this->createSubCrawler($domNode);
444+
if ($node->matches($selector)) {
445+
return $node;
446+
}
447+
448+
$domNode = $node->getNode(0)->parentNode;
449+
}
450+
451+
return null;
452+
}
453+
415454
/**
416455
* Returns the next siblings nodes of the current selection.
417456
*
@@ -585,6 +624,22 @@ public function html($default = null)
585624
return $html;
586625
}
587626

627+
public function outerHtml(): string
628+
{
629+
if (!\count($this)) {
630+
throw new \InvalidArgumentException('The current node list is empty.');
631+
}
632+
633+
$node = $this->getNode(0);
634+
$owner = $node->ownerDocument;
635+
636+
if (null !== $this->html5Parser && '<!DOCTYPE html>' === $owner->saveXML($owner->childNodes[0])) {
637+
$owner = $this->html5Parser;
638+
}
639+
640+
return $owner->saveHTML($node);
641+
}
642+
588643
/**
589644
* Evaluates an XPath expression.
590645
*

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)