Repository navigation
Expand file tree
/
Copy pathObjectAccess.php
More file actions
207 lines (183 loc) · 8.29 KB
/
Copy pathObjectAccess.php
File metadata and controls
207 lines (183 loc) · 8.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<?php
declare(strict_types=1);
namespace Soap\Encoding\Encoder;
use Closure;
use Soap\Encoding\Cache\ScopedCache;
use Soap\Encoding\EncoderRegistry;
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Node\ElementList;
use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use Soap\Engine\Metadata\Model\TypeMeta;
use Soap\Engine\Metadata\Model\XsdType;
use VeeWee\Reflecta\Iso\IsoInterface;
use VeeWee\Reflecta\Lens\LensInterface;
use function Psl\Vec\sort_by;
use function VeeWee\Reflecta\Lens\optional_property;
final class ObjectAccess
{
/**
* Everything the object encoder needs per property of one type, worked out once and cached per type.
* Every array is keyed by the normalized PHP property name.
*
* @param array<string, Property> $properties The metadata properties, attributes first.
* @param array<string, Closure(object): mixed> $encodeReaders Reads the property value from the PHP object on encode.
* @param array<string, Closure(ElementLookup): mixed> $decodeReaders Reads the raw XML value from the element on decode.
* @param array<string, IsoInterface<mixed, mixed, string|null, Element|ElementList|string|null>> $isos Converts between the PHP value and its XML form.
* @param bool $isAnyPropertyQualified Whether at least one property is namespace qualified.
* @param array<string, bool> $isAttribute Whether the property is an XML attribute instead of a child element.
* @param array<string, list<never>|null> $decodeDefaultValues The value a property decodes to when its element is missing from the XML.
* @param array<string, NullValueHandling> $onNull What encoding does with a null value for this property.
*/
public function __construct(
public readonly array $properties,
public readonly array $encodeReaders,
public readonly array $decodeReaders,
public readonly array $isos,
public readonly bool $isAnyPropertyQualified,
public readonly array $isAttribute,
public readonly array $decodeDefaultValues,
public readonly array $onNull,
) {
}
/**
* @return ScopedCache<EncoderRegistry, self>
*
* @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType, MixedReturnStatement
*/
private static function cache(): ScopedCache
{
static $cache = new ScopedCache();
return $cache;
}
private static function cacheKey(Context $context): string
{
return $context->type->getXmlNamespace() . '|' . $context->type->getName()
. '|' . $context->bindingUse->value;
}
public static function forContext(Context $context): self
{
return self::cache()->lookup(
$context->registry,
self::cacheKey($context),
static fn (): self => self::build($context)
);
}
private static function build(Context $context): self
{
$type = ComplexTypeBuilder::default()($context);
$sortedProperties = sort_by(
$type->getProperties(),
static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false),
);
$normalizedProperties = [];
$encodeReaders = [];
$decodeReaders = [];
$isos = [];
$isAttribute = [];
$decodeDefaultValues = [];
$onNull = [];
$isAnyPropertyQualified = false;
foreach ($sortedProperties as $property) {
$propertyType = $property->getType();
$propertyTypeMeta = $propertyType->getMeta();
$propertyContext = $context->withType($propertyType);
$name = $property->getName();
$normalizedName = PhpPropertyNameNormalizer::normalize($name);
$encoder = $context->registry->detectEncoderForContext($propertyContext);
$normalizedProperties[$normalizedName] = $property;
$customEncoderLens = self::createCustomEncoderLens($encoder, $type, $property);
/** @var Closure(object): mixed $encodeReader */
$encodeReader = $customEncoderLens !== null
? $customEncoderLens->get(...)
: optional_property($normalizedName)->get(...);
$encodeReaders[$normalizedName] = $encodeReader;
$customDecoderLens = self::createCustomDecoderLens($encoder, $type, $property);
/** @var Closure(ElementLookup): mixed $decodeReader */
$decodeReader = $customDecoderLens !== null
? $customDecoderLens->get(...)
: self::createDecoderReader($name, $propertyType);
$decodeReaders[$normalizedName] = $decodeReader;
$isos[$normalizedName] = $encoder->iso($propertyContext);
$isAttribute[$normalizedName] = $propertyTypeMeta->isAttribute()->unwrapOr(false);
$decodeDefaultValues[$normalizedName] = $propertyTypeMeta->isList()->unwrapOr(false) ? [] : null;
$onNull[$normalizedName] = self::onNull($name, $propertyTypeMeta, $encoder, $propertyContext, $customEncoderLens !== null);
$isAnyPropertyQualified = $isAnyPropertyQualified || $propertyTypeMeta->isQualified()->unwrapOr(false);
}
return new self(
$normalizedProperties,
$encodeReaders,
$decodeReaders,
$isos,
$isAnyPropertyQualified,
$isAttribute,
$decodeDefaultValues,
$onNull,
);
}
/**
* Attributes and the '_' text value always go through their encoder; elements follow what their encoder says.
* A custom lens may read something else than the property of that name, so its null never counts as absent.
*/
private static function onNull(
string $propertyName,
TypeMeta $meta,
XmlEncoder $encoder,
Context $context,
bool $hasCustomEncoderLens,
): NullValueHandling {
return match (true) {
$propertyName === '_' => NullValueHandling::Write,
$meta->isAttribute()->unwrapOr(false) => NullValueHandling::Write,
!$encoder instanceof Feature\NullValueAware => NullValueHandling::Write,
$hasCustomEncoderLens && $encoder->nullValueHandling($context) === NullValueHandling::SkipWhenAbsent => NullValueHandling::Write,
default => $encoder->nullValueHandling($context),
};
}
/**
* @return LensInterface<object|null, object|null, mixed, mixed>|null
*/
private static function createCustomEncoderLens(
XmlEncoder $encoder,
Type $type,
Property $property,
): ?LensInterface {
return match (true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomEncoderLens($encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectEncoderLens => $encoder::createObjectEncoderLens($type, $property),
default => null,
};
}
/**
* @return Closure(ElementLookup): mixed
*/
private static function createDecoderReader(string $name, XsdType $type): Closure
{
$meta = $type->getMeta();
if ($meta->isAttribute()->unwrapOr(false)) {
$namespace = $meta->isQualified()->unwrapOr(false) ? $type->getXmlTargetNamespace() : '';
return static fn (ElementLookup $lookup): ?string => $lookup->attribute($name, $namespace);
}
if ($name === '_') {
return static fn (ElementLookup $lookup): ?string => $lookup->text();
}
return static fn (ElementLookup $lookup): Element|ElementList|null => $lookup->element($name);
}
/**
* @return LensInterface<ElementLookup|null, ElementLookup|null, mixed, mixed>|null
*/
private static function createCustomDecoderLens(
XmlEncoder $encoder,
Type $type,
Property $property,
): ?LensInterface {
return match(true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomDecoderLens($encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectDecoderLens => $encoder::createObjectDecoderLens($type, $property),
default => null,
};
}
}