Skip to content

Commit fdc265c

Browse files
authored
Merge pull request #39 from veewee/xsi-type-improvements
Respect xsi:type information better
2 parents 5d8e020 + 3e44681 commit fdc265c

23 files changed

+636
-92
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"php-soap/engine": "^2.14",
2828
"php-soap/wsdl": "^1.12",
2929
"php-soap/xml": "^1.8",
30-
"php-soap/wsdl-reader": "~0.20"
30+
"php-soap/wsdl-reader": "~0.26"
3131
},
3232
"require-dev": {
3333
"vimeo/psalm": "^5.26",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types=1);
2+
3+
require_once \dirname(__DIR__, 3) . '/vendor/autoload.php';
4+
5+
use Soap\Encoding\Encoder;
6+
use Soap\Encoding\EncoderRegistry;
7+
use Soap\Encoding\Test\PhpCompatibility\Implied\ImpliedSchema015A;
8+
use Soap\Encoding\Test\PhpCompatibility\Implied\ImpliedSchema015B;
9+
10+
/**
11+
* Sometimes, your XSD schema tell you to use any implementation for a certain base type.
12+
* When building the SOAP payload, you want control over what child object you want to use.
13+
* This encoder can be customized to encoder and context to use for any PHP.
14+
* This encoder works together with the XsiTypeEncoder to decode back from xsi:type attributes.
15+
*
16+
* Example:
17+
* <complexType name="A">
18+
* <sequence>
19+
* <element name="foo" type="xsd:string" />
20+
* </sequence>
21+
* </complexType>
22+
* <complexType name="B">
23+
* <complexContent>
24+
* <extension base="tns:A">
25+
* <sequence>
26+
* <element name="bar" type="xsd:string" />
27+
* </sequence>
28+
* </extension>
29+
* </complexContent>
30+
* </complexType>
31+
* <element name="return">
32+
* <complexType>
33+
* <sequence>
34+
* <element name="responses" type="tns:A" minOccurs="0" maxOccurs="unbounded" />
35+
* </sequence>
36+
* </complexType>
37+
* </element>
38+
*
39+
*
40+
* The result looks like this:
41+
*
42+
* <testParam xsi:type="tns:return">
43+
* <responses xsi:type="tns:A">
44+
* <foo xsi:type="xsd:string">abc</foo>
45+
* </responses>
46+
* <responses xsi:type="tns:B">
47+
* <foo xsi:type="xsd:string">def</foo>
48+
* <bar xsi:type="xsd:string">ghi</bar>
49+
* </responses>
50+
* </testParam>
51+
*
52+
* <=>
53+
*
54+
* ^ {#2507
55+
* +"responses": array:2 [
56+
* 0 => A^ {#2501
57+
* +foo: "abc"
58+
* }
59+
* 1 => B {#2504
60+
* +foo: "def"
61+
* +bar: "ghi"
62+
* }
63+
* ]
64+
* }
65+
*/
66+
67+
EncoderRegistry::default()
68+
->addClassMap('http://test-uri/', 'B', ImpliedSchema015B::class)
69+
->addComplexTypeConverter('http://test-uri/', 'A', new Encoder\MatchingValueEncoder(
70+
encoderDetector: static fn (Encoder\Context $context, mixed $value): array =>
71+
$value instanceof ImpliedSchema015B
72+
? [
73+
$context->withType($context->type->copy('B')->withXmlTypeName('B')),
74+
new Encoder\ObjectEncoder(ImpliedSchema015B::class),
75+
]
76+
: [$context],
77+
defaultEncoder: new Encoder\ObjectEncoder(ImpliedSchema015A::class)
78+
))
79+
// Alternative for using stdObjects only:
80+
->addComplexTypeConverter('http://test-uri/', 'A', new Encoder\MatchingValueEncoder(
81+
encoderDetector: static fn (Encoder\Context $context, mixed $value): Encoder\Context => $context->withType(
82+
property_exists($value, 'bar')
83+
? $context->type->copy('B')->withXmlTypeName('B')
84+
: $context->type
85+
),
86+
defaultEncoder: new Encoder\ObjectEncoder(stdClass::class)
87+
));

