Skip to content

Commit 5bd6663

Browse files
authored
Merge pull request #37 from veewee/improved-soap-encoder
Rework SOAP:Array encoder to support complex type items.
2 parents 29c2389 + 065246a commit 5bd6663

File tree

3 files changed

+154
-58
lines changed

3 files changed

+154
-58
lines changed
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder\SoapEnc;
4+
5+
use Soap\Encoding\Encoder\Context;
6+
use Soap\Encoding\Encoder\XmlEncoder;
7+
use Soap\Engine\Metadata\Model\TypeMeta;
8+
use Soap\Engine\Metadata\Model\XsdType;
9+
use Soap\WsdlReader\Model\Definitions\BindingUse;
10+
use Soap\WsdlReader\Parser\Xml\QnameParser;
11+
use Soap\Xml\Xmlns;
12+
use Stringable;
13+
use function Psl\Result\try_catch;
14+
15+
final class SoapArrayAccess
16+
{
17+
/**
18+
* @param XmlEncoder<mixed, Stringable|string> $itemEncoder
19+
*/
20+
public function __construct(
21+
public readonly string $xsiType,
22+
public readonly Context $itemContext,
23+
public readonly XmlEncoder $itemEncoder,
24+
) {
25+
}
26+
27+
public static function forContext(Context $context): self
28+
{
29+
$meta = $context->type->getMeta();
30+
[$namespace, $nodePrefix, $nodeType] = $meta->arrayType()
31+
->map(static fn (array $info): array => [$info['namespace'], ...(new QnameParser())($info['itemType'])])
32+
->unwrapOr([
33+
Xmlns::xsd()->value(),
34+
$context->namespaces->lookupNameFromNamespace(Xmlns::xsd()->value())->unwrapOr('xsd'),
35+
'anyType'
36+
]);
37+
$itemNodeName = $meta->arrayNodeName()->unwrapOr(null);
38+
$xsiType = ltrim($nodePrefix . ':' . $nodeType, ':');
39+
40+
$xsdType = try_catch(
41+
static fn (): XsdType => $context->metadata->getTypes()
42+
->fetchByNameAndXmlNamespace($nodeType, $namespace)
43+
->getXsdType(),
44+
static fn (): XsdType => XsdType::any()
45+
->copy($nodeType)
46+
->withXmlTypeName($nodeType)
47+
->withXmlNamespace($namespace)
48+
->withMeta(static fn (TypeMeta $meta): TypeMeta => $meta->withIsElement(true))
49+
);
50+
51+
if ($context->bindingUse === BindingUse::ENCODED || $itemNodeName !== null) {
52+
$xsdType = $xsdType->withXmlTargetNodeName($itemNodeName ?? 'item');
53+
} else {
54+
$xsdType = $xsdType
55+
->withXmlTargetNodeName($nodeType)
56+
->withXmlTargetNamespaceName($nodePrefix)
57+
->withXmlTargetNamespace($namespace)
58+
->withMeta(
59+
static fn (TypeMeta $meta): TypeMeta => $meta->withIsQualified(true),
60+
);
61+
}
62+
63+
$itemContext = $context->withType($xsdType);
64+
$encoder = $context->registry->detectEncoderForContext($itemContext);
65+
66+
return new self(
67+
$xsiType,
68+
$itemContext,
69+
$encoder,
70+
);
71+
}
72+
}

src/Encoder/SoapEnc/SoapArrayEncoder.php

+16-58
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,22 @@
55

66
use Closure;
77
use DOMElement;
8-
use Generator;
98
use Soap\Encoding\Encoder\Context;
109
use Soap\Encoding\Encoder\Feature\ListAware;
11-
use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder;
1210
use Soap\Encoding\Encoder\XmlEncoder;
1311
use Soap\Encoding\TypeInference\XsiTypeDetector;
1412
use Soap\Encoding\Xml\Node\Element;
15-
use Soap\Encoding\Xml\Reader\ElementValueReader;
1613
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
1714
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
18-
use Soap\Engine\Metadata\Model\XsdType;
1915
use Soap\WsdlReader\Model\Definitions\BindingUse;
20-
use Soap\WsdlReader\Parser\Xml\QnameParser;
2116
use VeeWee\Reflecta\Iso\Iso;
22-
use XMLWriter;
2317
use function count;
18+
use function Psl\Fun\lazy;
2419
use function Psl\Vec\map;
2520
use function VeeWee\Xml\Dom\Locator\Element\children as readChildren;
2621
use function VeeWee\Xml\Writer\Builder\children;
27-
use function VeeWee\Xml\Writer\Builder\element;
28-
use function VeeWee\Xml\Writer\Builder\namespaced_element;
2922
use function VeeWee\Xml\Writer\Builder\prefixed_attribute;
30-
use function VeeWee\Xml\Writer\Builder\value as buildValue;
23+
use function VeeWee\Xml\Writer\Builder\raw as buildRaw;
3124

3225
/**
3326
* @implements XmlEncoder<list<mixed>, non-empty-string>
@@ -39,39 +32,35 @@ final class SoapArrayEncoder implements ListAware, XmlEncoder
3932
*/
4033
public function iso(Context $context): Iso
4134
{
35+
$arrayAccess = lazy(static fn (): SoapArrayAccess => SoapArrayAccess::forContext($context));
36+
4237
return (new Iso(
4338
/**
4439
* @param list<mixed> $value
4540
* @return non-empty-string
4641
*/
47-
fn (array $value): string => $this->encodeArray($context, $value),
42+
fn (array $value): string => $this->encodeArray($context, $arrayAccess(), $value),
4843
/**
4944
* @param non-empty-string|Element $value
5045
* @return list<mixed>
5146
*/
5247
fn (string|Element $value): array => $this->decodeArray(
5348
$context,
49+
$arrayAccess(),
5450
$value instanceof Element ? $value : Element::fromString($value)
5551
),
5652
));
5753
}
5854

