Skip to content

Commit a38af3e

Browse files
authored
Merge pull request #50 from veewee/duplicate-xmlns-bug
Fix duplicate xmlns attributes on qualified elements with xsi:type
2 parents e12ba03 + 48354a4 commit a38af3e

File tree

2 files changed

+153
-4
lines changed

2 files changed

+153
-4
lines changed

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)