diff --git a/examples/encoders/simpleType/anyType-with-xsi-info.php b/examples/encoders/simpleType/anyType-with-xsi-info.php index d875f75..53947ed 100644 --- a/examples/encoders/simpleType/anyType-with-xsi-info.php +++ b/examples/encoders/simpleType/anyType-with-xsi-info.php @@ -3,9 +3,11 @@ use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\Feature\ElementContextEnhancer; +use Soap\Encoding\Encoder\Feature\XsiTypeCalculator; use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder; use Soap\Encoding\Encoder\XmlEncoder; use Soap\Encoding\EncoderRegistry; +use Soap\Encoding\Xml\Writer\ElementValueBuilder; use Soap\WsdlReader\Model\Definitions\BindingUse; use VeeWee\Reflecta\Iso\Iso; @@ -31,7 +33,8 @@ 'anyType', new class implements ElementContextEnhancer, - XmlEncoder { + XmlEncoder, + XsiTypeCalculator { public function iso(Context $context): Iso { return (new ScalarTypeEncoder())->iso($context); @@ -45,5 +48,31 @@ public function enhanceElementContext(Context $context): Context { return $context->withBindingUse(BindingUse::ENCODED); } + + /** + * Can be used to fine-tune the xsi:type element. + * For example, xsi:type="xsd:date" when dealing with value's like `DateTimeImmutable`. + * + * A default fallback function is provided in the ElementValueBuilder class. + */ + 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), + }; + } + + /** + * Determines if the xmlns of the xsi:type prefix should be imported. + * For example: xsd:date will import xmlns:xsd="...". + * + * A default fallback function is provided in the ElementValueBuilder class. + */ + public function shouldIncludeXsiTargetNamespace(Context $context): bool + { + return ElementValueBuilder::shouldIncludeXsiTargetNamespace($context); + } } ); diff --git a/src/Encoder/Feature/DisregardXsiInformation.php b/src/Encoder/Feature/DisregardXsiInformation.php index d3b14a6..0407785 100644 --- a/src/Encoder/Feature/DisregardXsiInformation.php +++ b/src/Encoder/Feature/DisregardXsiInformation.php @@ -3,6 +3,10 @@ namespace Soap\Encoding\Encoder\Feature; +/** + * Tells the decoder to disregard any xsi:type information on the element when decoding an element. + * It will use the original provided decoder by default and won't try to guess the decoder based on xsi:type. + */ interface DisregardXsiInformation { } diff --git a/src/Encoder/Feature/XsiTypeCalculator.php b/src/Encoder/Feature/XsiTypeCalculator.php new file mode 100644 index 0000000..d4fc319 --- /dev/null +++ b/src/Encoder/Feature/XsiTypeCalculator.php @@ -0,0 +1,27 @@ +context; - $type = $context->type; + [$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( $this->context, - XsiTypeDetector::detectFromValue($context, $this->value), - includeXsiTargetNamespace: $type->getXmlTargetNamespace() !== $type->getXmlNamespace() - || !$type->getMeta()->isQualified()->unwrapOr(false) + $xsiType, + $includeXsiTargetNamespace, ))($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. + */ + 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 */ @@ -66,7 +95,7 @@ private function buildValue(XMLWriter $writer): Generator $encoded = $this->encoder->iso($this->context)->to($this->value); $builder = match (true) { - $this->encoder instanceof CData => cdata(value($encoded)), + $this->encoder instanceof Feature\CData => cdata(value($encoded)), default => value($encoded) }; diff --git a/tests/Unit/Encoder/ElementEncoderTest.php b/tests/Unit/Encoder/ElementEncoderTest.php index b19e0e0..97cf3d3 100644 --- a/tests/Unit/Encoder/ElementEncoderTest.php +++ b/tests/Unit/Encoder/ElementEncoderTest.php @@ -7,12 +7,14 @@ use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\ElementEncoder; use Soap\Encoding\Encoder\Feature\ElementContextEnhancer; +use Soap\Encoding\Encoder\Feature\XsiTypeCalculator; use Soap\Encoding\Encoder\SimpleType\IntTypeEncoder; use Soap\Encoding\Encoder\SimpleType\StringTypeEncoder; use Soap\Encoding\Encoder\XmlEncoder; use Soap\Encoding\Xml\Node\Element; use Soap\Engine\Metadata\Model\TypeMeta; use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Model\Definitions\BindingUse; use VeeWee\Reflecta\Iso\Iso; #[CoversClass(ElementEncoder::class)] @@ -25,7 +27,11 @@ public static function provideIsomorphicCases(): iterable 'context' => $context = self::createContext( $xsdType = XsdType::guess('string') ->withXmlTargetNodeName('hello') - ->withMeta(static fn (TypeMeta $meta): TypeMeta => $meta->withIsQualified(true)) + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsQualified(true) + ->withIsSimple(true) + ) ), ]; @@ -79,6 +85,32 @@ public function enhanceElementContext(Context $context): Context 'xml' => '32', 'data' => 32, ]; + yield 'xsi-type-calculating-encoder' => [ + ...$baseConfig, + 'encoder' => $encoder = new ElementEncoder(new class implements ElementContextEnhancer, XmlEncoder, XsiTypeCalculator { + public function iso(Context $context): Iso + { + return (new IntTypeEncoder())->iso($context); + } + + public function enhanceElementContext(Context $context): Context + { + return $context->withBindingUse(BindingUse::ENCODED); + } + + public function resolveXsiTypeForValue(Context $context, mixed $value): string + { + return 'xsd:'.get_debug_type($value); + } + + public function shouldIncludeXsiTargetNamespace(Context $context): bool + { + return true; + } + }), + 'xml' => '32', + 'data' => 32, + ]; } public function test_it_can_decode_from_xml_item(): void