From 4b0c308b7db4603fba2d0d31300b688c57cad1dd Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 15 Oct 2026 12:19:09 +0200 Subject: [PATCH 1/3] Report attributes declared through a ref as qualified A ref points to a global attribute declaration, and global attribute declarations are always qualified with the target namespace of the schema that declares them. The form and attributeFormDefault rules only apply to local declarations. --- .../AttributeSingleConfigurator.php | 7 +- .../AttributeQualificationTest.php | 171 ++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Metadata/Coverter/Types/Configurator/AttributeQualificationTest.php diff --git a/src/Metadata/Converter/Types/Configurator/AttributeSingleConfigurator.php b/src/Metadata/Converter/Types/Configurator/AttributeSingleConfigurator.php index 499acdf..cbd3472 100644 --- a/src/Metadata/Converter/Types/Configurator/AttributeSingleConfigurator.php +++ b/src/Metadata/Converter/Types/Configurator/AttributeSingleConfigurator.php @@ -3,6 +3,7 @@ namespace Soap\WsdlReader\Metadata\Converter\Types\Configurator; +use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeRef; use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle; use Soap\Engine\Metadata\Model\TypeMeta; use Soap\Engine\Metadata\Model\XsdType as EngineType; @@ -17,6 +18,10 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC return $engineType; } + // A ref points to a global attribute declaration, which is always qualified with the target namespace + // of the schema that declares it. The form and attributeFormDefault rules only apply to local declarations. + $isQualified = $xsdType instanceof AttributeRef || $xsdType->isQualified(); + $configure = pipe( static fn (EngineType $engineType): EngineType => (new RestrictionsConfigurator())($engineType, $xsdType->getType()?->getRestriction(), $context), ); @@ -25,7 +30,7 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC $engineType ->withMeta( static fn (TypeMeta $meta): TypeMeta => $meta - ->withIsQualified($xsdType->isQualified()) + ->withIsQualified($isQualified) ->withIsNil($xsdType->isNil()) ->withIsNullable($meta->isNullable()->unwrapOr(false) || $xsdType->isNil()) ->withUse($xsdType->getUse()) diff --git a/tests/Unit/Metadata/Coverter/Types/Configurator/AttributeQualificationTest.php b/tests/Unit/Metadata/Coverter/Types/Configurator/AttributeQualificationTest.php new file mode 100644 index 0000000..11b6893 --- /dev/null +++ b/tests/Unit/Metadata/Coverter/Types/Configurator/AttributeQualificationTest.php @@ -0,0 +1,171 @@ +getType(); + + static::assertTrue($type->getMeta()->isAttribute()->unwrapOr(false)); + static::assertSame($expectedQualified, $type->getMeta()->isQualified()->unwrapOr(false)); + static::assertSame($expectedTargetNamespace, $type->getXmlTargetNamespace()); + } + + public static function provideAttributes(): iterable + { + yield 'ref to xml:lang' => ['Direct', 'lang', true, self::XML]; + yield 'ref to global attribute in foreign schema' => ['Direct', 'flag', true, self::OTHER]; + yield 'ref to global attribute in same schema' => ['Direct', 'glob', true, self::TNS]; + yield 'local unqualified attribute' => ['Direct', 'plain', false, self::TNS]; + yield 'local form=qualified attribute' => ['Direct', 'qual', true, self::TNS]; + + yield 'attributeFormDefault=qualified local attribute' => ['QualifiedByDefault', 'local', true, self::QUALIFIED]; + yield 'attributeFormDefault=qualified local form=unqualified attribute' => ['QualifiedByDefault', 'unq', false, self::QUALIFIED]; + + yield 'ref inside attributeGroup' => ['Grouped', 'glob', true, self::TNS]; + yield 'xml:lang inside attributeGroup' => ['Grouped', 'lang', true, self::XML]; + yield 'local inside attributeGroup' => ['Grouped', 'grouped', false, self::TNS]; + yield 'ref inside nested attributeGroup' => ['Grouped', 'flag', true, self::OTHER]; + + yield 'ref inherited from complexContent extension base' => ['Extended', 'glob', true, self::TNS]; + yield 'ref declared on complexContent extension' => ['Extended', 'lang', true, self::XML]; + yield 'local declared on complexContent extension' => ['Extended', 'extra', false, self::TNS]; + + yield 'ref declared on complexContent restriction' => ['Restricted', 'glob', true, self::TNS]; + + yield 'ref declared on simpleContent extension' => ['Text', 'lang', true, self::XML]; + yield 'local declared on simpleContent extension' => ['Text', 'plain', false, self::TNS]; + } + + private static function findProperty(string $typeName, string $propertyName): Property + { + $type = self::metadata()->getTypes()->fetchFirstByName($typeName); + foreach ($type->getProperties() as $property) { + if ($property->getName() === $propertyName) { + return $property; + } + } + + static::fail(sprintf('Property %s not found on type %s', $propertyName, $typeName)); + } + + private static function metadata(): Metadata + { + if (self::$metadata) { + return self::$metadata; + } + + $tns = self::TNS; + $other = self::OTHER; + $qualified = self::QUALIFIED; + $wsdl = << + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EOXML; + + $info = (new Wsdl1Reader(new CallbackLoader(static fn (): string => $wsdl)))('some.wsdl'); + + return self::$metadata = (new Wsdl1MetadataProvider($info))->getMetadata(); + } +} From d4af0dcf813d6e5ac78d58c4b0302e64ef539e55 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 15 Oct 2026 12:24:35 +0200 Subject: [PATCH 2/3] Know the implicit xml prefix in the WSDL namespaces The xml prefix is bound by the XML namespaces spec and never declared in a WSDL. Attributes such as xml:lang now resolve to that namespace, so consumers can write them with the xml prefix instead of declaring the XML namespace themselves. --- src/Parser/Definitions/NamespacesParser.php | 4 +-- .../Definitions/NamespacesParserTest.php | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Parser/Definitions/NamespacesParserTest.php diff --git a/src/Parser/Definitions/NamespacesParser.php b/src/Parser/Definitions/NamespacesParser.php index 443d2b2..364e3d4 100644 --- a/src/Parser/Definitions/NamespacesParser.php +++ b/src/Parser/Definitions/NamespacesParser.php @@ -27,7 +27,7 @@ public static function tryParse(Document $wsdl): Namespaces */ static fn (array $map, NamespaceInfo $node): array => merge($map, [(string)$node->prefix => (string)$node->namespaceURI]), - [] + ['xml' => 'http://www.w3.org/XML/1998/namespace'] ), reduce( $allNamespaces, @@ -37,7 +37,7 @@ public static function tryParse(Document $wsdl): Namespaces */ static fn (array $map, NamespaceInfo $node): array => merge($map, [(string)$node->namespaceURI => (string)$node->prefix]), - [] + ['http://www.w3.org/XML/1998/namespace' => 'xml'] ) ); } diff --git a/tests/Unit/Parser/Definitions/NamespacesParserTest.php b/tests/Unit/Parser/Definitions/NamespacesParserTest.php new file mode 100644 index 0000000..c93e3e3 --- /dev/null +++ b/tests/Unit/Parser/Definitions/NamespacesParserTest.php @@ -0,0 +1,34 @@ +' + )); + + static::assertSame(self::XML, $namespaces->lookupNamespaceFromName('xml')->unwrap()); + static::assertSame('xml', $namespaces->lookupNameFromNamespace(self::XML)->unwrap()); + static::assertSame('https://test', $namespaces->lookupNamespaceFromName('tns')->unwrap()); + } + + public function test_it_keeps_an_explicit_xml_declaration(): void + { + $namespaces = NamespacesParser::tryParse(Document::fromXmlString( + '' + )); + + static::assertSame(self::XML, $namespaces->lookupNamespaceFromName('xml')->unwrap()); + static::assertSame('xml', $namespaces->lookupNameFromNamespace(self::XML)->unwrap()); + } +} From aef19cc6ea08c6b96a87a845d9ec1600a5042fbe Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 15 Oct 2026 13:01:37 +0200 Subject: [PATCH 3/3] Use the Xmlns::xml() shortcut for the implicit xml prefix --- src/Parser/Definitions/NamespacesParser.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Parser/Definitions/NamespacesParser.php b/src/Parser/Definitions/NamespacesParser.php index 364e3d4..05a05fc 100644 --- a/src/Parser/Definitions/NamespacesParser.php +++ b/src/Parser/Definitions/NamespacesParser.php @@ -6,6 +6,7 @@ use Dom\NamespaceInfo; use Soap\WsdlReader\Model\Definitions\Namespaces; use VeeWee\Xml\Dom\Document; +use VeeWee\Xml\Xmlns\Xmlns; use function Psl\Dict\merge; use function Psl\Iter\reduce; use function VeeWee\Xml\Dom\Locator\document_element; @@ -27,7 +28,7 @@ public static function tryParse(Document $wsdl): Namespaces */ static fn (array $map, NamespaceInfo $node): array => merge($map, [(string)$node->prefix => (string)$node->namespaceURI]), - ['xml' => 'http://www.w3.org/XML/1998/namespace'] + ['xml' => Xmlns::xml()->value()] ), reduce( $allNamespaces, @@ -37,7 +38,7 @@ public static function tryParse(Document $wsdl): Namespaces */ static fn (array $map, NamespaceInfo $node): array => merge($map, [(string)$node->namespaceURI => (string)$node->prefix]), - ['http://www.w3.org/XML/1998/namespace' => 'xml'] + [Xmlns::xml()->value() => 'xml'] ) ); }