Skip to content

Commit

Permalink
Rework SOAP:Array encoder to support complex type items.
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Feb 10, 2025
1 parent 29c2389 commit 065246a
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 58 deletions.
72 changes: 72 additions & 0 deletions src/Encoder/SoapEnc/SoapArrayAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder\SoapEnc;

use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\XmlEncoder;
use Soap\Engine\Metadata\Model\TypeMeta;
use Soap\Engine\Metadata\Model\XsdType;
use Soap\WsdlReader\Model\Definitions\BindingUse;
use Soap\WsdlReader\Parser\Xml\QnameParser;
use Soap\Xml\Xmlns;
use Stringable;
use function Psl\Result\try_catch;

final class SoapArrayAccess
{
/**
* @param XmlEncoder<mixed, Stringable|string> $itemEncoder
*/
public function __construct(
public readonly string $xsiType,
public readonly Context $itemContext,
public readonly XmlEncoder $itemEncoder,
) {
}

public static function forContext(Context $context): self
{
$meta = $context->type->getMeta();
[$namespace, $nodePrefix, $nodeType] = $meta->arrayType()
->map(static fn (array $info): array => [$info['namespace'], ...(new QnameParser())($info['itemType'])])
->unwrapOr([
Xmlns::xsd()->value(),
$context->namespaces->lookupNameFromNamespace(Xmlns::xsd()->value())->unwrapOr('xsd'),
'anyType'
]);
$itemNodeName = $meta->arrayNodeName()->unwrapOr(null);
$xsiType = ltrim($nodePrefix . ':' . $nodeType, ':');

$xsdType = try_catch(
static fn (): XsdType => $context->metadata->getTypes()
->fetchByNameAndXmlNamespace($nodeType, $namespace)
->getXsdType(),
static fn (): XsdType => XsdType::any()
->copy($nodeType)
->withXmlTypeName($nodeType)
->withXmlNamespace($namespace)
->withMeta(static fn (TypeMeta $meta): TypeMeta => $meta->withIsElement(true))
);

if ($context->bindingUse === BindingUse::ENCODED || $itemNodeName !== null) {
$xsdType = $xsdType->withXmlTargetNodeName($itemNodeName ?? 'item');
} else {
$xsdType = $xsdType
->withXmlTargetNodeName($nodeType)
->withXmlTargetNamespaceName($nodePrefix)
->withXmlTargetNamespace($namespace)
->withMeta(
static fn (TypeMeta $meta): TypeMeta => $meta->withIsQualified(true),
);
}

$itemContext = $context->withType($xsdType);
$encoder = $context->registry->detectEncoderForContext($itemContext);

return new self(
$xsiType,
$itemContext,
$encoder,
);
}
}
74 changes: 16 additions & 58 deletions src/Encoder/SoapEnc/SoapArrayEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,22 @@

use Closure;
use DOMElement;
use Generator;
use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\Feature\ListAware;
use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder;
use Soap\Encoding\Encoder\XmlEncoder;
use Soap\Encoding\TypeInference\XsiTypeDetector;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Reader\ElementValueReader;
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
use Soap\Engine\Metadata\Model\XsdType;
use Soap\WsdlReader\Model\Definitions\BindingUse;
use Soap\WsdlReader\Parser\Xml\QnameParser;
use VeeWee\Reflecta\Iso\Iso;
use XMLWriter;
use function count;
use function Psl\Fun\lazy;
use function Psl\Vec\map;
use function VeeWee\Xml\Dom\Locator\Element\children as readChildren;
use function VeeWee\Xml\Writer\Builder\children;
use function VeeWee\Xml\Writer\Builder\element;
use function VeeWee\Xml\Writer\Builder\namespaced_element;
use function VeeWee\Xml\Writer\Builder\prefixed_attribute;
use function VeeWee\Xml\Writer\Builder\value as buildValue;
use function VeeWee\Xml\Writer\Builder\raw as buildRaw;

/**
* @implements XmlEncoder<list<mixed>, non-empty-string>
Expand All @@ -39,39 +32,35 @@ final class SoapArrayEncoder implements ListAware, XmlEncoder
*/
public function iso(Context $context): Iso
{
$arrayAccess = lazy(static fn (): SoapArrayAccess => SoapArrayAccess::forContext($context));

return (new Iso(
/**
* @param list<mixed> $value
* @return non-empty-string
*/
fn (array $value): string => $this->encodeArray($context, $value),
fn (array $value): string => $this->encodeArray($context, $arrayAccess(), $value),
/**
* @param non-empty-string|Element $value
* @return list<mixed>
*/
fn (string|Element $value): array => $this->decodeArray(
$context,
$arrayAccess(),
$value instanceof Element ? $value : Element::fromString($value)
),
));
}


