Skip to content

Commit b90da84

Browse files
authored
Merge pull request #29 from veewee/any
Add support for <any />
2 parents 6e95c7e + 9eee0e7 commit b90da84

15 files changed

+528
-26
lines changed

composer.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
"require": {
2323
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
2424
"azjezz/psl": "^3.0",
25-
"veewee/reflecta": "~0.10",
25+
"veewee/reflecta": "~0.11",
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

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

src/Encoder/ErrorHandlingEncoder.php

+10-2
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
*/
+17
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+
}
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+
}
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

+51-17
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

+4-1
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)