examples/encoders/simpleType/anyType-with-xsi-info.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder;
88
use Soap\Encoding\Encoder\XmlEncoder;
99
use Soap\Encoding\EncoderRegistry;
10-
use Soap\Encoding\Xml\Writer\ElementValueBuilder;
10+
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
1111
use Soap\WsdlReader\Model\Definitions\BindingUse;
1212
use VeeWee\Reflecta\Iso\Iso;
1313

@@ -60,7 +60,7 @@ public function resolveXsiTypeForValue(Context $context, mixed $value): string
6060
return match (true) {
6161
$value instanceof \DateTime => 'xsd:datetime',
6262
$value instanceof \Date => 'xsd:date',
63-
default => ElementValueBuilder::resolveXsiTypeForValue($context, $value),
63+
default => XsiAttributeBuilder::resolveXsiTypeForValue($context, $value),
6464
};
6565
}
6666

@@ -72,7 +72,7 @@ public function resolveXsiTypeForValue(Context $context, mixed $value): string
7272
*/
7373
public function shouldIncludeXsiTargetNamespace(Context $context): bool
7474
{
75-
return ElementValueBuilder::shouldIncludeXsiTargetNamespace($context);
75+
return XsiAttributeBuilder::shouldIncludeXsiTargetNamespace($context);
7676
}
7777
}
7878
);

src/Encoder/EncoderDetector.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace Soap\Encoding\Encoder;
55

66
use Soap\Engine\Metadata\Model\XsdType;
7+
use Soap\WsdlReader\Model\Definitions\BindingUse;
78
use stdClass;
89
use WeakMap;
910

@@ -42,10 +43,27 @@ public function __invoke(Context $context): XmlEncoder
4243

4344
$meta = $type->getMeta();
4445

45-
$encoder = match(true) {
46-
$meta->isSimple()->unwrapOr(false) => SimpleType\EncoderDetector::default()($context),
47-
default => $this->detectComplexTypeEncoder($type, $context),
48-
};
46+
return $this->cache[$type] = $this->enhanceEncoder(
47+
$context,
48+
match(true) {
49+
$meta->isSimple()->unwrapOr(false) => SimpleType\EncoderDetector::default()($context),
50+
default => $this->detectComplexTypeEncoder($type, $context)
51+
}
52+
);
53+
}
54+
55+
/**
56+
* @param XmlEncoder<mixed, string> $encoder
57+
* @return XmlEncoder<mixed, string>
58+
*/
59+
private function enhanceEncoder(Context $context, XmlEncoder $encoder): XmlEncoder
60+
{
61+
$meta = $context->type->getMeta();
62+
$isSimple = $meta->isSimple()->unwrapOr(false);
63+
64+
if (!$isSimple && !$encoder instanceof Feature\DisregardXsiInformation && $context->bindingUse === BindingUse::ENCODED) {
65+
$encoder = new XsiTypeEncoder($encoder);
66+
}
4967

5068
if (!$encoder instanceof Feature\ListAware && $meta->isRepeatingElement()->unwrapOr(false)) {
5169
$encoder = new RepeatingElementEncoder($encoder);
@@ -55,9 +73,7 @@ public function __invoke(Context $context): XmlEncoder
5573
$encoder = new OptionalElementEncoder($encoder);
5674
}
5775

58-
$encoder = new ErrorHandlingEncoder($encoder);
59-
60-
return $this->cache[$type] = $encoder;
76+
return new ErrorHandlingEncoder($encoder);
6177
}
6278

