Skip to content

Commit 03335d2

Browse files
authored
Merge pull request #49 from veewee/unknown-xsi-prefix-fix
Fix xsi:type namespace resolution for server-side prefixes
2 parents 14f369f + 9f3f4e4 commit 03335d2

File tree

2 files changed

+156
-8
lines changed

2 files changed

+156
-8
lines changed

src/TypeInference/XsiTypeDetector.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,21 @@ static function () use ($context, $value) {
4646
*/
4747
public static function detectXsdTypeFromXmlElement(Context $context, DOMElement $element): Option
4848
{
49-
$xsiType = $element->getAttributeNS(Xmlns::xsi()->value(), 'type');
49+
$xsiType = $element->getAttributeNS(Xmlns::xsi()->value(), 'type') ?: $element->getAttribute('xsi:type');
5050
if (!$xsiType) {
5151
return none();
5252
}
5353

5454
[$prefix, $localName] = (new QnameParser)($xsiType);
55-
if (!$prefix || !$localName) {
55+
if (!$localName) {
5656
return none();
5757
}
5858

59-
$namespace = $context->namespaces->lookupNamespaceFromName($prefix);
60-
if (!$namespace->isSome()) {
61-
return none();
62-
}
59+
$namespaceUri = $prefix
60+
? $element->lookupNamespaceURI($prefix) ?? $context->namespaces->lookupNamespaceFromName($prefix)->unwrapOr(null)
61+
: $element->lookupNamespaceURI(null) ?? $element->namespaceURI;
6362

64-
$namespaceUri = $namespace->unwrap();
65-
if (!$namespaceUri) {
63+
if ($namespaceUri === null || $namespaceUri === '') {
6664
return none();
6765
}
6866

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Test\Unit\TypeInference;
5+
6+
use DOMDocument;
7+
use DOMElement;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\TestCase;
10+
use Soap\Encoding\Test\Unit\ContextCreatorTrait;
11+
use Soap\Encoding\TypeInference\XsiTypeDetector;
12+
use Soap\Engine\Metadata\Model\XsdType;
13+
14+
#[CoversClass(XsiTypeDetector::class)]
15+
final class XsiTypeDetectorTest extends TestCase
16+
{
17+
use ContextCreatorTrait;
18+
19+
public function test_it_returns_none_when_no_xsi_type_attribute(): void
20+
{
21+
$element = $this->createElement('<element xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>');
22+
$context = self::createContext(XsdType::guess('anyType'));
23+
24+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
25+
26+
static::assertFalse($result->isSome());
27+
}
28+
29+
public function test_it_detects_xsi_type_with_matching_prefix(): void
30+
{
31+
$element = $this->createElement(
32+
'<element xmlns:tns="https://test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="tns:MyType"/>'
33+
);
34+
$context = self::createContext(XsdType::guess('anyType'));
35+
36+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
37+
38+
static::assertTrue($result->isSome());
39+
$type = $result->unwrap();
40+
static::assertSame('MyType', $type->getXmlTypeName());
41+
static::assertSame('https://test', $type->getXmlNamespace());
42+
}
43+
44+
public function test_it_detects_xsi_type_with_mismatched_prefix(): void
45+
{
46+
$element = $this->createElement(
47+
'<element xmlns:ns2="https://test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:MyType"/>'
48+
);
49+
$context = self::createContext(XsdType::guess('anyType'));
50+
51+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
52+
53+
static::assertTrue($result->isSome());
54+
$type = $result->unwrap();
55+
static::assertSame('MyType', $type->getXmlTypeName());
56+
static::assertSame('https://test', $type->getXmlNamespace());
57+
}
58+
59+
/**
60+
* Servers that omit xmlns:xsi but still produce xsi:type attributes.
61+
* Falls back to getAttribute('xsi:type').
62+
*/
63+
public function test_it_detects_xsi_type_without_xsi_namespace_declaration(): void
64+
{
65+
$doc = new DOMDocument();
66+
$doc->loadXML('<element xmlns:tns="https://test"/>');
67+
$element = $doc->documentElement;
68+
$element->setAttribute('xsi:type', 'tns:MyType');
69+
70+
$context = self::createContext(XsdType::guess('anyType'));
71+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
72+
73+
static::assertTrue($result->isSome());
74+
$type = $result->unwrap();
75+
static::assertSame('MyType', $type->getXmlTypeName());
76+
static::assertSame('https://test', $type->getXmlNamespace());
77+
}
78+
79+
/**
80+
* Unprefixed xsi:type value resolves via the element's default namespace.
81+
*/
82+
public function test_it_detects_unprefixed_xsi_type_via_default_namespace(): void
83+
{
84+
$element = $this->createElement(
85+
'<element xmlns="https://test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="MyType"/>'
86+
);
87+
$context = self::createContext(XsdType::guess('anyType'));
88+
89+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
90+
91+
static::assertTrue($result->isSome());
92+
$type = $result->unwrap();
93+
static::assertSame('MyType', $type->getXmlTypeName());
94+
static::assertSame('https://test', $type->getXmlNamespace());
95+
}
96+
97+
/**
98+
* Unprefixed xsi:type without any default namespace returns none.
99+
*/
100+
public function test_it_returns_none_for_unprefixed_xsi_type_without_default_namespace(): void
101+
{
102+
$element = $this->createElement(
103+
'<element xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="MyType"/>'
104+
);
105+
$context = self::createContext(XsdType::guess('anyType'));
106+
107+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
108+
109+
static::assertFalse($result->isSome());
110+
}
111+
112+
/**
113+
* Exercises the WSDL fallback path: prefix exists in WSDL Namespaces but NOT in the DOM.
114+
*/
115+
public function test_it_falls_back_to_wsdl_namespaces_when_dom_has_no_declaration(): void
116+
{
117+
$doc = new DOMDocument();
118+
$doc->loadXML('<element xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>');
119+
$element = $doc->documentElement;
120+
$element->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:type', 'tns:MyType');
121+
122+
$context = self::createContext(XsdType::guess('anyType'));
123+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
124+
125+
static::assertTrue($result->isSome());
126+
$type = $result->unwrap();
127+
static::assertSame('MyType', $type->getXmlTypeName());
128+
static::assertSame('https://test', $type->getXmlNamespace());
129+
}
130+
131+
public function test_it_returns_none_for_unknown_prefix(): void
132+
{
133+
$element = $this->createElement(
134+
'<element xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="unknown:MyType"/>'
135+
);
136+
$context = self::createContext(XsdType::guess('anyType'));
137+
138+
$result = XsiTypeDetector::detectXsdTypeFromXmlElement($context, $element);
139+
140+
static::assertFalse($result->isSome());
141+
}
142+
143+
private function createElement(string $xml): DOMElement
144+
{
145+
$doc = new DOMDocument();
146+
$doc->loadXML($xml);
147+
148+
return $doc->documentElement;
149+
}
150+
}

0 commit comments

Comments
 (0)