Skip to content

Commit dd63b4b

Browse files
stloydnorberttech
authored andcommitted
Add a new DOMElementSibling
1 parent ad79b74 commit dd63b4b

File tree

6 files changed

+178
-4
lines changed

6 files changed

+178
-4
lines changed

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ parameters:
8787

8888
ignoreErrors:
8989
-
90-
message: '#Dom\\(HTMLDocument|HTMLElement|Element)#i'
90+
message: '#Dom\\(CharacterData|HTMLDocument|HTMLElement|Element)#i'
9191
identifier: class.notFound
9292

9393
includes:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Function\DOM;
6+
7+
enum ElementSibling : string
8+
{
9+
case NEXT = 'next';
10+
case PREVIOUS = 'previous';
11+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Function;
6+
7+
use function Flow\Types\DSL\type_instance_of;
8+
use Dom\{CharacterData, HTMLElement};
9+
use Flow\ETL\{Exception\InvalidArgumentException, FlowContext, Function\DOM\ElementSibling, Row};
10+
11+
final class DOMElementSibling extends ScalarFunctionChain
12+
{
13+
public function __construct(
14+
private readonly ScalarFunction|\DOMNode|CharacterData|HTMLElement $element,
15+
private readonly ElementSibling $sibling,
16+
private readonly bool $allowOnlyElement,
17+
) {
18+
}
19+
20+
public function eval(Row $row, FlowContext $context) : \DOMNode|CharacterData|HTMLElement|null
21+
{
22+
$types = [
23+
type_instance_of(\DOMNode::class),
24+
];
25+
26+
if (\class_exists('\Dom\HTMLElement')) {
27+
$types[] = type_instance_of(CharacterData::class);
28+
$types[] = type_instance_of(HTMLElement::class);
29+
}
30+
31+
$node = (new Parameter($this->element))->as(
32+
$row,
33+
$context,
34+
...$types
35+
);
36+
37+
if ($node instanceof \DOMDocument) {
38+
$node = $node->documentElement;
39+
}
40+
41+
if ($this->allowOnlyElement) {
42+
if (!$node instanceof \DOMElement) {
43+
return $context->functions()->invalidResult(new InvalidArgumentException('DOMElementSibling with option $allowOnlyElement requires DOMElement.'));
44+
}
45+
46+
if ($node instanceof CharacterData) {
47+
return $context->functions()->invalidResult(new InvalidArgumentException('DOMElementSibling with option $allowOnlyElement requires HTMLElement.'));
48+
}
49+
50+
return $this->sibling === ElementSibling::NEXT ? $node->nextElementSibling : $node->previousElementSibling;
51+
}
52+
53+
/* @phpstan-ignore-next-line */
54+
return $this->sibling === ElementSibling::NEXT ? $node->nextSibling : $node->previousSibling;
55+
}
56+
}

src/core/etl/src/Flow/ETL/Function/DOMElementValue.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
namespace Flow\ETL\Function;
66

77
use function Flow\Types\DSL\{type_instance_of, type_list};
8-
use Dom\HTMLElement;
8+
use Dom\{CharacterData, HTMLElement};
99
use Flow\ETL\{FlowContext, Row};
1010

1111
final class DOMElementValue extends ScalarFunctionChain
1212
{
13-
public function __construct(private readonly ScalarFunction|\DOMNode|HTMLElement $node)
13+
public function __construct(private readonly ScalarFunction|\DOMNode|CharacterData|HTMLElement $node)
1414
{
1515
}
1616

@@ -22,6 +22,7 @@ public function eval(Row $row, FlowContext $context) : mixed
2222
];
2323

2424
if (\class_exists('\Dom\HTMLElement')) {
25+
$types[] = type_instance_of(CharacterData::class);
2526
$types[] = type_instance_of(HTMLElement::class);
2627
$types[] = type_list(type_instance_of(HTMLElement::class));
2728
}
@@ -44,7 +45,7 @@ public function eval(Row $row, FlowContext $context) : mixed
4445
return $node->nodeValue;
4546
}
4647

