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
14 changes: 8 additions & 6 deletions src/core/etl/src/Flow/ETL/DSL/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Dom\Element;
use Dom\HTMLDocument;
use Dom\HTMLElement;
use Dom\XMLDocument;
use DOMDocument;
use DOMElement;
use Exception;
Expand Down Expand Up @@ -892,16 +894,16 @@ function uuid_entry(string $name, FlowUuid|string|null $value, ?Metadata $metada
/**
* @throws InvalidArgumentException
*
* @return ($value is null ? Entry<null> : Entry<\DOMDocument>)
* @return ($value is null ? Entry<null> : Entry<\DOMDocument|XMLDocument>)
*/
#[DocumentationDSL(module: Module::CORE, type: DSLType::ENTRY)]
function xml_entry(string $name, DOMDocument|string|null $value, ?Metadata $metadata = null): Entry
function xml_entry(string $name, DOMDocument|XMLDocument|string|null $value, ?Metadata $metadata = null): Entry
{
if ($value === null) {
return new XMLEntry($name, null, $metadata);
}

if ($value instanceof DOMDocument) {
if ($value instanceof DOMDocument || $value instanceof XMLDocument) {
return new XMLEntry($name, $value, $metadata);
}

Expand All @@ -917,16 +919,16 @@ function xml_entry(string $name, DOMDocument|string|null $value, ?Metadata $meta
/**
* @throws InvalidArgumentException
*
* @return ($value is null ? Entry<null> : Entry<\DOMElement>)
* @return ($value is null ? Entry<null> : Entry<\DOMElement|Element>)
*/
#[DocumentationDSL(module: Module::CORE, type: DSLType::ENTRY)]
function xml_element_entry(string $name, DOMElement|string|null $value, ?Metadata $metadata = null): Entry
function xml_element_entry(string $name, DOMElement|Element|string|null $value, ?Metadata $metadata = null): Entry
{
if ($value === null) {
return new XMLElementEntry($name, null, $metadata);
}

if ($value instanceof DOMElement) {
if ($value instanceof DOMElement || $value instanceof Element) {
return new XMLElementEntry($name, $value, $metadata);
}

Expand Down
88 changes: 88 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,88 @@
<?php

declare(strict_types=1);

namespace Flow\ETL\Function;

use Dom\Document;
use Dom\Element;
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\Element')) {
$types[] = type_instance_of(Element::class);
$types[] = type_list(type_instance_of(Element::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 Element) {
return null;
}

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

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

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

$document = $node->ownerDocument;

$xpath = $document instanceof Document ? new XPath($document) : new DOMXPath($document);

/** @var \DOMNameSpaceNode $nsNode */
// @mago-ignore analysis:invalid-iterator
foreach ($xpath->query('namespace::*') ?: [] as $nsNode) {
Comment thread
stloyd marked this conversation as resolved.
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
19 changes: 14 additions & 5 deletions src/core/etl/src/Flow/ETL/Row/Entry/XMLElementEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Flow\ETL\Row\Entry;

use Dom\Element;
use Dom\XMLDocument;
use DOMDocument;
use DOMElement;
use Flow\ETL\Exception\InvalidArgumentException;
Expand All @@ -24,7 +26,7 @@
use function sprintf;

/**
* @template-covariant T of \DOMElement|null
* @template-covariant T of \DOMElement|Element|null
*
* @implements Entry<T>
*/
Expand All @@ -39,7 +41,7 @@ final class XMLElementEntry implements Entry
*/
public function __construct(
private readonly string $name,
private readonly ?DOMElement $value,
private readonly DOMElement|Element|null $value,
?Metadata $metadata = null,
) {
$this->definition = new XMLElementDefinition(
Expand Down Expand Up @@ -168,7 +170,14 @@ public function toString(): string
return '';
}

$serialized = $ownerDocument->saveXML($this->value);
if ($ownerDocument instanceof XMLDocument) {
// @mago-ignore analysis:possibly-invalid-argument
$serialized = $ownerDocument->saveXml($this->value);

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.

In official docs, this method is not documented, but it's visible in the official stub.

} else {
/** @var string|false $serialized */
// @mago-ignore analysis:possibly-invalid-argument,non-existent-method
$serialized = $ownerDocument->saveXML($this->value);
}

if ($serialized === false) {
throw new RuntimeException('Failed to serialize XML element.');
Expand All @@ -178,7 +187,7 @@ public function toString(): string
}

/**
* @return Type<DOMElement>
* @return Type<DOMElement|Element>
*/
public function type(): Type
{
Expand All @@ -188,7 +197,7 @@ public function type(): Type
/**
* @return T
*/
public function value(): ?DOMElement
public function value(): DOMElement|Element|null
{
return $this->value;
}
Expand Down
16 changes: 11 additions & 5 deletions src/core/etl/src/Flow/ETL/Row/Entry/XMLEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flow\ETL\Row\Entry;

use Dom\XMLDocument;
use DOMDocument;
use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\Row\Entry;
Expand All @@ -23,7 +24,7 @@
use function sprintf;

/**
* @template-covariant T of \DOMDocument|null
* @template-covariant T of \DOMDocument|XMLDocument|null
*
* @implements Entry<T>
*/
Expand All @@ -38,7 +39,7 @@ final class XMLEntry implements Entry
*/
public function __construct(
private readonly string $name,
private readonly ?DOMDocument $value,
private readonly DOMDocument|XMLDocument|null $value,
?Metadata $metadata = null,
) {
$this->definition = new XMLDefinition($this->name, $this->value === null, $metadata ?: Metadata::empty());
Expand Down Expand Up @@ -153,7 +154,12 @@ public function toString(): string
return '';
}

$serialized = $this->value->saveXML($this->value->documentElement);
if ($this->value instanceof XMLDocument) {
$serialized = $this->value->saveXml($this->value->documentElement);
} else {
// @mago-ignore analysis:possibly-invalid-argument,possibly-invalid-argument
$serialized = $this->value->saveXML($this->value->documentElement);
Comment on lines +157 to +161

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.

Mago has a problem holding the Dom object type, even with an instance of pointing to its new or old version.

}

if ($serialized === false) {
throw new RuntimeException('Failed to serialize XML document.');
Expand All @@ -163,7 +169,7 @@ public function toString(): string
}

/**
* @return Type<DOMDocument>
* @return Type<DOMDocument|XMLDocument>
*/
public function type(): Type
{
Expand All @@ -173,7 +179,7 @@ public function type(): Type
/**
* @return T
*/
public function value(): ?DOMDocument
public function value(): DOMDocument|XMLDocument|null
{
return $this->value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flow\ETL\Schema\Definition;

use Dom\XMLDocument;
use Flow\ETL\Exception\RuntimeException;
use Flow\ETL\Row\Entry;
use Flow\ETL\Row\EntryReference;
Expand All @@ -18,7 +19,7 @@
use function sprintf;

/**
* @implements Definition<\DOMDocument>
* @implements Definition<\DOMDocument|XMLDocument>
*/
final class XMLDefinition implements Definition
{
Expand All @@ -27,7 +28,7 @@ final class XMLDefinition implements Definition
private readonly Reference $ref;

/**
* @var Type<\DOMDocument>
* @var Type<\DOMDocument|XMLDocument>
*/
private readonly Type $type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use function sprintf;

/**
* @implements Definition<\DOMElement>
* @implements Definition<\DOMElement|\Dom\Element>
*/
final class XMLElementDefinition implements Definition
{
Expand All @@ -27,7 +27,7 @@ final class XMLElementDefinition implements Definition
private readonly Reference $ref;

/**
* @var Type<\DOMElement>
* @var Type<\DOMElement|\Dom\Element>
*/
private readonly Type $type;

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\Element;
use Dom\XMLDocument;
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 = XMLDocument::createFromString(self::XML, LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR);

static::assertInstanceOf(Element::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()));
}
}
Loading
Loading