diff --git a/composer.json b/composer.json index da6a2de..957a1a3 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "php-soap/engine": "^2.14", "php-soap/wsdl": "^1.12", "php-soap/xml": "^1.8", - "php-soap/wsdl-reader": "~0.20" + "php-soap/wsdl-reader": "~0.26" }, "require-dev": { "vimeo/psalm": "^5.26", diff --git a/examples/encoders/complexType/matching-value.php b/examples/encoders/complexType/matching-value.php new file mode 100644 index 0000000..93ee704 --- /dev/null +++ b/examples/encoders/complexType/matching-value.php @@ -0,0 +1,87 @@ + + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * The result looks like this: + * + * + * + * abc + * + * + * def + * ghi + * + * + * + * <=> + * + * ^ {#2507 + * +"responses": array:2 [ + * 0 => A^ {#2501 + * +foo: "abc" + * } + * 1 => B {#2504 + * +foo: "def" + * +bar: "ghi" + * } + * ] + * } + */ + +EncoderRegistry::default() + ->addClassMap('http://test-uri/', 'B', ImpliedSchema015B::class) + ->addComplexTypeConverter('http://test-uri/', 'A', new Encoder\MatchingValueEncoder( + encoderDetector: static fn (Encoder\Context $context, mixed $value): array => + $value instanceof ImpliedSchema015B + ? [ + $context->withType($context->type->copy('B')->withXmlTypeName('B')), + new Encoder\ObjectEncoder(ImpliedSchema015B::class), + ] + : [$context], + defaultEncoder: new Encoder\ObjectEncoder(ImpliedSchema015A::class) + )) + // Alternative for using stdObjects only: + ->addComplexTypeConverter('http://test-uri/', 'A', new Encoder\MatchingValueEncoder( + encoderDetector: static fn (Encoder\Context $context, mixed $value): Encoder\Context => $context->withType( + property_exists($value, 'bar') + ? $context->type->copy('B')->withXmlTypeName('B') + : $context->type + ), + defaultEncoder: new Encoder\ObjectEncoder(stdClass::class) + )); diff --git a/examples/encoders/simpleType/anyType-with-xsi-info.php b/examples/encoders/simpleType/anyType-with-xsi-info.php index 53947ed..392ae9a 100644 --- a/examples/encoders/simpleType/anyType-with-xsi-info.php +++ b/examples/encoders/simpleType/anyType-with-xsi-info.php @@ -7,7 +7,7 @@ use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder; use Soap\Encoding\Encoder\XmlEncoder; use Soap\Encoding\EncoderRegistry; -use Soap\Encoding\Xml\Writer\ElementValueBuilder; +use Soap\Encoding\Xml\Writer\XsiAttributeBuilder; use Soap\WsdlReader\Model\Definitions\BindingUse; use VeeWee\Reflecta\Iso\Iso; @@ -60,7 +60,7 @@ public function resolveXsiTypeForValue(Context $context, mixed $value): string return match (true) { $value instanceof \DateTime => 'xsd:datetime', $value instanceof \Date => 'xsd:date', - default => ElementValueBuilder::resolveXsiTypeForValue($context, $value), + default => XsiAttributeBuilder::resolveXsiTypeForValue($context, $value), }; } @@ -72,7 +72,7 @@ public function resolveXsiTypeForValue(Context $context, mixed $value): string */ public function shouldIncludeXsiTargetNamespace(Context $context): bool { - return ElementValueBuilder::shouldIncludeXsiTargetNamespace($context); + return XsiAttributeBuilder::shouldIncludeXsiTargetNamespace($context); } } ); diff --git a/src/Encoder/EncoderDetector.php b/src/Encoder/EncoderDetector.php index 2bab86c..1c81c12 100644 --- a/src/Encoder/EncoderDetector.php +++ b/src/Encoder/EncoderDetector.php @@ -4,6 +4,7 @@ namespace Soap\Encoding\Encoder; use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Model\Definitions\BindingUse; use stdClass; use WeakMap; @@ -42,10 +43,27 @@ public function __invoke(Context $context): XmlEncoder $meta = $type->getMeta(); - $encoder = match(true) { - $meta->isSimple()->unwrapOr(false) => SimpleType\EncoderDetector::default()($context), - default => $this->detectComplexTypeEncoder($type, $context), - }; + return $this->cache[$type] = $this->enhanceEncoder( + $context, + match(true) { + $meta->isSimple()->unwrapOr(false) => SimpleType\EncoderDetector::default()($context), + default => $this->detectComplexTypeEncoder($type, $context) + } + ); + } + + /** + * @param XmlEncoder $encoder + * @return XmlEncoder + */ + private function enhanceEncoder(Context $context, XmlEncoder $encoder): XmlEncoder + { + $meta = $context->type->getMeta(); + $isSimple = $meta->isSimple()->unwrapOr(false); + + if (!$isSimple && !$encoder instanceof Feature\DisregardXsiInformation && $context->bindingUse === BindingUse::ENCODED) { + $encoder = new XsiTypeEncoder($encoder); + } if (!$encoder instanceof Feature\ListAware && $meta->isRepeatingElement()->unwrapOr(false)) { $encoder = new RepeatingElementEncoder($encoder); @@ -55,9 +73,7 @@ public function __invoke(Context $context): XmlEncoder $encoder = new OptionalElementEncoder($encoder); } - $encoder = new ErrorHandlingEncoder($encoder); - - return $this->cache[$type] = $encoder; + return new ErrorHandlingEncoder($encoder); } /** diff --git a/src/Encoder/FixedIsoEncoder.php b/src/Encoder/FixedIsoEncoder.php new file mode 100644 index 0000000..f41dc04 --- /dev/null +++ b/src/Encoder/FixedIsoEncoder.php @@ -0,0 +1,29 @@ + + */ +final readonly class FixedIsoEncoder implements XmlEncoder +{ + /** + * @param Iso $iso + */ + public function __construct( + private Iso $iso, + ) { + } + + /** + * @return Iso + */ + public function iso(Context $context): Iso + { + return $this->iso; + } +} diff --git a/src/Encoder/MatchingValueEncoder.php b/src/Encoder/MatchingValueEncoder.php new file mode 100644 index 0000000..ec98999 --- /dev/null +++ b/src/Encoder/MatchingValueEncoder.php @@ -0,0 +1,62 @@ +|null} + * @psalm-type MatchingEncoderDetector = \Closure(Context, mixed): MatchedEncoderInfo + * + * @psalm-suppress UnusedClass + * + * @implements XmlEncoder + */ +final readonly class MatchingValueEncoder implements XmlEncoder +{ + /** + * @param MatchingEncoderDetector $encoderDetector + * @param XmlEncoder $defaultEncoder + */ + public function __construct( + private Closure $encoderDetector, + private XmlEncoder $defaultEncoder, + ) { + } + + public function iso(Context $context): Iso + { + /** @var Iso $defaultIso */ + $defaultIso = $this->defaultEncoder->iso($context); + + return new Iso( + to: fn (mixed $value): string => $this->to($context, $value), + /** + * @param string|Element $value + */ + from: static fn (string|Element $value): mixed => $defaultIso->from($value), + ); + } + + private function to(Context $context, mixed $value): string + { + $matchedEncoderInfo = ($this->encoderDetector)($context, $value); + [$context, $encoder] = match(true) { + $matchedEncoderInfo instanceof Context => [$matchedEncoderInfo, $this->defaultEncoder], + default => [$matchedEncoderInfo[0], $matchedEncoderInfo[1] ?? $this->defaultEncoder], + }; + + /** @psalm-suppress RedundantConditionGivenDocblockType - This gives better feedback to people using this encoder */ + // Ensure that the encoderDetector returns valid data. + invariant($context instanceof Context, 'The MatchingValueEncoder::$encoderDetector callable must return a Context or an array with a Context as first element.'); + 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.'); + + return $encoder->iso($context)->to($value); + } +} diff --git a/src/Encoder/ObjectEncoder.php b/src/Encoder/ObjectEncoder.php index 33304a4..995ebe5 100644 --- a/src/Encoder/ObjectEncoder.php +++ b/src/Encoder/ObjectEncoder.php @@ -5,7 +5,6 @@ use Closure; use Exception; -use Soap\Encoding\TypeInference\XsiTypeDetector; use Soap\Encoding\Xml\Node\Element; use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader; use Soap\Encoding\Xml\Writer\AttributeBuilder; @@ -83,11 +82,12 @@ private function to(Context $context, ObjectAccess $objectAccess, object|array $ $context, writeChildren( [ - (new XsiAttributeBuilder( + XsiAttributeBuilder::forEncodedValue( $context, - XsiTypeDetector::detectFromValue($context, []), - includeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified, - )), + $this, + $data, + forceIncludeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified, + ), ...map_with_key( $objectAccess->properties, static function (string $normalizePropertyName, Property $property) use ($objectAccess, $data, $defaultAction) : Closure { diff --git a/src/Encoder/SimpleType/EncoderDetector.php b/src/Encoder/SimpleType/EncoderDetector.php index 2c12423..74eecf3 100644 --- a/src/Encoder/SimpleType/EncoderDetector.php +++ b/src/Encoder/SimpleType/EncoderDetector.php @@ -8,7 +8,9 @@ use Soap\Encoding\Encoder\Feature; use Soap\Encoding\Encoder\OptionalElementEncoder; use Soap\Encoding\Encoder\XmlEncoder; +use Soap\Encoding\Encoder\XsiTypeEncoder; use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Model\Definitions\BindingUse; use function Psl\Iter\any; final class EncoderDetector @@ -25,11 +27,22 @@ public static function default(): self * @return XmlEncoder */ public function __invoke(Context $context): XmlEncoder + { + return $this->enhanceEncoder( + $context, + $this->detectSimpleTypeEncoder($context) + ); + } + + /** + * @param XmlEncoder $encoder + * @return XmlEncoder + */ + private function enhanceEncoder(Context $context, XmlEncoder $encoder): XmlEncoder { $type = $context->type; $meta = $type->getMeta(); - $encoder = $this->detectSimpleTypeEncoder($type, $context); if (!$encoder instanceof Feature\ListAware && $this->detectIsListType($type)) { $encoder = new SimpleListEncoder($encoder); } @@ -43,6 +56,10 @@ public function __invoke(Context $context): XmlEncoder $encoder = new ElementEncoder($encoder); } + if (!$encoder instanceof Feature\DisregardXsiInformation && $context->bindingUse === BindingUse::ENCODED) { + $encoder = new XsiTypeEncoder($encoder); + } + if ($meta->isNullable()->unwrapOr(false) && !$encoder instanceof Feature\OptionalAware) { $encoder = new OptionalElementEncoder($encoder); } @@ -54,8 +71,9 @@ public function __invoke(Context $context): XmlEncoder /** * @return XmlEncoder */ - private function detectSimpleTypeEncoder(XsdType $type, Context $context): XmlEncoder + private function detectSimpleTypeEncoder(Context $context): XmlEncoder { + $type = $context->type; $meta = $type->getMeta(); // Try to find a direct match: diff --git a/src/Encoder/SoapEnc/ApacheMapEncoder.php b/src/Encoder/SoapEnc/ApacheMapEncoder.php index 776eae4..3ed1e76 100644 --- a/src/Encoder/SoapEnc/ApacheMapEncoder.php +++ b/src/Encoder/SoapEnc/ApacheMapEncoder.php @@ -8,7 +8,6 @@ use Soap\Encoding\Encoder\Context; 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; @@ -58,18 +57,18 @@ private function encodeArray(Context $context, array $data): string return (new XsdTypeXmlElementWriter())( $context, buildChildren([ - new XsiAttributeBuilder($context, XsiTypeDetector::detectFromValue($context, $data)), + new XsiAttributeBuilder($context, XsiAttributeBuilder::resolveXsiTypeForValue($context, $data)), ...\Psl\Vec\map_with_key( $data, static fn (mixed $key, mixed $value): Closure => element( 'item', buildChildren([ element('key', buildChildren([ - (new XsiAttributeBuilder($anyContext, XsiTypeDetector::detectFromValue($anyContext, $key))), + (new XsiAttributeBuilder($anyContext, XsiAttributeBuilder::resolveXsiTypeForValue($anyContext, $key))), buildValue(ScalarTypeEncoder::default()->iso($context)->to($key)) ])), element('value', buildChildren([ - (new XsiAttributeBuilder($anyContext, XsiTypeDetector::detectFromValue($anyContext, $value))), + (new XsiAttributeBuilder($anyContext, XsiAttributeBuilder::resolveXsiTypeForValue($anyContext, $value))), buildValue(ScalarTypeEncoder::default()->iso($context)->to($value)) ])), ]), diff --git a/src/Encoder/SoapEnc/SoapArrayEncoder.php b/src/Encoder/SoapEnc/SoapArrayEncoder.php index fa4fe67..c002992 100644 --- a/src/Encoder/SoapEnc/SoapArrayEncoder.php +++ b/src/Encoder/SoapEnc/SoapArrayEncoder.php @@ -8,7 +8,6 @@ use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\Feature\ListAware; use Soap\Encoding\Encoder\XmlEncoder; -use Soap\Encoding\TypeInference\XsiTypeDetector; use Soap\Encoding\Xml\Node\Element; use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter; use Soap\Encoding\Xml\Writer\XsiAttributeBuilder; @@ -70,7 +69,7 @@ private function encodeArray(Context $context, SoapArrayAccess $arrayAccess, arr ? [ new XsiAttributeBuilder( $context, - XsiTypeDetector::detectFromValue($context, []) + XsiAttributeBuilder::resolveXsiTypeForValue($context, []) ), prefixed_attribute( 'SOAP-ENC', diff --git a/src/Encoder/SoapEnc/SoapObjectEncoder.php b/src/Encoder/SoapEnc/SoapObjectEncoder.php index 92cceb2..f617823 100644 --- a/src/Encoder/SoapEnc/SoapObjectEncoder.php +++ b/src/Encoder/SoapEnc/SoapObjectEncoder.php @@ -8,7 +8,6 @@ use Soap\Encoding\Encoder\Context; 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; @@ -56,13 +55,13 @@ private function encodeArray(Context $context, object $data): string return (new XsdTypeXmlElementWriter())( $context, children([ - new XsiAttributeBuilder($context, XsiTypeDetector::detectFromValue($context, $data)), + new XsiAttributeBuilder($context, XsiAttributeBuilder::resolveXsiTypeForValue($context, $data)), ...\Psl\Vec\map_with_key( (array) $data, static fn (mixed $key, mixed $value): Closure => element( (string) $key, children([ - (new XsiAttributeBuilder($anyContext, XsiTypeDetector::detectFromValue($anyContext, $value))), + (new XsiAttributeBuilder($anyContext, XsiAttributeBuilder::resolveXsiTypeForValue($anyContext, $value))), buildValue(ScalarTypeEncoder::default()->iso($context)->to($value)) ]), ) diff --git a/src/Encoder/XsiTypeEncoder.php b/src/Encoder/XsiTypeEncoder.php new file mode 100644 index 0000000..b2d2954 --- /dev/null +++ b/src/Encoder/XsiTypeEncoder.php @@ -0,0 +1,59 @@ + + */ +final readonly class XsiTypeEncoder implements Feature\ElementAware, XmlEncoder +{ + /** + * @param XmlEncoder $encoder + */ + public function __construct( + private XmlEncoder $encoder + ) { + } + + /** + * @return Iso + */ + public function iso(Context $context): Iso + { + return new Iso( + function (mixed $value) use ($context) : string { + return $this->to($context, $value); + }, + function (string|Element $value) use ($context) : mixed { + return $this->from( + $context, + ($value instanceof Element ? $value : Element::fromString(non_empty_string()->assert($value))) + ); + } + ); + } + + private function to(Context $context, mixed $value): string + { + // There is no way to know what xsi:type to use when encoding any type. + // The type defined in the wsdl will always be used to encode the value. + // If you want more control over the encoded type, please control how to encode by using the MatchingValueEncoder. + return $this->encoder->iso($context)->to($value); + } + + private function from(Context $context, Element $value): mixed + { + /** @var XmlEncoder $encoder */ + $encoder = match (true) { + $this->encoder instanceof Feature\DisregardXsiInformation => $this->encoder, + default => XsiTypeDetector::detectEncoderFromXmlElement($context, $value->element())->unwrapOr($this->encoder) + }; + + return $encoder->iso($context)->from($value); + } +} diff --git a/src/EncoderRegistry.php b/src/EncoderRegistry.php index aef4c87..fcc921e 100644 --- a/src/EncoderRegistry.php +++ b/src/EncoderRegistry.php @@ -9,7 +9,6 @@ use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\EncoderDetector; use Soap\Encoding\Encoder\ObjectEncoder; -use Soap\Encoding\Encoder\OptionalElementEncoder; use Soap\Encoding\Encoder\SimpleType; use Soap\Encoding\Encoder\SoapEnc; use Soap\Encoding\Encoder\XmlEncoder; @@ -174,7 +173,7 @@ public function addClassMap(string $namespace, string $name, string $class): sel { $this->complextTypeMap->add( (new QNameFormatter())($namespace, $name), - new OptionalElementEncoder(new ObjectEncoder($class)) + new ObjectEncoder($class) ); return $this; @@ -281,12 +280,17 @@ public function findSimpleEncoderByNamespaceName(string $namespace, string $name public function hasRegisteredSimpleTypeForXsdType(XsdType $type): bool { - $qNameFormatter = new QNameFormatter(); - - return $this->simpleTypeMap->contains($qNameFormatter( + return $this->hasRegisteredSimpleTypeForNamespaceName( $type->getXmlNamespace(), $type->getXmlTypeName() - )); + ); + } + + public function hasRegisteredSimpleTypeForNamespaceName(string $namespace, string $name): bool + { + $qNameFormatter = new QNameFormatter(); + + return $this->simpleTypeMap->contains($qNameFormatter($namespace, $name)); } /** @@ -312,19 +316,22 @@ public function findComplexEncoderByNamespaceName(string $namespace, string $nam return $found; } - return new OptionalElementEncoder( - new ObjectEncoder(stdClass::class) - ); + return new ObjectEncoder(stdClass::class); } public function hasRegisteredComplexTypeForXsdType(XsdType $type): bool { - $qNameFormatter = new QNameFormatter(); - - return $this->complextTypeMap->contains($qNameFormatter( + return $this->hasRegisteredComplexTypeForNamespaceName( $type->getXmlNamespace(), $type->getXmlTypeName() - )); + ); + } + + public function hasRegisteredComplexTypeForNamespaceName(string $namespace, string $name): bool + { + $qNameFormatter = new QNameFormatter(); + + return $this->complextTypeMap->contains($qNameFormatter($namespace, $name)); } /** diff --git a/src/TypeInference/XsiTypeDetector.php b/src/TypeInference/XsiTypeDetector.php index 41abcee..d5be07f 100644 --- a/src/TypeInference/XsiTypeDetector.php +++ b/src/TypeInference/XsiTypeDetector.php @@ -6,7 +6,8 @@ use DOMElement; use Psl\Option\Option; use Soap\Encoding\Encoder\Context; -use Soap\Encoding\Encoder\XmlEncoder; +use Soap\Encoding\Encoder\FixedIsoEncoder; +use Soap\Engine\Metadata\Model\XsdType; use Soap\WsdlReader\Model\Definitions\BindingUse; use Soap\WsdlReader\Parser\Xml\QnameParser; use Soap\Xml\Xmlns as SoapXmlns; @@ -42,9 +43,9 @@ static function () use ($context, $value) { } /** - * @return Option> + * @return Option */ - public static function detectEncoderFromXmlElement(Context $context, DOMElement $element): Option + public static function detectXsdTypeFromXmlElement(Context $context, DOMElement $element): Option { if ($context->bindingUse !== BindingUse::ENCODED) { return none(); @@ -70,14 +71,41 @@ public static function detectEncoderFromXmlElement(Context $context, DOMElement return none(); } - $type = $context->type; - $meta = $type->getMeta(); + return some( + // We create a new type based on the detected xsi:type, but we keep the meta information of the original type. + // This way we can still detect if the type is nullable, a union, used on an element, ... + $context->type + ->copy($localName) + ->withXmlTypeName($localName) + ->withXmlNamespace($namespaceUri) + ); + } + + /** + * @return Option> + */ + public static function detectEncoderFromXmlElement(Context $context, DOMElement $element): Option + { + $requestedXsiType = self::detectXsdTypeFromXmlElement($context, $element); + if (!$requestedXsiType->isSome()) { + return none(); + } + + // Enhance context to avoid duplicate optionals, repeating elements, xsi:type detections, ... + $type = $requestedXsiType->unwrap(); + $encoderDetectorTypeMeta = $type->getMeta() + ->withIsNullable(false) + ->withIsRepeatingElement(false); + $encoderDetectorContext = $context + ->withType($type->withMeta(static fn () => $encoderDetectorTypeMeta)) + ->withBindingUse(BindingUse::LITERAL); return some( - match(true) { - $meta->isSimple()->unwrapOr(false) => $context->registry->findSimpleEncoderByNamespaceName($namespaceUri, $localName), - default => $context->registry->findComplexEncoderByNamespaceName($namespaceUri, $localName), - } + new FixedIsoEncoder( + $context->registry->detectEncoderForContext($encoderDetectorContext)->iso( + $context->withType($type) + ), + ) ); } diff --git a/src/Xml/Reader/ElementValueReader.php b/src/Xml/Reader/ElementValueReader.php index 21b244e..0ceb8e8 100644 --- a/src/Xml/Reader/ElementValueReader.php +++ b/src/Xml/Reader/ElementValueReader.php @@ -5,9 +5,7 @@ use DOMElement; use Soap\Encoding\Encoder\Context; -use Soap\Encoding\Encoder\Feature\DisregardXsiInformation; use Soap\Encoding\Encoder\XmlEncoder; -use Soap\Encoding\TypeInference\XsiTypeDetector; use function Psl\Type\string; use function VeeWee\Xml\Dom\Locator\Node\value as readValue; @@ -22,12 +20,6 @@ public function __invoke( XmlEncoder $encoder, DOMElement $element ): mixed { - /** @var XmlEncoder $encoder */ - $encoder = match (true) { - $encoder instanceof DisregardXsiInformation => $encoder, - default => XsiTypeDetector::detectEncoderFromXmlElement($context, $element)->unwrapOr($encoder) - }; - return $encoder->iso($context)->from( readValue($element, string()) ); diff --git a/src/Xml/Writer/ElementValueBuilder.php b/src/Xml/Writer/ElementValueBuilder.php index 0b9522e..65a197b 100644 --- a/src/Xml/Writer/ElementValueBuilder.php +++ b/src/Xml/Writer/ElementValueBuilder.php @@ -7,8 +7,6 @@ use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\Feature; use Soap\Encoding\Encoder\XmlEncoder; -use Soap\Encoding\TypeInference\XsiTypeDetector; -use Soap\WsdlReader\Model\Definitions\BindingUse; use XMLWriter; use function VeeWee\Xml\Writer\Builder\cdata; use function VeeWee\Xml\Writer\Builder\children; @@ -43,48 +41,27 @@ public function __invoke(XMLWriter $writer): Generator */ private function buildXsiType(XMLWriter $writer): Generator { - if ($this->context->bindingUse !== BindingUse::ENCODED) { - return; - } - - $context = $this->context; - [$xsiType, $includeXsiTargetNamespace] = match(true) { - $this->encoder instanceof Feature\XsiTypeCalculator => [ - $this->encoder->resolveXsiTypeForValue($context, $this->value), - $this->encoder->shouldIncludeXsiTargetNamespace($context), - ], - default => [ - self::resolveXsiTypeForValue($context, $this->value), - self::shouldIncludeXsiTargetNamespace($context), - ], - }; - - yield from (new XsiAttributeBuilder( + yield from XsiAttributeBuilder::forEncodedValue( $this->context, - $xsiType, - $includeXsiTargetNamespace, - ))($writer); + $this->encoder, + $this->value, + )($writer); } /** - * Can be used as a default fallback function when implementing the XsiTypeCalculator interface. - * Tells the XsiAttributeBuilder what xsi:type attribute should be set to for a given value. + * @deprecated Use XsiAttributeBuilder::resolveXsiTypeForValue() instead. Will be removed in 1.0.0. */ public static function resolveXsiTypeForValue(Context $context, mixed $value): string { - return XsiTypeDetector::detectFromValue($context, $value); + return XsiAttributeBuilder::resolveXsiTypeForValue($context, $value); } /** - * Can be used as a default fallback function when implementing the XsiTypeCalculator interface. - * Tells the XsiAttributeBuilder that the prefix of the xsi:type should be imported as a xmlns namespace. + * @deprecated Use XsiAttributeBuilder::shouldIncludeXsiTargetNamespace() instead. Will be removed in 1.0.0. */ public static function shouldIncludeXsiTargetNamespace(Context $context): bool { - $type = $context->type; - - return $type->getXmlTargetNamespace() !== $type->getXmlNamespace() - || !$type->getMeta()->isQualified()->unwrapOr(false); + return XsiAttributeBuilder::shouldIncludeXsiTargetNamespace($context); } /** diff --git a/src/Xml/Writer/XsiAttributeBuilder.php b/src/Xml/Writer/XsiAttributeBuilder.php index 6da83ea..1d62a41 100644 --- a/src/Xml/Writer/XsiAttributeBuilder.php +++ b/src/Xml/Writer/XsiAttributeBuilder.php @@ -3,12 +3,17 @@ namespace Soap\Encoding\Xml\Writer; +use Closure; use Generator; use Soap\Encoding\Encoder\Context; +use Soap\Encoding\Encoder\Feature; +use Soap\Encoding\Encoder\XmlEncoder; +use Soap\Encoding\TypeInference\XsiTypeDetector; use Soap\WsdlReader\Model\Definitions\BindingUse; use Soap\WsdlReader\Parser\Xml\QnameParser; use VeeWee\Xml\Xmlns\Xmlns; use XMLWriter; +use function VeeWee\Xml\Writer\Builder\children; use function VeeWee\Xml\Writer\Builder\namespace_attribute; use function VeeWee\Xml\Writer\Builder\namespaced_attribute; @@ -21,6 +26,58 @@ public function __construct( ) { } + /** + * @return Closure(XMLWriter): Generator + */ + public static function forEncodedValue( + Context $context, + XmlEncoder $encoder, + mixed $value, + ?bool $forceIncludeXsiTargetNamespace = null, + ): Closure { + if ($context->bindingUse !== BindingUse::ENCODED) { + return children([]); + } + + [$xsiType, $includeXsiTargetNamespace] = match(true) { + $encoder instanceof Feature\XsiTypeCalculator => [ + $encoder->resolveXsiTypeForValue($context, $value), + $forceIncludeXsiTargetNamespace ?? $encoder->shouldIncludeXsiTargetNamespace($context), + ], + default => [ + self::resolveXsiTypeForValue($context, $value), + $forceIncludeXsiTargetNamespace ?? self::shouldIncludeXsiTargetNamespace($context), + ], + }; + + return (new self( + $context, + $xsiType, + $includeXsiTargetNamespace, + ))(...); + } + + /** + * Can be used as a default fallback function when implementing the XsiTypeCalculator interface. + * Tells the XsiAttributeBuilder what xsi:type attribute should be set to for a given value. + */ + public static function resolveXsiTypeForValue(Context $context, mixed $value): string + { + return XsiTypeDetector::detectFromValue($context, $value); + } + + /** + * Can be used as a default fallback function when implementing the XsiTypeCalculator interface. + * Tells the XsiAttributeBuilder that the prefix of the xsi:type should be imported as a xmlns namespace. + */ + public static function shouldIncludeXsiTargetNamespace(Context $context): bool + { + $type = $context->type; + + return $type->getXmlTargetNamespace() !== $type->getXmlNamespace() + || !$type->getMeta()->isQualified()->unwrapOr(false); + } + /** * @return Generator */ diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema009Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema009Test.php index 85a5c33..38ce0fe 100644 --- a/tests/PhpCompatibility/Implied/ImpliedSchema009Test.php +++ b/tests/PhpCompatibility/Implied/ImpliedSchema009Test.php @@ -50,7 +50,7 @@ protected function expectXml(): string xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"> - + 0 diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema010Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema010Test.php index 87b5e9c..eda0ac8 100644 --- a/tests/PhpCompatibility/Implied/ImpliedSchema010Test.php +++ b/tests/PhpCompatibility/Implied/ImpliedSchema010Test.php @@ -46,7 +46,7 @@ protected function expectXml(): string xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"> - + diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema011Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema011Test.php index 0d13376..703136d 100644 --- a/tests/PhpCompatibility/Implied/ImpliedSchema011Test.php +++ b/tests/PhpCompatibility/Implied/ImpliedSchema011Test.php @@ -46,7 +46,7 @@ protected function expectXml(): string xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"> - + diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema012Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema012Test.php index 1813457..b7a7124 100644 --- a/tests/PhpCompatibility/Implied/ImpliedSchema012Test.php +++ b/tests/PhpCompatibility/Implied/ImpliedSchema012Test.php @@ -49,7 +49,7 @@ protected function expectXml(): string xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"> - + 0 diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema014Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema014Test.php new file mode 100644 index 0000000..f8a9a1c --- /dev/null +++ b/tests/PhpCompatibility/Implied/ImpliedSchema014Test.php @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + EOXML; + protected string $type = 'type="tns:return"'; + + protected function calculateParam(): mixed + { + return (object)[ + 'responses' => [ + (object)['foo' => 'abc'], + (object)['foo' => 'def', 'bar' => 'ghi'], + ], + ]; + } + + protected function registry(): EncoderRegistry + { + return parent::registry() + ->addComplexTypeConverter( + 'http://test-uri/', + 'A', + new Encoder\MatchingValueEncoder( + encoderDetector: static fn (Encoder\Context $context, mixed $value): Encoder\Context => $context->withType( + property_exists($value, 'bar') + ? $context->type->copy('B')->withXmlTypeName('B') + : $context->type + ), + defaultEncoder: new Encoder\ObjectEncoder(stdClass::class) + ) + ); + } + + protected function expectXml(): string + { + return << + + + + + abc + + + def + ghi + + + + + + XML; + } +} diff --git a/tests/PhpCompatibility/Implied/ImpliedSchema015Test.php b/tests/PhpCompatibility/Implied/ImpliedSchema015Test.php new file mode 100644 index 0000000..e66ce27 --- /dev/null +++ b/tests/PhpCompatibility/Implied/ImpliedSchema015Test.php @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + EOXML; + protected string $type = 'type="tns:return"'; + + protected function calculateParam(): mixed + { + return (object)[ + 'responses' => [ + new ImpliedSchema015A('abc'), + new ImpliedSchema015B('def', 'ghi'), + ], + ]; + } + + protected function registry(): EncoderRegistry + { + return parent::registry() + ->addClassMap('http://test-uri/', 'B', ImpliedSchema015B::class) + ->addComplexTypeConverter( + 'http://test-uri/', + 'A', + new Encoder\MatchingValueEncoder( + encoderDetector: static fn (Encoder\Context $context, mixed $value): array => + $value instanceof ImpliedSchema015B + ? [ + $context->withType($context->type->copy('B')->withXmlTypeName('B')), + new Encoder\ObjectEncoder(ImpliedSchema015B::class), + ] + : [$context], + defaultEncoder: new Encoder\ObjectEncoder(ImpliedSchema015A::class) + ) + ); + } + + protected function expectXml(): string + { + return << + + + + + abc + + + def + ghi + + + + + + XML; + } +} + +final class ImpliedSchema015A +{ + public function __construct( + public string $foo, + ) { + } +} + +final class ImpliedSchema015B +{ + public function __construct( + public string $foo, + public string $bar, + ) { + } +}