diff --git a/psalm.xml b/psalm.xml
index c362257..0dd8970 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -27,4 +27,7 @@
+
+
+
diff --git a/src/Xml/Configurator/FlattenWsdlImports.php b/src/Xml/Configurator/FlattenWsdlImports.php
index 6646cc7..767b20e 100644
--- a/src/Xml/Configurator/FlattenWsdlImports.php
+++ b/src/Xml/Configurator/FlattenWsdlImports.php
@@ -16,6 +16,7 @@
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Locator\document_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
+use function VeeWee\Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes;
use function VeeWee\Xml\Dom\Manipulator\Node\append_external_node;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
use function VeeWee\Xml\Dom\Manipulator\Node\replace_by_external_nodes;
@@ -82,6 +83,7 @@ private function importWsdlImportElement(DOMElement $import): void
private function importWsdlPart(DOMElement $importElement, Document $importedDocument): void
{
$definitions = $importedDocument->map(document_element());
+ copy_named_xmlns_attributes($importElement->ownerDocument->documentElement, $definitions);
replace_by_external_nodes(
$importElement,
diff --git a/src/Xml/Configurator/FlattenXsdImports.php b/src/Xml/Configurator/FlattenXsdImports.php
index 05d5c56..4f0a484 100644
--- a/src/Xml/Configurator/FlattenXsdImports.php
+++ b/src/Xml/Configurator/FlattenXsdImports.php
@@ -12,6 +12,8 @@
use Soap\Wsdl\Loader\Context\FlatteningContext;
use Soap\Wsdl\Uri\IncludePathBuilder;
use Soap\Wsdl\Xml\Exception\FlattenException;
+use Soap\Wsdl\Xml\Xmlns\FixRemovedDefaultXmlnsDeclarationsDuringImport;
+use Soap\Wsdl\Xml\Xmlns\RegisterNonConflictingXmlnsNamespaces;
use Soap\Xml\Xpath\WsdlPreset;
use VeeWee\Xml\Dom\Configurator\Configurator;
use VeeWee\Xml\Dom\Document;
@@ -21,7 +23,6 @@
use function Psl\Vec\reverse;
use function VeeWee\Xml\Dom\Assert\assert_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
-use function VeeWee\Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes;
use function VeeWee\Xml\Dom\Manipulator\Node\append_external_node;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
@@ -170,6 +171,7 @@ private function loadSchema(string $location): ?DOMElement
* This function registers the newly provided schema in the WSDL types section.
* It groups all imports by targetNamespace.
*
+ * @throws \RuntimeException
* @throws RuntimeException
* @throws AssertException
*/
@@ -187,42 +189,19 @@ private function registerSchemaInTypes(DOMElement $schema): void
// If no schema exists yet: Add the newly loaded schema as a completely new schema in the WSDL types.
if (!$existingSchema) {
$imported = assert_element(append_external_node($types, $schema));
- $this->fixRemovedDefaultXmlnsDeclarationsDuringImport($imported, $schema);
+ (new FixRemovedDefaultXmlnsDeclarationsDuringImport())($imported, $schema);
return;
}
// When an existing schema exists, all xmlns attributes need to be copied.
// This is to make sure that possible QNames (strings) get resolved in XSD.
// Finally - all children of the newly loaded schema can be appended to the existing schema.
- copy_named_xmlns_attributes($existingSchema, $schema);
- $this->fixRemovedDefaultXmlnsDeclarationsDuringImport($existingSchema, $schema);
+ (new RegisterNonConflictingXmlnsNamespaces())($existingSchema, $schema);
children($schema)->forEach(
static fn (DOMNode $node) => append_external_node($existingSchema, $node)
);
}
- /**
- * @see https://gist.github.com/veewee/32c3aa94adcf878700a9d5baa4b2a2de
- *
- * PHP does an optimization of namespaces during `importNode()`.
- * In some cases, this causes the root xmlns to be removed from the imported node which could lead to xsd qname errors.
- *
- * This function tries to re-add the root xmlns if it's available on the source but not on the target.
- *
- * It will most likely be solved in PHP 8.4's new spec compliant DOM\XMLDocument implementation.
- * @see https://github.com/php/php-src/pull/13031
- *
- * For now, this will do the trick.
- */
- private function fixRemovedDefaultXmlnsDeclarationsDuringImport(DOMElement $target, DOMElement $source): void
- {
- if (!$source->getAttribute('xmlns') || $target->hasAttribute('xmlns')) {
- return;
- }
-
- $target->setAttribute('xmlns', $source->getAttribute('xmlns'));
- }
-
/**
* Makes sure to rearrange the import statements on top of the flattened XSD schema.
* This makes the flattened XSD spec compliant:
diff --git a/src/Xml/Visitor/ReprefixTypeQname.php b/src/Xml/Visitor/ReprefixTypeQname.php
new file mode 100644
index 0000000..dab0375
--- /dev/null
+++ b/src/Xml/Visitor/ReprefixTypeQname.php
@@ -0,0 +1,40 @@
+ $prefixMap - "From" key - "To" value prefix map
+ */
+ public function __construct(
+ private readonly array $prefixMap
+ ) {
+ }
+
+ public function onNodeEnter(DOMNode $node): Action
+ {
+ if (!is_attribute($node) || $node->localName !== 'type') {
+ return new Action\Noop();
+ }
+
+ $parts = explode(':', $node->nodeValue ?? '', 2);
+ if (count($parts) !== 2) {
+ return new Action\Noop();
+ }
+
+ [$currentPrefix, $currentTypeName] = $parts;
+ if (!array_key_exists($currentPrefix, $this->prefixMap)) {
+ return new Action\Noop();
+ }
+
+ $node->nodeValue = $this->prefixMap[$currentPrefix].':'.$currentTypeName;
+
+ return new Action\Noop();
+ }
+}
diff --git a/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php b/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php
new file mode 100644
index 0000000..3d1a244
--- /dev/null
+++ b/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php
@@ -0,0 +1,30 @@
+getAttribute('xmlns') || $target->hasAttribute('xmlns')) {
+ return;
+ }
+
+ $target->setAttribute('xmlns', $source->getAttribute('xmlns'));
+ }
+}
diff --git a/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php b/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php
new file mode 100644
index 0000000..95a4748
--- /dev/null
+++ b/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php
@@ -0,0 +1,129 @@
+
+ */
+final class RegisterNonConflictingXmlnsNamespaces
+{
+ /**
+ * @throws RuntimeException
+ */
+ public function __invoke(DOMElement $existingSchema, DOMElement $newSchema): void
+ {
+ $existingLinkedNamespaces = linked_namespaces($existingSchema);
+
+ $rePrefixMap = linked_namespaces($newSchema)->reduce(
+ /**
+ * @param RePrefixMap $rePrefixMap
+ * @return RePrefixMap
+ */
+ function (array $rePrefixMap, DOMNameSpaceNode $xmlns) use ($existingSchema, $existingLinkedNamespaces): array {
+ // Skip non-named xmlns attributes:
+ if (!$xmlns->prefix) {
+ return $rePrefixMap;
+ }
+
+ // Check for duplicates:
+ if ($existingSchema->hasAttribute($xmlns->nodeName) && $existingSchema->getAttribute($xmlns->nodeName) !== $xmlns->prefix) {
+ return merge(
+ $rePrefixMap,
+ // Can be improved with orElse when we are using PSL V3.
+ $this->tryUsingExistingPrefix($existingLinkedNamespaces, $xmlns)
+ ->unwrapOrElse(
+ fn () => $this->tryUsingUniquePrefixHash($existingSchema, $xmlns)
+ ->unwrapOrElse(
+ static fn () => throw new RuntimeException('Could not resolve conflicting namespace declarations whilst flattening your WSDL file.')
+ )
+ )
+ );
+ }
+
+ xmlns_attribute($xmlns->prefix, $xmlns->namespaceURI)($existingSchema);
+
+ return $rePrefixMap;
+ },
+ []
+ );
+
+ if (count($rePrefixMap)) {
+ Document::fromUnsafeDocument($newSchema->ownerDocument)->traverse(new ReprefixTypeQname($rePrefixMap));
+ }
+ (new FixRemovedDefaultXmlnsDeclarationsDuringImport())($existingSchema, $newSchema);
+ }
+
+ /**
+ * @param NodeList $existingLinkedNamespaces
+ *
+ * @return Option
+ */
+ private function tryUsingExistingPrefix(
+ NodeList $existingLinkedNamespaces,
+ DOMNameSpaceNode $xmlns
+ ): Option {
+ $existingPrefix = $existingLinkedNamespaces->filter(
+ static fn (DOMNameSpaceNode $node) => $node->namespaceURI === $xmlns->namespaceURI
+ )->first()?->prefix;
+
+ if ($existingPrefix === null) {
+ /** @var Option */
+ return none();
+ }
+
+ /** @var Option */
+ return some([$xmlns->prefix => $existingPrefix]);
+ }
+
+ /**
+ * @return Option
+ *
+ * @throws RuntimeException
+ */
+ private function tryUsingUniquePrefixHash(
+ DOMElement $existingSchema,
+ DOMNameSpaceNode $xmlns
+ ): Option {
+ $uniquePrefix = 'ns' . substr(md5($xmlns->namespaceURI), 0, 8);
+ if ($existingSchema->hasAttribute('xmlns:'.$uniquePrefix)) {
+ /** @var Option */
+ return none();
+ }
+
+ $this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix);
+
+ /** @var Option */
+ return some([$xmlns->prefix => $uniquePrefix]);
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ private function copyXmlnsDeclaration(DOMElement $existingSchema, string $namespaceUri, string $prefix): void
+ {
+ xmlns_attribute($prefix, $namespaceUri)($existingSchema);
+ }
+}
diff --git a/stubs/dom.phpstub b/stubs/dom.phpstub
new file mode 100644
index 0000000..73162e9
--- /dev/null
+++ b/stubs/dom.phpstub
@@ -0,0 +1,7 @@
+ FIXTURE_DIR.'/flattening/import-multi-xsd.wsdl',
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-multi-xsd-result.wsdl', comparable()),
];
+ yield 'import-namespaces' => [
+ 'wsdl' => FIXTURE_DIR.'/flattening/import-namespaces.wsdl',
+ 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-namespaces.wsdl', comparable()),
+ ];
}
}
diff --git a/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php b/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php
index fe06559..f6dff00 100644
--- a/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php
+++ b/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php
@@ -76,5 +76,10 @@ public function provideTestCases()
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/rearranged-imports.wsdl'),
comparable(),
];
+ yield 'import-xmlns-issue' => [
+ 'wsdl' => FIXTURE_DIR.'/flattening/conflicting-imports.wsdl',
+ 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/conflicting-imports.wsdl'),
+ canonicalize(),
+ ];
}
}
diff --git a/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php b/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php
new file mode 100644
index 0000000..0a41ee4
--- /dev/null
+++ b/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php
@@ -0,0 +1,73 @@
+traverse(new ReprefixTypeQname([
+ 'tns' => 'new',
+ 'new' => 'previous', // To make sure prefix replacements don't get chained
+ ]));
+
+ static::assertXmlStringEqualsXmlString($expected, $doc->toXmlString());
+ }
+
+ public static function provideCases(): iterable
+ {
+ yield 'no-attr' => [
+ '',
+ '',
+ ];
+ yield 'other-attribute' => [
+ '',
+ '',
+ ];
+ yield 'no-qualified' => [
+ '',
+ '',
+ ];
+ yield 'simple' => [
+ '',
+ '',
+ ];
+ yield 'element' => [
+ '',
+ '',
+ ];
+ yield 'attribute' => [
+ '',
+ '',
+ ];
+ yield 'nested-schema' => [
+ <<
+
+
+
+
+ EOXML,
+ <<
+
+
+
+
+ EOXML,
+ ];
+ yield 'dont-chain-reprefixes' => [
+ '',
+ '',
+ ];
+ }
+}
diff --git a/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php b/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php
new file mode 100644
index 0000000..93a7a4a
--- /dev/null
+++ b/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php
@@ -0,0 +1,72 @@
+locateDocumentElement(),
+ $importedSchema->locateDocumentElement()
+ );
+
+ static::assertSame($expectedExistingSchemaXml, $existingSchema->stringifyDocumentElement());
+ static::assertSame($expectedImportedSchemaXml, $importedSchema->stringifyDocumentElement());
+ }
+
+ public static function provideCases(): iterable
+ {
+ yield 'no-conflict' => [
+ 'existingSchemaXml' => '',
+ 'importedSchemaXml' => '',
+ 'expectedExistingSchemaXml' => '',
+ 'expectedImportedSchemaXml' => '',
+ ];
+ yield 'conflict' => [
+ 'existingSchemaXml' => '',
+ 'importedSchemaXml' => '',
+ 'expectedExistingSchemaXml' => '',
+ 'expectedImportedSchemaXml' => '',
+ ];
+ yield 'conflict-with-existing-alternative' => [
+ 'existingSchemaXml' => '',
+ 'importedSchemaXml' => '',
+ 'expectedExistingSchemaXml' => '',
+ 'expectedImportedSchemaXml' => '',
+ ];
+ yield 'multiple-conflicts' => [
+ 'existingSchemaXml' => '',
+ 'importedSchemaXml' => <<
+
+
+
+
+ EOXML,
+ 'expectedExistingSchemaXml' => '',
+ 'expectedImportedSchemaXml' => <<
+
+
+
+
+ EOXML,
+ ];
+ }
+}
diff --git a/tests/fixtures/flattening/conflicting-imports.wsdl b/tests/fixtures/flattening/conflicting-imports.wsdl
new file mode 100644
index 0000000..f3793e6
--- /dev/null
+++ b/tests/fixtures/flattening/conflicting-imports.wsdl
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/flattening/import-namespaces.wsdl b/tests/fixtures/flattening/import-namespaces.wsdl
new file mode 100644
index 0000000..ea75982
--- /dev/null
+++ b/tests/fixtures/flattening/import-namespaces.wsdl
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/tests/fixtures/flattening/result/conflicting-imports.wsdl b/tests/fixtures/flattening/result/conflicting-imports.wsdl
new file mode 100644
index 0000000..afaa05a
--- /dev/null
+++ b/tests/fixtures/flattening/result/conflicting-imports.wsdl
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/flattening/result/import-namespaces.wsdl b/tests/fixtures/flattening/result/import-namespaces.wsdl
new file mode 100644
index 0000000..3a6f2fe
--- /dev/null
+++ b/tests/fixtures/flattening/result/import-namespaces.wsdl
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/tests/fixtures/flattening/wsdl/import-namespaces.wsdl b/tests/fixtures/flattening/wsdl/import-namespaces.wsdl
new file mode 100644
index 0000000..2fac0dc
--- /dev/null
+++ b/tests/fixtures/flattening/wsdl/import-namespaces.wsdl
@@ -0,0 +1,8 @@
+
+
+
diff --git a/tests/fixtures/flattening/xsd/conflicting-import.xsd b/tests/fixtures/flattening/xsd/conflicting-import.xsd
new file mode 100644
index 0000000..61e1279
--- /dev/null
+++ b/tests/fixtures/flattening/xsd/conflicting-import.xsd
@@ -0,0 +1,9 @@
+
+
+
+
+