Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion examples/encoders/simpleType/anyType-with-xsi-info.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,7 +33,8 @@
'anyType',
new class implements
ElementContextEnhancer,
XmlEncoder {
XmlEncoder,
XsiTypeCalculator {
public function iso(Context $context): Iso
{
return (new ScalarTypeEncoder())->iso($context);
Expand All @@ -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);
}
}
);
4 changes: 4 additions & 0 deletions src/Encoder/Feature/DisregardXsiInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
}
27 changes: 27 additions & 0 deletions src/Encoder/Feature/XsiTypeCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Encoder\Feature;

use Soap\Encoding\Encoder\Context;

/**
* By implementing this feature on your simpleType encoder, you can let the encoder decide what xsi:type attribute should be set to for a given value.
*/
interface XsiTypeCalculator
{
/**
* @return string The value for the xsi:type attribute
*
* A sensible default fallback function is provided in the `ElementValueBuilder` class.
*/
public function resolveXsiTypeForValue(Context $context, mixed $value): string;


/**
* Tells the XsiAttributeBuilder that the prefix of the xsi:type should be imported as a xmlns namespace.
*
* A sensible default fallback function is provided in the `ElementValueBuilder` class.
*/
public function shouldIncludeXsiTargetNamespace(Context $context): bool;
}
41 changes: 35 additions & 6 deletions src/Xml/Writer/ElementValueBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use Generator;
use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\Feature\CData;
use Soap\Encoding\Encoder\Feature;
use Soap\Encoding\Encoder\XmlEncoder;
use Soap\Encoding\TypeInference\XsiTypeDetector;
use Soap\WsdlReader\Model\Definitions\BindingUse;
Expand Down Expand Up @@ -48,16 +48,45 @@ private function buildXsiType(XMLWriter $writer): Generator
}

$context = $this->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<bool>
*/
Expand All @@ -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)
};

Expand Down
34 changes: 33 additions & 1 deletion tests/Unit/Encoder/ElementEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)
)
),
];

Expand Down Expand Up @@ -79,6 +85,32 @@ public function enhanceElementContext(Context $context): Context
'xml' => '<bonjour>32</bonjour>',
'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' => '<hello xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:int" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">32</hello>',
'data' => 32,
];
}

public function test_it_can_decode_from_xml_item(): void
Expand Down