Skip to content

Commit a848a11

Browse files
committed
Add support for <any />
1 parent 6e95c7e commit a848a11

15 files changed

+529
-25
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
"azjezz/psl": "^3.0",
2525
"veewee/reflecta": "~0.10",
2626
"veewee/xml": "^3.3",
27-
"php-soap/engine": "^2.13",
27+
"php-soap/engine": "^2.14",
2828
"php-soap/wsdl": "^1.12",
2929
"php-soap/xml": "^1.8",
30-
"php-soap/wsdl-reader": "~0.18"
30+
"php-soap/wsdl-reader": "~0.20"
3131
},
3232
"require-dev": {
3333
"vimeo/psalm": "^5.26",

src/Encoder/AnyElementEncoder.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder;
4+
5+
use RuntimeException;
6+
use Soap\Encoding\Xml\Node\Element;
7+
use Soap\Encoding\Xml\Node\ElementList;
8+
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
9+
use Soap\Engine\Metadata\Model\Property;
10+
use Soap\Engine\Metadata\Model\Type;
11+
use VeeWee\Reflecta\Iso\Iso;
12+
use VeeWee\Reflecta\Lens\Lens;
13+
use function is_array;
14+
use function is_string;
15+
use function Psl\Dict\diff_by_key;
16+
use function Psl\Iter\first;
17+
use function Psl\Iter\reduce;
18+
use function Psl\Str\join;
19+
use function Psl\Type\string;
20+
use function Psl\Type\vec;
21+
22+
/**
23+
* @implements XmlEncoder<array|string|null, string>
24+
*
25+
* @psalm-import-type LookupArray from DocumentToLookupArrayReader
26+
*
27+
* @template-implements Feature\ProvidesObjectDecoderLens<LookupArray, ElementList>
28+
*/
29+
final class AnyElementEncoder implements Feature\ListAware, Feature\OptionalAware, Feature\ProvidesObjectDecoderLens, XmlEncoder
30+
{
31+
/**
32+
* This lens will be used to decode XML into an 'any' property.
33+
* It will contain all the XML tags available in the object that is surrounding the 'any' property.
34+
* Properties that are already known by the object, will be omitted.
35+
*
36+
* @return Lens<LookupArray, ElementList>
37+
*/
38+
public static function createObjectDecoderLens(Type $parentType, Property $currentProperty): Lens
39+
{
40+
$omittedKeys = reduce(
41+
$parentType->getProperties(),
42+
static fn (array $omit, Property $property): array => [
43+
...$omit,
44+
...($property->getName() !== $currentProperty->getName() ? [$property->getName()] : []),
45+
],
46+
[]
47+
);
48+
49+
/**
50+
* @param LookupArray $data
51+
* @return LookupArray
52+
*/
53+
$omit = static fn (array $data): array => diff_by_key($data, array_flip($omittedKeys));
54+
55+
/** @var Lens<LookupArray, ElementList> */
56+
return new Lens(
57+
/**
58+
* @psalm-suppress MixedArgumentTypeCoercion - Psalm gets confused about the result of omit.
59+
* @param LookupArray $data
60+
*/
61+
static fn (array $data): ElementList => ElementList::fromLookupArray($omit($data)),
62+
static fn (array $_data, ElementList $_value): never => throw new RuntimeException('Readonly lens')
63+
);
64+
}
65+
66+
/**
67+
* @return Iso<array|string|null, string>
68+
*/
69+
public function iso(Context $context): Iso
70+
{
71+
$meta = $context->type->getMeta();
72+
$isNullable = $meta->isNullable()->unwrapOr(false);
73+
$isList = $meta->isList()->unwrapOr(false);
74+
75+
return new Iso(
76+
static fn (string|array|null $raw): string => match (true) {
77+
is_string($raw) => $raw,
78+
is_array($raw) => join(vec(string())->assert($raw), ''),
79+
default => '',
80+
},
81+
/**
82+
* @psalm-suppress DocblockTypeContradiction - Psalm gets confused about the return type of first() in default case.
83+
* @psalm-return null|array<array-key, string>|string
84+
*/
85+
static fn (ElementList|string $xml): mixed => match(true) {
86+
is_string($xml) => $xml,
87+
$isList && !$xml->hasElements() => [],
88+
$isNullable && !$xml->hasElements() => null,
89+
$isList => $xml->traverse(static fn (Element $element) => $element->value()),
90+
default => first($xml->elements())?->value(),
91+
}
92+
);
93+
}
94+
}

src/Encoder/ErrorHandlingEncoder.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
* @template-covariant TXml
1313
*
1414
* @implements XmlEncoder<TData, TXml>
15-
*
15+
* @implements Feature\DecoratingEncoder<TData, TXml>
1616
*/
17-
final class ErrorHandlingEncoder implements XmlEncoder
17+
final class ErrorHandlingEncoder implements Feature\DecoratingEncoder, XmlEncoder
1818
{
1919
/**
2020
* @param XmlEncoder<TData, TXml> $encoder
@@ -24,6 +24,14 @@ public function __construct(
2424
) {
2525
}
2626

27+
/**
28+
* @return XmlEncoder<TData, TXml>
29+
*/
30+
public function decoratedEncoder(): XmlEncoder
31+
{
32+
return $this->encoder;
33+
}
34+
2735
/**
2836
* @return Iso<TData, TXml>
2937
*/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder\Feature;
4+
5+
use Soap\Encoding\Encoder\XmlEncoder;
6+
7+
/**
8+
* @template-covariant TData
9+
* @template-covariant TXml
10+
*/
11+
interface DecoratingEncoder
12+
{
13+
/**
14+
* @return XmlEncoder<TData, TXml>
15+
*/
16+
public function decoratedEncoder(): XmlEncoder;
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder\Feature;
4+
5+
use Soap\Engine\Metadata\Model\Property;
6+
use Soap\Engine\Metadata\Model\Type;
7+
use VeeWee\Reflecta\Lens\Lens;
8+
9+
/**
10+
* When an encoder implements this feature interface, it knows how to create a lens that will be applied on the parent data that is being decoded.
11+
*
12+
* @template-covariant S
13+
* @template-covariant A
14+
*/
15+
interface ProvidesObjectDecoderLens
16+
{
17+
/**
18+
* @return Lens<S, A>
19+
*/
20+
public static function createObjectDecoderLens(Type $parentType, Property $currentProperty): Lens;
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Encoding\Encoder\Feature;
4+
5+
use Soap\Engine\Metadata\Model\Property;
6+
use Soap\Engine\Metadata\Model\Type;
7+
use VeeWee\Reflecta\Lens\Lens;
8+
9+
/**
10+
* When an encoder implements this feature interface, it knows how to create a lens that will be applied on the parent data that is being encoded.
11+
*
12+
* @template-covariant S
13+
* @template-covariant A
14+
*/
15+
interface ProvidesObjectEncoderLens
16+
{
17+
/**
18+
* @return Lens<S, A>
19+
*/
20+
public static function createObjectEncoderLens(Type $parentType, Property $currentProperty): Lens;
21+
}

src/Encoder/ObjectAccess.php

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
77
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
88
use Soap\Engine\Metadata\Model\Property;
9+
use Soap\Engine\Metadata\Model\Type;
910
use Soap\Engine\Metadata\Model\TypeMeta;
1011
use VeeWee\Reflecta\Iso\Iso;
1112
use VeeWee\Reflecta\Lens\Lens;
@@ -47,17 +48,21 @@ public static function forContext(Context $context): self
4748
$isAnyPropertyQualified = false;
4849

4950
foreach ($sortedProperties as $property) {
50-
$typeMeta = $property->getType()->getMeta();
51+
$propertyType = $property->getType();
52+
$propertyTypeMeta = $propertyType->getMeta();
53+
$propertyContext = $context->withType($propertyType);
5154
$name = $property->getName();
5255
$normalizedName = PhpPropertyNameNormalizer::normalize($name);
5356

54-
$shouldLensBeOptional = self::shouldLensBeOptional($typeMeta);
57+
$encoder = $context->registry->detectEncoderForContext($propertyContext);
58+
$shouldLensBeOptional = self::shouldLensBeOptional($propertyTypeMeta);
5559
$normalizedProperties[$normalizedName] = $property;
56-
$encoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(property($normalizedName)) : property($normalizedName);
57-
$decoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(index($name)) : index($name);
58-
$isos[$normalizedName] = self::grabIsoForProperty($context, $property);
5960

60-
$isAnyPropertyQualified = $isAnyPropertyQualified || $typeMeta->isQualified()->unwrapOr(false);
61+
$encoderLenses[$normalizedName] = self::createEncoderLensForType($shouldLensBeOptional, $normalizedName, $encoder, $type, $property);
62+
$decoderLenses[$normalizedName] = self::createDecoderLensForType($shouldLensBeOptional, $name, $encoder, $type, $property);
63+
$isos[$normalizedName] = $encoder->iso($propertyContext);
64+
65+
$isAnyPropertyQualified = $isAnyPropertyQualified || $propertyTypeMeta->isQualified()->unwrapOr(false);
6166
}
6267

6368
return new self(
@@ -69,6 +74,46 @@ public static function forContext(Context $context): self
6974
);
7075
}
7176

77+
/**
78+
* @return Lens<object, mixed>
79+
*/
80+
private static function createEncoderLensForType(
81+
bool $shouldLensBeOptional,
82+
string $normalizedName,
83+
XmlEncoder $encoder,
84+
Type $type,
85+
Property $property,
86+
): Lens {
87+
$lens = match (true) {
88+
$encoder instanceof Feature\DecoratingEncoder => self::createEncoderLensForType($shouldLensBeOptional, $normalizedName, $encoder->decoratedEncoder(), $type, $property),
89+
$encoder instanceof Feature\ProvidesObjectEncoderLens => $encoder::createObjectEncoderLens($type, $property),
90+
default => property($normalizedName)
91+
};
92+
93+
/** @var Lens<object, mixed> */
94+
return $shouldLensBeOptional ? optional($lens) : $lens;
95+
}
96+
97+
/**
98+
* @return Lens<array, mixed>
99+
*/
100+
private static function createDecoderLensForType(
101+
bool $shouldLensBeOptional,
102+
string $name,
103+
XmlEncoder $encoder,
104+
Type $type,
105+
Property $property,
106+
): Lens {
107+
$lens = match(true) {
108+
$encoder instanceof Feature\DecoratingEncoder => self::createDecoderLensForType($shouldLensBeOptional, $name, $encoder->decoratedEncoder(), $type, $property),
109+
$encoder instanceof Feature\ProvidesObjectDecoderLens => $encoder::createObjectDecoderLens($type, $property),
110+
default => index($name),
111+
};
112+
113+
/** @var Lens<array, mixed> */
114+
return $shouldLensBeOptional ? optional($lens) : $lens;
115+
}
116+
72117
private static function shouldLensBeOptional(TypeMeta $meta): bool
73118
{
74119
if ($meta->isNullable()->unwrapOr(false)) {
@@ -84,15 +129,4 @@ private static function shouldLensBeOptional(TypeMeta $meta): bool
84129

85130
return false;
86131
}
87-
88-
/**
89-
* @return Iso<mixed, string>
90-
*/
91-
private static function grabIsoForProperty(Context $context, Property $property): Iso
92-
{
93-
$propertyContext = $context->withType($property->getType());
94-
95-
return $context->registry->detectEncoderForContext($propertyContext)
96-
->iso($propertyContext);
97-
}
98132
}

src/EncoderRegistry.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Psl\Collection\MutableMap;
77
use Soap\Encoding\ClassMap\ClassMapCollection;
8+
use Soap\Encoding\Encoder\AnyElementEncoder;
89
use Soap\Encoding\Encoder\Context;
910
use Soap\Encoding\Encoder\ElementEncoder;
1011
use Soap\Encoding\Encoder\EncoderDetector;
@@ -106,7 +107,6 @@ public static function default(): self
106107
$qNameFormatter($xsd, 'decimal') => new SimpleType\FloatTypeEncoder(),
107108

108109
// Scalar:
109-
$qNameFormatter($xsd, 'any') => new SimpleType\ScalarTypeEncoder(),
110110
$qNameFormatter($xsd, 'anyType') => new SimpleType\ScalarTypeEncoder(),
111111
$qNameFormatter($xsd, 'anyXML') => new SimpleType\ScalarTypeEncoder(),
112112
$qNameFormatter($xsd, 'anySimpleType') => new SimpleType\ScalarTypeEncoder(),
@@ -159,6 +159,9 @@ public static function default(): self
159159

160160
// Apache Map
161161
$qNameFormatter(ApacheMapDetector::NAMESPACE, 'Map') => new SoapEnc\ApacheMapEncoder(),
162+
163+
// Special XSD cases
164+
$qNameFormatter($xsd, 'any') => new AnyElementEncoder(),
162165
])
163166
);
164167
}

0 commit comments

Comments
 (0)