Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/core/etl/src/Flow/ETL/Function/DOMElementNamespaceValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Function;

use Dom\HTMLElement;
use Dom\XPath;
use DOMDocument;
use DOMNode;
use DOMXPath;
use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\FlowContext;
use Flow\ETL\Row;

use function class_exists;
use function count;
use function Flow\Types\DSL\type_instance_of;
use function Flow\Types\DSL\type_list;
use function is_array;
use function reset;

final class DOMElementNamespaceValue extends ScalarFunctionChain
{
public function __construct(
private readonly ScalarFunction|DOMNode|HTMLElement $domElement,
private readonly ScalarFunction|string|null $attribute,
) {}

public function eval(Row $row, FlowContext $context): ?string
{
$types = [
type_instance_of(DOMNode::class),
type_list(type_instance_of(DOMNode::class)),
];

if (class_exists('\Dom\HTMLElement')) {
$types[] = type_instance_of(HTMLElement::class);
$types[] = type_list(type_instance_of(HTMLElement::class));
}

$node = (new Parameter($this->domElement))->as($row, $context, ...$types);

if ($node instanceof DOMDocument) {
$node = $node->documentElement;
}

if (is_array($node) && count($node)) {
$node = reset($node);
}

if ($node === null) {
return $context
->functions()
->invalidResult(new InvalidArgumentException('DOMElementNamespaceValue requires non-null DOMNode'));
}

if (!$node instanceof DOMNode && !$node instanceof HTMLElement) {
return null;
}

$attributeName = (new Parameter($this->attribute))->asString($row, $context);

if ($attributeName === null) {
return $node->namespaceURI;
}

if (null === $node->ownerDocument) {
return null;
}

if (class_exists('\Dom\XPath')) {
// @mago-ignore analysis:possibly-invalid-argument
$xpath = new XPath($node->ownerDocument);
} else {
// @mago-ignore analysis:possibly-invalid-argument
$xpath = new DOMXPath($node->ownerDocument);
}

/** @var \DOMNameSpaceNode $nsNode */
// @mago-ignore analysis:invalid-iterator
foreach ($xpath->query('namespace::*') ?: [] as $nsNode) {
Comment on lines +81 to +82

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New version always returns a traversable object, old returned array or false, and Mago has a problem detecting which is correct.

if ($nsNode->nodeName === $attributeName || str_starts_with($nsNode->nodeName, $attributeName . ':')) {
return $nsNode->nodeValue;
}
}

return null;
}
}
5 changes: 5 additions & 0 deletions src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ public function domElementAttributeValue(ScalarFunction|string $attribute): DOME
return new DOMElementAttributeValue($this, $attribute);
}

public function domElementNamespace(ScalarFunction|string|null $attribute = null): DOMElementNamespaceValue
{
return new DOMElementNamespaceValue($this, $attribute);
}

public function domElementNextSibling(bool $allowOnlyElement = false): DOMElementNextSibling
{
return new DOMElementNextSibling($this, $allowOnlyElement);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Tests\Unit\Function;

use Dom\HTMLDocument;
use Dom\HTMLElement;
use DOMDocument;
use DOMElement;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\TestCase;

use function Flow\ETL\DSL\config;
use function Flow\ETL\DSL\flow_context;
use function Flow\ETL\DSL\ref;
use function Flow\ETL\DSL\row;

use const LIBXML_HTML_NOIMPLIED;
use const LIBXML_NOERROR;

final class DOMElementNamespaceTest extends TestCase
{
private const string XML = <<<'XML'
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:sh="http://www.unece.org/cefact/nodes/StandardBusinessDocumentHeader">
</Invoice>
XML;

#[RequiresPhp('>= 8.4')]
public function test_html_getting_element_namespace(): void
{
// @mago-ignore analysis:unavailable-method
$element = HTMLDocument::createFromString(self::XML, LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR);

static::assertInstanceOf(HTMLElement::class, $element->documentElement);
static::assertSame('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', ref('node')
->domElementNamespace()
->eval(
row(flow_context(config())->entryFactory()->create('node', $element->documentElement)),
flow_context(),
));
}

public function test_xml_getting_element_namespace(): void
{
$xml = new DOMDocument();
$xml->loadXML(self::XML);

static::assertInstanceOf(DOMElement::class, $xml->documentElement);
static::assertSame('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', ref('node')
->domElementNamespace()
->eval(row(flow_context(config())->entryFactory()->create('node', $xml->documentElement)), flow_context()));
}

public function test_xml_getting_element_non_default_namespace(): void
{
$xml = new DOMDocument();
$xml->loadXML(self::XML);

static::assertInstanceOf(DOMElement::class, $xml->documentElement);
static::assertSame('http://www.unece.org/cefact/nodes/StandardBusinessDocumentHeader', ref('node')
->domElementNamespace('xmlns:sh')
->eval(row(flow_context(config())->entryFactory()->create('node', $xml->documentElement)), flow_context()));
}
}
2 changes: 1 addition & 1 deletion web/landing/resources/api.json

Large diffs are not rendered by default.

Loading