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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions examples/encoders/complexType/matching-value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types=1);

require_once \dirname(__DIR__, 3) . '/vendor/autoload.php';

use Soap\Encoding\Encoder;
use Soap\Encoding\EncoderRegistry;
use Soap\Encoding\Test\PhpCompatibility\Implied\ImpliedSchema015A;
use Soap\Encoding\Test\PhpCompatibility\Implied\ImpliedSchema015B;

/**
* Sometimes, your XSD schema tell you to use any implementation for a certain base type.
* When building the SOAP payload, you want control over what child object you want to use.
* This encoder can be customized to encoder and context to use for any PHP.
* This encoder works together with the XsiTypeEncoder to decode back from xsi:type attributes.
*
* Example:
* <complexType name="A">
* <sequence>
* <element name="foo" type="xsd:string" />
* </sequence>
* </complexType>
* <complexType name="B">
* <complexContent>
* <extension base="tns:A">
* <sequence>
* <element name="bar" type="xsd:string" />
* </sequence>
* </extension>
* </complexContent>
* </complexType>
* <element name="return">
* <complexType>
* <sequence>
* <element name="responses" type="tns:A" minOccurs="0" maxOccurs="unbounded" />
* </sequence>
* </complexType>
* </element>
*
*
* The result looks like this:
*
* <testParam xsi:type="tns:return">
* <responses xsi:type="tns:A">
* <foo xsi:type="xsd:string">abc</foo>
* </responses>
* <responses xsi:type="tns:B">
* <foo xsi:type="xsd:string">def</foo>
* <bar xsi:type="xsd:string">ghi</bar>
* </responses>
* </testParam>
*
* <=>
*
* ^ {#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)
));
6 changes: 3 additions & 3 deletions examples/encoders/simpleType/anyType-with-xsi-info.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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),
};
}

Expand All @@ -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);
}
}
);
30 changes: 23 additions & 7 deletions src/Encoder/EncoderDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Soap\Encoding\Encoder;

use Soap\Engine\Metadata\Model\XsdType;
use Soap\WsdlReader\Model\Definitions\BindingUse;
use stdClass;
use WeakMap;

Expand Down Expand Up @@ -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<mixed, string> $encoder
* @return XmlEncoder<mixed, string>
*/
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);
Expand All @@ -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);
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/Encoder/FixedIsoEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder;

use VeeWee\Reflecta\Iso\Iso;

/**
* @template S
* @template A
* @implements XmlEncoder<S, A>
*/
final readonly class FixedIsoEncoder implements XmlEncoder
{
/**
* @param Iso<S, A> $iso
*/
public function __construct(
private Iso $iso,
) {
}

/**
* @return Iso<S, A>
*/
public function iso(Context $context): Iso
{
return $this->iso;
}
}
62 changes: 62 additions & 0 deletions src/Encoder/MatchingValueEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder;

use Closure;
use Soap\Encoding\Xml\Node\Element;
use VeeWee\Reflecta\Iso\Iso;
use function Psl\invariant;

/**
* This encoder can be used to select an encoder based on the value being encoded.
* For decoding, it will always use the default encoder.
*
* @psalm-type MatchedEncoderInfo = Context | array{0: Context, 1 ?: XmlEncoder<mixed, string>|null}
* @psalm-type MatchingEncoderDetector = \Closure(Context, mixed): MatchedEncoderInfo
*
* @psalm-suppress UnusedClass
*
* @implements XmlEncoder<mixed, string>
*/
final readonly class MatchingValueEncoder implements XmlEncoder
{
/**
* @param MatchingEncoderDetector $encoderDetector
* @param XmlEncoder<mixed, string> $defaultEncoder
*/
public function __construct(
private Closure $encoderDetector,
private XmlEncoder $defaultEncoder,
) {
}

public function iso(Context $context): Iso
{
/** @var Iso<string, mixed> $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);
}
}
10 changes: 5 additions & 5 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 20 additions & 2 deletions src/Encoder/SimpleType/EncoderDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,11 +27,22 @@ public static function default(): self
* @return XmlEncoder<mixed, string|null>
*/
public function __invoke(Context $context): XmlEncoder
{
return $this->enhanceEncoder(
$context,
$this->detectSimpleTypeEncoder($context)
);
}

/**
* @param XmlEncoder<mixed, string> $encoder
* @return XmlEncoder<mixed, string|null>
*/
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);
}
Expand All @@ -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);
}
Expand All @@ -54,8 +71,9 @@ public function __invoke(Context $context): XmlEncoder
/**
* @return XmlEncoder<mixed, string>
*/
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:
Expand Down
7 changes: 3 additions & 4 deletions src/Encoder/SoapEnc/ApacheMapEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
])),
]),
Expand Down
3 changes: 1 addition & 2 deletions src/Encoder/SoapEnc/SoapArrayEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
Loading