Skip to content

Commit 48354a4

Browse files
committed
Fix duplicate xmlns attributes on qualified elements with xsi:type
When using MatchingValueEncoder with withBindingUse(ENCODED) on qualified elements (elementFormDefault="qualified"), XsiAttributeBuilder wrote a redundant xmlns declaration for the xsi:type prefix even though ElementBuilder.namespaced_element() already declared it via startElementNs(). This produced invalid XML with duplicate xmlns attributes on simpleContent types (e.g. Amount with only unqualified attributes) and types with form="unqualified" elements, where isAnyPropertyQualified was false and forceIncludeXsiTargetNamespace was forced to true. The fix skips writing the namespace declaration in XsiAttributeBuilder when the wrapping element is qualified and already uses the same prefix.
1 parent e12ba03 commit 48354a4

2 files changed

Lines changed: 153 additions & 4 deletions

File tree

src/Xml/Writer/XsiAttributeBuilder.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,17 @@ public function __invoke(XMLWriter $writer): Generator
9696
// Add xmlns for target namespace
9797
[$prefix] = (new QnameParser())($this->xsiType);
9898
if ($prefix && $this->includeXsiTargetNamespace) {
99-
yield from namespace_attribute(
100-
$this->context->namespaces->lookupNamespaceFromName($prefix)->unwrap(),
101-
$prefix
102-
)($writer);
99+
$type = $this->context->type;
100+
$elementPrefix = $type->getXmlTargetNamespaceName();
101+
$isQualified = $type->getMeta()->isQualified()->unwrapOr(false);
102+
103+
// Skip if the wrapping element already declared this namespace via namespaced_element()
104+
if (!($isQualified && $elementPrefix === $prefix)) {
105+
yield from namespace_attribute(
106+
$this->context->namespaces->lookupNamespaceFromName($prefix)->unwrap(),
107+
$prefix
108+
)($writer);
109+
}
103110
}
104111

105112
yield from namespaced_attribute(
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Test\PhpCompatibility\Implied;
5+
6+
use PHPUnit\Framework\Attributes\CoversClass;
7+
use Soap\Encoding\Decoder;
8+
use Soap\Encoding\Driver;
9+
use Soap\Encoding\Encoder;
10+
use Soap\Encoding\EncoderRegistry;
11+
use Soap\Encoding\Test\PhpCompatibility\AbstractCompatibilityTests;
12+
use Soap\WsdlReader\Model\Definitions\BindingUse;
13+
use stdClass;
14+
15+
/**
16+
* Tests MatchingValueEncoder + withBindingUse(ENCODED) in LITERAL mode
17+
* with qualified elements and simpleContent types (only unqualified attributes).
18+
*
19+
* Without the fix, duplicate xmlns attributes are produced on simpleContent types
20+
* like Amount (isAnyPropertyQualified=false), causing invalid XML.
21+
*/
22+
#[CoversClass(Driver::class)]
23+
#[CoversClass(Encoder::class)]
24+
#[CoversClass(Decoder::class)]
25+
#[CoversClass(Encoder\MatchingValueEncoder::class)]
26+
#[CoversClass(Encoder\ObjectEncoder::class)]
27+
final class ImpliedSchema016Test extends AbstractCompatibilityTests
28+
{
29+
protected string $style = 'document';
30+
protected string $use = 'literal';
31+
protected string $attributeFormDefault = 'elementFormDefault="qualified" attributeFormDefault="unqualified"';
32+
33+
protected string $schema = <<<EOXML
34+
<!-- simpleContent: only unqualified attribute, no qualified elements -->
35+
<complexType name="Amount">
36+
<simpleContent>
37+
<extension base="xsd:decimal">
38+
<attribute name="currencyCode" type="xsd:string" use="required" />
39+
</extension>
40+
</simpleContent>
41+
</complexType>
42+
<complexType name="BaseModule" abstract="true">
43+
<sequence>
44+
<element name="position" type="xsd:int" minOccurs="0" />
45+
</sequence>
46+
</complexType>
47+
<complexType name="CostModule">
48+
<complexContent>
49+
<extension base="tns:BaseModule">
50+
<sequence>
51+
<element name="amount" type="tns:Amount" minOccurs="0" />
52+
</sequence>
53+
</extension>
54+
</complexContent>
55+
</complexType>
56+
<complexType name="ModuleSpecialization">
57+
<sequence>
58+
<element name="module" type="tns:BaseModule" minOccurs="0" />
59+
<element name="replacement" type="xsd:boolean" />
60+
</sequence>
61+
</complexType>
62+
EOXML;
63+
protected string $type = 'type="tns:ModuleSpecialization"';
64+
65+
protected function calculateParam(): mixed
66+
{
67+
return (object) [
68+
'module' => new ImpliedSchema016CostModule(
69+
position: 99,
70+
amount: (object) ['_' => 25.0, 'currencyCode' => 'EUR'],
71+
),
72+
'replacement' => false,
73+
];
74+
}
75+
76+
protected function expectDecoded(): mixed
77+
{
78+
return (object) [
79+
'module' => new ImpliedSchema016CostModule(
80+
position: 99,
81+
amount: (object) ['_' => 25.0, 'currencyCode' => 'EUR'],
82+
),
83+
'replacement' => false,
84+
];
85+
}
86+
87+
protected function registry(): EncoderRegistry
88+
{
89+
return parent::registry()
90+
->addClassMap('http://test-uri/', 'CostModule', ImpliedSchema016CostModule::class)
91+
->addComplexTypeConverter(
92+
'http://test-uri/',
93+
'BaseModule',
94+
new Encoder\MatchingValueEncoder(
95+
encoderDetector: static fn (Encoder\Context $context, mixed $value): array =>
96+
$value instanceof ImpliedSchema016CostModule
97+
? [
98+
$context
99+
->withType($context->type->copy('CostModule')->withXmlTypeName('CostModule'))
100+
->withBindingUse(BindingUse::ENCODED),
101+
new Encoder\ObjectEncoder(ImpliedSchema016CostModule::class),
102+
]
103+
: [$context],
104+
defaultEncoder: new Encoder\ObjectEncoder(stdClass::class)
105+
)
106+
);
107+
}
108+
109+
protected function expectXml(): string
110+
{
111+
return <<<XML
112+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
113+
<SOAP-ENV:Body xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
114+
<tns:ModuleSpecialization xmlns:tns="http://test-uri/">
115+
<tns:module xsi:type="tns:CostModule"
116+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
117+
xmlns:tns="http://test-uri/">
118+
<tns:position xmlns:xsd="http://www.w3.org/2001/XMLSchema"
119+
xsi:type="xsd:int"
120+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
121+
xmlns:tns="http://test-uri/">99</tns:position>
122+
<tns:amount xsi:type="tns:Amount"
123+
currencyCode="EUR"
124+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
125+
xmlns:tns="http://test-uri/">25</tns:amount>
126+
</tns:module>
127+
<tns:replacement xmlns:tns="http://test-uri/">false</tns:replacement>
128+
</tns:ModuleSpecialization>
129+
</SOAP-ENV:Body>
130+
</SOAP-ENV:Envelope>
131+
XML;
132+
}
133+
}
134+
135+
final class ImpliedSchema016CostModule
136+
{
137+
public function __construct(
138+
public int $position,
139+
public ?object $amount = null,
140+
) {
141+
}
142+
}

0 commit comments

Comments
 (0)