47-
if ($node instanceof HTMLElement) {
48+
if ($node instanceof CharacterData || $node instanceof HTMLElement) {
4849
return $node->textContent;
4950
}
5051

src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Flow\ETL\Function\ArrayExpand\ArrayExpand;
1212
use Flow\ETL\Function\ArraySort\Sort;
1313
use Flow\ETL\Function\Between\Boundary;
14+
use Flow\ETL\Function\DOM\ElementSibling;
1415
use Flow\ETL\Function\StyleConverter\StringStyles as OldStringStyles;
1516
use Flow\ETL\Hash\{Algorithm, NativePHPHash};
1617
use Flow\ETL\String\StringStyles;
@@ -246,6 +247,11 @@ public function domElementParent() : DOMElementParent
246247
return new DOMElementParent($this);
247248
}
248249

250+
public function domElementSibling(ElementSibling $sibling, bool $allowOnlyElement = false) : DOMElementSibling
251+
{
252+
return new DOMElementSibling($this, $sibling, $allowOnlyElement);
253+
}
254+
249255
public function domElementValue() : DOMElementValue
250256
{
251257
return new DOMElementValue($this);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Tests\Integration\Function;
6+
7+
use function Flow\ETL\DSL\{df, from_rows, html_element_entry, ref, row, rows, xml_element_entry};
8+
use Flow\ETL\Function\DOM\ElementSibling;
9+
use Flow\ETL\Tests\FlowTestCase;
10+
use PHPUnit\Framework\Attributes\RequiresPhp;
11+
12+
final class DOMElementSiblingTest extends FlowTestCase
13+
{
14+
#[RequiresPhp('>= 8.4')]
15+
public function test_dom_element_sibling_text_value() : void
16+
{
17+
$rows = df()
18+
->read(from_rows(
19+
rows(
20+
row(
21+
html_element_entry('html_element', '<article><section><h1>User Name</h1></section>01</article>')
22+
)
23+
)
24+
))
25+
->withEntry('user_details', ref('html_element')->htmlQuerySelector('section'))
26+
->withEntry('user_name', ref('user_details')->htmlQuerySelector('h1')->domElementValue())
27+
->withEntry('user_id', ref('user_details')->domElementSibling(ElementSibling::NEXT)->domElementValue())
28+
->select('user_name', 'user_id')
29+
->fetch();
30+
31+
self::assertSame(
32+
[
33+
[
34+
'user_name' => 'User Name',
35+
'user_id' => '01',
36+
],
37+
],
38+
$rows->toArray()
39+
);
40+
}
41+
42+
#[RequiresPhp('>= 8.4')]
43+
public function test_dom_element_sibling_text_value_when_only_element_is_allowed() : void
44+
{
45+
$rows = df()
46+
->read(from_rows(
47+
rows(
48+
row(
49+
html_element_entry('html_element', '<article><section><h1>User Name</h1></section>01</article>')
50+
)
51+
)
52+
))
53+
->withEntry('user_details', ref('html_element')->htmlQuerySelector('section'))
54+
->withEntry('user_name', ref('user_details')->htmlQuerySelector('h1')->domElementValue())
55+
->withEntry('user_id', ref('user_details')->domElementSibling(ElementSibling::NEXT, true)->domElementValue())
56+
->select('user_name', 'user_id')
57+
->fetch();
58+
59+
self::assertSame(
60+
[
61+
[
62+
'user_name' => 'User Name',
63+
'user_id' => null,
64+
],
65+
],
66+
$rows->toArray()
67+
);
68+
}
69+
70+
public function test_xml_sibling_element_value() : void
71+
{
72+
$dom = new \DOMDocument();
73+
$dom->loadXML('<user><name>User Name</name><number>01</number></user>');
74+
75+
$rows = df()
76+
->read(
77+
from_rows(
78+
rows(
79+
row(
80+
xml_element_entry('xml_element', $dom->getElementsByTagName('name')->item(0))
81+
)
82+
)
83+
)
84+
)
85+
->withEntry('user_name', ref('xml_element')->domElementValue())
86+
->withEntry('user_id', ref('xml_element')->domElementSibling(ElementSibling::NEXT)->domElementValue())
87+
->select('user_name', 'user_id')
88+
->fetch();
89+
90+
self::assertSame(
91+
[
92+
[
93+
'user_name' => 'User Name',
94+
'user_id' => '01',
95+
],
96+
],
97+
$rows->toArray()
98+
);
99+
}
100+
}

0 commit comments

Comments
 (0)