Skip to content
Merged
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
33 changes: 33 additions & 0 deletions docs/dom.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,22 @@ Document::fromUnsafeDocument(
);
```

#### promote_namespaces

This configurator moves all prefixed namespace declarations from child elements to the document root element.
Unlike `optimize_namespaces`, it preserves the original prefix names.
This is useful when dealing with servers that require all namespace declarations on the root element.

```php
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Configurator\promote_namespaces;

Document::fromUnsafeDocument(
$document,
promote_namespaces()
);
```

#### pretty_print

Makes the output of the DOM document human-readable.
Expand Down Expand Up @@ -1008,6 +1024,23 @@ $doc->manipulate(
);
```

#### promote_namespaces

Moves all prefixed namespace declarations from child elements to the document root element, preserving the original prefix names.

```php
use \Dom\XMLDocument;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Manipulator\Document\promote_namespaces;

$doc = Document::empty();
$doc->manipulate(
static function (XMLDocument $document): void {
promote_namespaces($document);
}
);
```

### Element

Element specific manipulators operate on `Dom\Element` instances.
Expand Down
21 changes: 21 additions & 0 deletions src/Xml/Dom/Configurator/promote_namespaces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Configurator;

use Closure;
use Dom\XMLDocument;
use function VeeWee\Xml\Dom\Manipulator\Document\promote_namespaces as promote_namespaces_manipulator;

/**
* @return Closure(XMLDocument): XMLDocument
*/
function promote_namespaces(): Closure
{
return static function (XMLDocument $document): XMLDocument {
promote_namespaces_manipulator($document);

return $document;
};
}
48 changes: 48 additions & 0 deletions src/Xml/Dom/Manipulator/Document/promote_namespaces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Manipulator\Document;

use Dom\Attr;
use Dom\XMLDocument;
use VeeWee\Xml\Exception\RuntimeException;
use function Psl\Dict\pull;
use function VeeWee\Xml\Dom\Builder\xmlns_attribute;
use function VeeWee\Xml\Dom\Locator\Attribute\xmlns_attributes_list;
use function VeeWee\Xml\Dom\Locator\document_element;
use function VeeWee\Xml\Dom\Manipulator\Node\remove_namespace;

/**
* @throws RuntimeException
*/
function promote_namespaces(XMLDocument $document): void
{
$documentElement = document_element()($document);

/** @var array<string, string> $promoted prefix => URI */
$promoted = pull(
xmlns_attributes_list($documentElement)->filter(static fn (Attr $attr): bool => $attr->prefix !== null),
static fn (Attr $attr): string => $attr->value,
static fn (Attr $attr): string => $attr->localName,
);

foreach ($documentElement->getElementsByTagName('*') as $element) {
$prefixedXmlns = xmlns_attributes_list($element)
->filter(static fn (Attr $attr): bool => $attr->prefix !== null);

foreach ($prefixedXmlns as $attr) {
$prefix = $attr->localName;
$uri = $attr->value;

if (!array_key_exists($prefix, $promoted)) {
xmlns_attribute($prefix, $uri)($documentElement);
$promoted[$prefix] = $uri;
}

if ($promoted[$prefix] === $uri) {
remove_namespace($attr, $element);
}
}
}
}
2 changes: 2 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'Xml\Dom\Configurator\normalize' => __DIR__.'/Xml/Dom/Configurator/normalize.php',
'Xml\Dom\Configurator\optimize_namespaces' => __DIR__.'/Xml/Dom/Configurator/optimize_namespaces.php',
'Xml\Dom\Configurator\pretty_print' => __DIR__.'/Xml/Dom/Configurator/pretty_print.php',
'Xml\Dom\Configurator\promote_namespaces' => __DIR__.'/Xml/Dom/Configurator/promote_namespaces.php',
'Xml\Dom\Configurator\traverse' => __DIR__.'/Xml/Dom/Configurator/traverse.php',
'Xml\Dom\Configurator\trim_spaces' => __DIR__.'/Xml/Dom/Configurator/trim_spaces.php',
'Xml\Dom\Configurator\utf8' => __DIR__.'/Xml/Dom/Configurator/utf8.php',
Expand Down Expand Up @@ -59,6 +60,7 @@
'Xml\Dom\Locator\root_namespace' => __DIR__.'/Xml/Dom/Locator/root_namespace.php',
'Xml\Dom\Manipulator\Attribute\rename' => __DIR__.'/Xml/Dom/Manipulator/Attribute/rename.php',
'Xml\Dom\Manipulator\Document\optimize_namespaces' => __DIR__.'/Xml/Dom/Manipulator/Document/optimize_namespaces.php',
'Xml\Dom\Manipulator\Document\promote_namespaces' => __DIR__.'/Xml/Dom/Manipulator/Document/promote_namespaces.php',
'Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes' => __DIR__.'/Xml/Dom/Manipulator/Element/copy_named_xmlns_attributes.php',
'Xml\Dom\Manipulator\Element\rename' => __DIR__.'/Xml/Dom/Manipulator/Element/rename.php',
'Xml\Dom\Manipulator\Node\append_external_node' => __DIR__.'/Xml/Dom/Manipulator/Node/append_external_node.php',
Expand Down
81 changes: 81 additions & 0 deletions tests/Xml/Dom/Configurator/PromoteNamespacesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Configurator;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Configurator\promote_namespaces;
use function VeeWee\Xml\Dom\Locator\document_element;
use function VeeWee\Xml\Dom\Mapper\xml_string;

final class PromoteNamespacesTest extends TestCase
{
#[DataProvider('provideXmls')]
public function test_it_can_promote_namespaces(string $input, string $expected): void
{
$doc = Document::fromXmlString($input, promote_namespaces());
$actual = xml_string()($doc->map(document_element()));

static::assertSame($expected, $actual);
}

public static function provideXmls(): iterable
{
yield 'no-action' => [
'<hello/>',
'<hello/>',
];

yield 'child-to-root' => [
<<<EOXML
<foo>
<bar xmlns:a="http://a"><a:baz/></bar>
</foo>
EOXML,
<<<EOXML
<foo xmlns:a="http://a">
<bar><a:baz/></bar>
</foo>
EOXML,
];

yield 'mixed-namespaces' => [
<<<EOXML
<foo>
<bar xmlns:a="http://a"><a:x/></bar>
<baz xmlns:b="http://b"><b:y/></baz>
</foo>
EOXML,
<<<EOXML
<foo xmlns:a="http://a" xmlns:b="http://b">
<bar><a:x/></bar>
<baz><b:y/></baz>
</foo>
EOXML,
];

yield 'soap-like' => [
<<<EOXML
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<tns:getUser xmlns:tns="https://example.com">
<tns:id xmlns:tns="https://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:int" xmlns:xsd="http://www.w3.org/2001/XMLSchema">1</tns:id>
</tns:getUser>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
EOXML,
<<<EOXML
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="https://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<tns:getUser>
<tns:id xsi:type="xsd:int">1</tns:id>
</tns:getUser>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
EOXML,
];
}
}
Loading