diff --git a/src/Encoder/SoapEnc/SoapArrayAccess.php b/src/Encoder/SoapEnc/SoapArrayAccess.php new file mode 100644 index 0000000..d4bd0a7 --- /dev/null +++ b/src/Encoder/SoapEnc/SoapArrayAccess.php @@ -0,0 +1,72 @@ + $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, + ); + } +} diff --git a/src/Encoder/SoapEnc/SoapArrayEncoder.php b/src/Encoder/SoapEnc/SoapArrayEncoder.php index da50958..fa4fe67 100644 --- a/src/Encoder/SoapEnc/SoapArrayEncoder.php +++ b/src/Encoder/SoapEnc/SoapArrayEncoder.php @@ -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, non-empty-string> @@ -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 $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 */ fn (string|Element $value): array => $this->decodeArray( $context, + $arrayAccess(), $value instanceof Element ? $value : Element::fromString($value) ), )); } + /** * @param list $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, @@ -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 - */ - 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 */ - 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 $list * @return list */ - 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]; }, diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema013Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema013Test.php new file mode 100644 index 0000000..bf35134 --- /dev/null +++ b/tests/PhpCompatibility/Implied/ImpliedSchema013Test.php @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + EOXML; + protected string $type = 'type="tns:ArrayOfFoo"'; + + protected function calculateParam(): mixed + { + return [ + (object)['id' => 'abc'], + (object)['id' => 'def'], + ]; + } + + protected function expectXml(): string + { + return << + + + + + abc + + + def + + + + + + XML; + } +}