6379
/**

src/Encoder/FixedIsoEncoder.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder;
4+
5+
use VeeWee\Reflecta\Iso\Iso;
6+
7+
/**
8+
* @template S
9+
* @template A
10+
* @implements XmlEncoder<S, A>
11+
*/
12+
final readonly class FixedIsoEncoder implements XmlEncoder
13+
{
14+
/**
15+
* @param Iso<S, A> $iso
16+
*/
17+
public function __construct(
18+
private Iso $iso,
19+
) {
20+
}
21+
22+
/**
23+
* @return Iso<S, A>
24+
*/
25+
public function iso(Context $context): Iso
26+
{
27+
return $this->iso;
28+
}
29+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder;
4+
5+
use Closure;
6+
use Soap\Encoding\Xml\Node\Element;
7+
use VeeWee\Reflecta\Iso\Iso;
8+
use function Psl\invariant;
9+
10+
/**
11+
* This encoder can be used to select an encoder based on the value being encoded.
12+
* For decoding, it will always use the default encoder.
13+
*
14+
* @psalm-type MatchedEncoderInfo = Context | array{0: Context, 1 ?: XmlEncoder<mixed, string>|null}
15+
* @psalm-type MatchingEncoderDetector = \Closure(Context, mixed): MatchedEncoderInfo
16+
*
17+
* @psalm-suppress UnusedClass
18+
*
19+
* @implements XmlEncoder<mixed, string>
20+
*/
21+
final readonly class MatchingValueEncoder implements XmlEncoder
22+
{
23+
/**
24+
* @param MatchingEncoderDetector $encoderDetector
25+
* @param XmlEncoder<mixed, string> $defaultEncoder
26+
*/
27+
public function __construct(
28+
private Closure $encoderDetector,
29+
private XmlEncoder $defaultEncoder,
30+
) {
31+
}
32+
33+
public function iso(Context $context): Iso
34+
{
35+
/** @var Iso<string, mixed> $defaultIso */
36+
$defaultIso = $this->defaultEncoder->iso($context);
37+
38+
return new Iso(
39+
to: fn (mixed $value): string => $this->to($context, $value),
40+
/**
41+
* @param string|Element $value
42+
*/
43+
from: static fn (string|Element $value): mixed => $defaultIso->from($value),
44+
);
45+
}
46+
47+
private function to(Context $context, mixed $value): string
48+
{
49+
$matchedEncoderInfo = ($this->encoderDetector)($context, $value);
50+
[$context, $encoder] = match(true) {
51+
$matchedEncoderInfo instanceof Context => [$matchedEncoderInfo, $this->defaultEncoder],
52+
default => [$matchedEncoderInfo[0], $matchedEncoderInfo[1] ?? $this->defaultEncoder],
53+
};
54+
55+
/** @psalm-suppress RedundantConditionGivenDocblockType - This gives better feedback to people using this encoder */
56+
// Ensure that the encoderDetector returns valid data.
57+
invariant($context instanceof Context, 'The MatchingValueEncoder::$encoderDetector callable must return a Context or an array with a Context as first element.');
58+
invariant($encoder instanceof XmlEncoder, 'The MatchingValueEncoder::$encoderDetector callable must return a Context or an array with a Context as first element and an optional XmlEncoder as second element.');
59+
60+
return $encoder->iso($context)->to($value);
61+
}
62+
}

src/Encoder/ObjectEncoder.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
use Closure;
77
use Exception;
8-
use Soap\Encoding\TypeInference\XsiTypeDetector;
98
use Soap\Encoding\Xml\Node\Element;
109
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
1110
use Soap\Encoding\Xml\Writer\AttributeBuilder;
@@ -83,11 +82,12 @@ private function to(Context $context, ObjectAccess $objectAccess, object|array $
8382
$context,
8483
writeChildren(
8584
[
86-
(new XsiAttributeBuilder(
85+
XsiAttributeBuilder::forEncodedValue(
8786
$context,
88-
XsiTypeDetector::detectFromValue($context, []),
89-
includeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified,
90-
)),
87+
$this,
88+
$data,
89+
forceIncludeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified,
90+
),
9191
...map_with_key(
9292
$objectAccess->properties,
9393
static function (string $normalizePropertyName, Property $property) use ($objectAccess, $data, $defaultAction) : Closure {

src/Encoder/SimpleType/EncoderDetector.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use Soap\Encoding\Encoder\Feature;
99
use Soap\Encoding\Encoder\OptionalElementEncoder;
1010
use Soap\Encoding\Encoder\XmlEncoder;
11+
use Soap\Encoding\Encoder\XsiTypeEncoder;
1112
use Soap\Engine\Metadata\Model\XsdType;
13+
use Soap\WsdlReader\Model\Definitions\BindingUse;
1214
use function Psl\Iter\any;
1315

1416
final class EncoderDetector
@@ -25,11 +27,22 @@ public static function default(): self
2527
* @return XmlEncoder<mixed, string|null>
2628
*/
2729
public function __invoke(Context $context): XmlEncoder
30+
{
31+
return $this->enhanceEncoder(
32+
$context,
33+
$this->detectSimpleTypeEncoder($context)
34+
);
35+
}
36+
37+
/**
38+
* @param XmlEncoder<mixed, string> $encoder
39+
* @return XmlEncoder<mixed, string|null>
40+
*/
41+
private function enhanceEncoder(Context $context, XmlEncoder $encoder): XmlEncoder
2842
{
2943
$type = $context->type;
3044
$meta = $type->getMeta();
3145

32-
$encoder = $this->detectSimpleTypeEncoder($type, $context);
3346
if (!$encoder instanceof Feature\ListAware && $this->detectIsListType($type)) {
3447
$encoder = new SimpleListEncoder($encoder);
3548
}
@@ -43,6 +56,10 @@ public function __invoke(Context $context): XmlEncoder
4356
$encoder = new ElementEncoder($encoder);
4457
}
4558

59+
if (!$encoder instanceof Feature\DisregardXsiInformation && $context->bindingUse === BindingUse::ENCODED) {
60+
$encoder = new XsiTypeEncoder($encoder);
61+
}
62+
4663
if ($meta->isNullable()->unwrapOr(false) && !$encoder instanceof Feature\OptionalAware) {
4764
$encoder = new OptionalElementEncoder($encoder);
4865
}
@@ -54,8 +71,9 @@ public function __invoke(Context $context): XmlEncoder
5471
/**
5572
* @return XmlEncoder<mixed, string>
5673
*/
57-
private function detectSimpleTypeEncoder(XsdType $type, Context $context): XmlEncoder
74+
private function detectSimpleTypeEncoder(Context $context): XmlEncoder
5875
{
76+
$type = $context->type;
5977
$meta = $type->getMeta();
6078

6179
// Try to find a direct match:

src/Encoder/SoapEnc/ApacheMapEncoder.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Soap\Encoding\Encoder\Context;
99
use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder;
1010
use Soap\Encoding\Encoder\XmlEncoder;
11-
use Soap\Encoding\TypeInference\XsiTypeDetector;
1211
use Soap\Encoding\Xml\Node\Element;
1312
use Soap\Encoding\Xml\Reader\ElementValueReader;
1413
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
@@ -58,18 +57,18 @@ private function encodeArray(Context $context, array $data): string
5857
return (new XsdTypeXmlElementWriter())(
5958
$context,
6059
buildChildren([
61-
new XsiAttributeBuilder($context, XsiTypeDetector::detectFromValue($context, $data)),
60+
new XsiAttributeBuilder($context, XsiAttributeBuilder::resolveXsiTypeForValue($context, $data)),
6261
...\Psl\Vec\map_with_key(
6362
$data,
6463
static fn (mixed $key, mixed $value): Closure => element(
6564
'item',
6665
buildChildren([
6766
element('key', buildChildren([
68-
(new XsiAttributeBuilder($anyContext, XsiTypeDetector::detectFromValue($anyContext, $key))),
67+
(new XsiAttributeBuilder($anyContext, XsiAttributeBuilder::resolveXsiTypeForValue($anyContext, $key))),
6968
buildValue(ScalarTypeEncoder::default()->iso($context)->to($key))
7069
])),
7170
element('value', buildChildren([
72-
(new XsiAttributeBuilder($anyContext, XsiTypeDetector::detectFromValue($anyContext, $value))),
71+
(new XsiAttributeBuilder($anyContext, XsiAttributeBuilder::resolveXsiTypeForValue($anyContext, $value))),
7372
buildValue(ScalarTypeEncoder::default()->iso($context)->to($value))
7473
])),
7574
]),

src/Encoder/SoapEnc/SoapArrayEncoder.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Soap\Encoding\Encoder\Context;
99
use Soap\Encoding\Encoder\Feature\ListAware;
1010
use Soap\Encoding\Encoder\XmlEncoder;
11-
use Soap\Encoding\TypeInference\XsiTypeDetector;
1211
use Soap\Encoding\Xml\Node\Element;
1312
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
1413
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
@@ -70,7 +69,7 @@ private function encodeArray(Context $context, SoapArrayAccess $arrayAccess, arr
7069
? [
7170
new XsiAttributeBuilder(
7271
$context,
73-
XsiTypeDetector::detectFromValue($context, [])
72+
XsiAttributeBuilder::resolveXsiTypeForValue($context, [])
7473
),
7574
prefixed_attribute(
7675
'SOAP-ENC',

0 commit comments

Comments
 (0)