/**
* @param list<mixed> $data
*
* @return non-empty-string
*/
private function encodeArray(Context $context, array $data): string
private function encodeArray(Context $context, SoapArrayAccess $arrayAccess, array $data): string
{
$type = $context->type;
$meta = $type->getMeta();
$itemNodeName = $meta->arrayNodeName()->unwrapOr(null);
$itemType = $meta->arrayType()
->map(static fn (array $info): string => $info['itemType'])
->unwrapOr(XsiTypeDetector::detectFromValue(
$context->withType(XsdType::any()),
$data[0] ?? null
));
$iso = $arrayAccess->itemEncoder->iso($arrayAccess->itemContext);

return (new XsdTypeXmlElementWriter())(
$context,
Expand All @@ -86,66 +75,35 @@ private function encodeArray(Context $context, array $data): string
prefixed_attribute(
'SOAP-ENC',
'arrayType',
$itemType . '['.count($data).']'
$arrayAccess->xsiType . '['.count($data).']'
),
]
: []
),
...map(
$data,
fn (mixed $value): Closure => $this->itemElement($context, $itemNodeName, $itemType, $value)
static fn (mixed $value): Closure => buildRaw((string) $iso->to($value))
)
])
);
}

/**
* @psalm-param mixed $value
* @return Closure(XMLWriter): Generator<bool>
*/
private function itemElement(Context $context, ?string $itemNodeName, string $itemType, mixed $value): Closure
{
$buildValue = buildValue(ScalarTypeEncoder::default()->iso($context)->to($value));

if ($context->bindingUse === BindingUse::ENCODED || $itemNodeName !== null) {
return element(
$itemNodeName ?? 'item',
children([
(new XsiAttributeBuilder($context, $itemType)),
$buildValue
])
);
}

[$prefix, $localName] = (new QnameParser())($itemType);

return namespaced_element(
$context->namespaces->lookupNamespaceFromName($prefix)->unwrap(),
$prefix,
$localName,
$buildValue
);
}

/**
* @return list<mixed>
*/
private function decodeArray(Context $context, Element $value): array
private function decodeArray(Context $context, SoapArrayAccess $arrayAccess, Element $value): array
{
$element = $value->element();
$iso = $arrayAccess->itemEncoder->iso($arrayAccess->itemContext);

return readChildren($element)->reduce(
/**
* @param list<mixed> $list
* @return list<mixed>
*/
static function (array $list, DOMElement $item) use ($context): array {
/** @psalm-var mixed $value */
$value = (new ElementValueReader())(
$context->withType(XsdType::any()),
ScalarTypeEncoder::default(),
$item
);
static function (array $list, DOMElement $item) use ($iso): array {
/** @var mixed $value */
$value = $iso->from(Element::fromDOMElement($item));

return [...$list, $value];
},
Expand Down
66 changes: 66 additions & 0 deletions tests/PhpCompatibility/Implied/ImpliedSchema013Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Test\PhpCompatibility\Implied;

use PHPUnit\Framework\Attributes\CoversClass;
use Soap\Encoding\Decoder;
use Soap\Encoding\Driver;
use Soap\Encoding\Encoder;
use Soap\Encoding\Test\PhpCompatibility\AbstractCompatibilityTests;

#[CoversClass(Driver::class)]
#[CoversClass(Encoder::class)]
#[CoversClass(Decoder::class)]
#[CoversClass(Encoder\SoapEnc\SoapArrayEncoder::class)]
final class ImpliedSchema013Test extends AbstractCompatibilityTests
{
protected string $schema = <<<EOXML
<complexType name="Foo">
<all>
<element name="id" type="string" />
</all>
</complexType>
<complexType name="ArrayOfFoo" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/">
<complexContent>
<restriction base="soap-enc:Array">
<attribute ref="soap-enc:arrayType" wsdl:arrayType="tns:Foo[]"/>
</restriction>
</complexContent>
</complexType>
<element name="testType" minOccurs="1" maxOccurs="1" type="tns:ArrayOfFoo" />
EOXML;
protected string $type = 'type="tns:ArrayOfFoo"';

protected function calculateParam(): mixed
{
return [
(object)['id' => 'abc'],
(object)['id' => 'def'],
];
}

protected function expectXml(): string
{
return <<<XML
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://test-uri/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<tns:test>
<testParam SOAP-ENC:arrayType="tns:Foo[2]" xsi:type="tns:ArrayOfFoo">
<item xsi:type="tns:Foo">
<id xsi:type="xsd:string">abc</id>
</item>
<item xsi:type="tns:Foo">
<id xsi:type="xsd:string">def</id>
</item>
</testParam>
</tns:test>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
XML;
}
}

0 comments on commit 065246a

Please sign in to comment.