55+
5956
/**
6057
* @param list<mixed> $data
6158
*
6259
* @return non-empty-string
6360
*/
64-
private function encodeArray(Context $context, array $data): string
61+
private function encodeArray(Context $context, SoapArrayAccess $arrayAccess, array $data): string
6562
{
66-
$type = $context->type;
67-
$meta = $type->getMeta();
68-
$itemNodeName = $meta->arrayNodeName()->unwrapOr(null);
69-
$itemType = $meta->arrayType()
70-
->map(static fn (array $info): string => $info['itemType'])
71-
->unwrapOr(XsiTypeDetector::detectFromValue(
72-
$context->withType(XsdType::any()),
73-
$data[0] ?? null
74-
));
63+
$iso = $arrayAccess->itemEncoder->iso($arrayAccess->itemContext);
7564

7665
return (new XsdTypeXmlElementWriter())(
7766
$context,
@@ -86,66 +75,35 @@ private function encodeArray(Context $context, array $data): string
8675
prefixed_attribute(
8776
'SOAP-ENC',
8877
'arrayType',
89-
$itemType . '['.count($data).']'
78+
$arrayAccess->xsiType . '['.count($data).']'
9079
),
9180
]
9281
: []
9382
),
9483
...map(
9584
$data,
96-
fn (mixed $value): Closure => $this->itemElement($context, $itemNodeName, $itemType, $value)
85+
static fn (mixed $value): Closure => buildRaw((string) $iso->to($value))
9786
)
9887
])
9988
);
10089
}
10190

102-
/**
103-
* @psalm-param mixed $value
104-
* @return Closure(XMLWriter): Generator<bool>
105-
*/
106-
private function itemElement(Context $context, ?string $itemNodeName, string $itemType, mixed $value): Closure
107-
{
108-
$buildValue = buildValue(ScalarTypeEncoder::default()->iso($context)->to($value));
109-
110-
if ($context->bindingUse === BindingUse::ENCODED || $itemNodeName !== null) {
111-
return element(
112-
$itemNodeName ?? 'item',
113-
children([
114-
(new XsiAttributeBuilder($context, $itemType)),
115-
$buildValue
116-
])
117-
);
118-
}
119-
120-
[$prefix, $localName] = (new QnameParser())($itemType);
121-
122-
return namespaced_element(
123-
$context->namespaces->lookupNamespaceFromName($prefix)->unwrap(),
124-
$prefix,
125-
$localName,
126-
$buildValue
127-
);
128-
}
129-
13091
/**
13192
* @return list<mixed>
13293
*/
133-
private function decodeArray(Context $context, Element $value): array
94+
private function decodeArray(Context $context, SoapArrayAccess $arrayAccess, Element $value): array
13495
{
13596
$element = $value->element();
97+
$iso = $arrayAccess->itemEncoder->iso($arrayAccess->itemContext);
13698

13799
return readChildren($element)->reduce(
138100
/**
139101
* @param list<mixed> $list
140102
* @return list<mixed>
141103
*/
142-
static function (array $list, DOMElement $item) use ($context): array {
143-
/** @psalm-var mixed $value */
144-
$value = (new ElementValueReader())(
145-
$context->withType(XsdType::any()),
146-
ScalarTypeEncoder::default(),
147-
$item
148-
);
104+
static function (array $list, DOMElement $item) use ($iso): array {
105+
/** @var mixed $value */
106+
$value = $iso->from(Element::fromDOMElement($item));
149107

150108
return [...$list, $value];
151109
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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\Test\PhpCompatibility\AbstractCompatibilityTests;
11+
12+
#[CoversClass(Driver::class)]
13+
#[CoversClass(Encoder::class)]
14+
#[CoversClass(Decoder::class)]
15+
#[CoversClass(Encoder\SoapEnc\SoapArrayEncoder::class)]
16+
final class ImpliedSchema013Test extends AbstractCompatibilityTests
17+
{
18+
protected string $schema = <<<EOXML
19+
<complexType name="Foo">
20+
<all>
21+
<element name="id" type="string" />
22+
</all>
23+
</complexType>
24+
<complexType name="ArrayOfFoo" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/">
25+
<complexContent>
26+
<restriction base="soap-enc:Array">
27+
<attribute ref="soap-enc:arrayType" wsdl:arrayType="tns:Foo[]"/>
28+
</restriction>
29+
</complexContent>
30+
</complexType>
31+
<element name="testType" minOccurs="1" maxOccurs="1" type="tns:ArrayOfFoo" />
32+
EOXML;
33+
protected string $type = 'type="tns:ArrayOfFoo"';
34+
35+
protected function calculateParam(): mixed
36+
{
37+
return [
38+
(object)['id' => 'abc'],
39+
(object)['id' => 'def'],
40+
];
41+
}
42+
43+
protected function expectXml(): string
44+
{
45+
return <<<XML
46+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
47+
xmlns:tns="http://test-uri/"
48+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
49+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
50+
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
51+
<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
52+
<tns:test>
53+
<testParam SOAP-ENC:arrayType="tns:Foo[2]" xsi:type="tns:ArrayOfFoo">
54+
<item xsi:type="tns:Foo">
55+
<id xsi:type="xsd:string">abc</id>
56+
</item>
57+
<item xsi:type="tns:Foo">
58+
<id xsi:type="xsd:string">def</id>
59+
</item>
60+
</testParam>
61+
</tns:test>
62+
</SOAP-ENV:Body>
63+
</SOAP-ENV:Envelope>
64+
XML;
65+
}
66+
}

0 commit comments

Comments
 (0)