Skip to content

Commit f2f7de2

Browse files
authored
Merge pull request #14 from veewee/precalculated-object-access
Pre-calculate object access tools
2 parents 2ac0cfc + 1bd7520 commit f2f7de2

File tree

3 files changed

+127
-97
lines changed

3 files changed

+127
-97
lines changed

src/Encoder/ObjectAccess.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Encoder;
5+
6+
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
7+
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
8+
use Soap\Engine\Metadata\Model\Property;
9+
use Soap\Engine\Metadata\Model\TypeMeta;
10+
use VeeWee\Reflecta\Iso\Iso;
11+
use VeeWee\Reflecta\Lens\Lens;
12+
use function Psl\Vec\sort_by;
13+
use function VeeWee\Reflecta\Lens\index;
14+
use function VeeWee\Reflecta\Lens\optional;
15+
use function VeeWee\Reflecta\Lens\property;
16+
17+
final class ObjectAccess
18+
{
19+
/**
20+
* @param array<string, Property> $properties
21+
* @param array<string, Lens<object, mixed>> $encoderLenses
22+
* @param array<string, Lens<array, mixed>> $decoderLenses
23+
* @param array<string, Iso<mixed, string>> $isos
24+
*/
25+
public function __construct(
26+
public readonly array $properties,
27+
public readonly array $encoderLenses,
28+
public readonly array $decoderLenses,
29+
public readonly array $isos,
30+
public readonly bool $isAnyPropertyQualified
31+
) {
32+
}
33+
34+
public static function forContext(Context $context): self
35+
{
36+
$type = ComplexTypeBuilder::default()($context);
37+
38+
$sortedProperties = sort_by(
39+
$type->getProperties(),
40+
static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false),
41+
);
42+
43+
$normalizedProperties = [];
44+
$encoderLenses = [];
45+
$decoderLenses = [];
46+
$isos = [];
47+
$isAnyPropertyQualified = false;
48+
49+
foreach ($sortedProperties as $property) {
50+
$typeMeta = $property->getType()->getMeta();
51+
$name = $property->getName();
52+
$normalizedName = PhpPropertyNameNormalizer::normalize($name);
53+
54+
$shouldLensBeOptional = self::shouldLensBeOptional($typeMeta);
55+
$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);
59+
60+
$isAnyPropertyQualified = $isAnyPropertyQualified || $typeMeta->isQualified()->unwrapOr(false);
61+
}
62+
63+
return new self(
64+
$normalizedProperties,
65+
$encoderLenses,
66+
$decoderLenses,
67+
$isos,
68+
$isAnyPropertyQualified
69+
);
70+
}
71+
72+
private static function shouldLensBeOptional(TypeMeta $meta): bool
73+
{
74+
if ($meta->isNullable()->unwrapOr(false)) {
75+
return true;
76+
}
77+
78+
if (
79+
$meta->isAttribute()->unwrapOr(false) &&
80+
$meta->use()->unwrapOr('optional') === 'optional'
81+
) {
82+
return true;
83+
}
84+
85+
return false;
86+
}
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+
}
98+
}

src/Encoder/ObjectEncoder.php

Lines changed: 27 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
use Closure;
77
use Exception;
8-
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
9-
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
108
use Soap\Encoding\TypeInference\XsiTypeDetector;
119
use Soap\Encoding\Xml\Node\Element;
1210
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
@@ -15,19 +13,11 @@
1513
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
1614
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
1715
use Soap\Engine\Metadata\Model\Property;
18-
use Soap\Engine\Metadata\Model\TypeMeta;
1916
use VeeWee\Reflecta\Iso\Iso;
2017
use VeeWee\Reflecta\Lens\Lens;
2118
use function is_array;
22-
use function Psl\Dict\map;
2319
use function Psl\Dict\map_with_key;
24-
use function Psl\Dict\reindex;
25-
use function Psl\Iter\any;
26-
use function Psl\Vec\sort_by;
2720
use function VeeWee\Reflecta\Iso\object_data;
28-
use function VeeWee\Reflecta\Lens\index;
29-
use function VeeWee\Reflecta\Lens\optional;
30-
use function VeeWee\Reflecta\Lens\property;
3121
use function VeeWee\Xml\Writer\Builder\children as writeChildren;
3222
use function VeeWee\Xml\Writer\Builder\raw;
3323
use function VeeWee\Xml\Writer\Builder\value as buildValue;
@@ -52,24 +42,24 @@ public function __construct(
5242
*/
5343
public function iso(Context $context): Iso
5444
{
55-
$properties = $this->detectProperties($context);
45+
$objectAccess = ObjectAccess::forContext($context);
5646

5747
return new Iso(
5848
/**
5949
* @param TObj|array $value
6050
* @return non-empty-string
6151
*/
62-
function (object|array $value) use ($context, $properties) : string {
63-
return $this->to($context, $properties, $value);
52+
function (object|array $value) use ($context, $objectAccess) : string {
53+
return $this->to($context, $objectAccess, $value);
6454
},
6555
/**
6656
* @param non-empty-string|Element $value
6757
* @return TObj
6858
*/
69-
function (string|Element $value) use ($context, $properties) : object {
59+
function (string|Element $value) use ($context, $objectAccess) : object {
7060
return $this->from(
7161
$context,
72-
$properties,
62+
$objectAccess,
7363
($value instanceof Element ? $value : Element::fromString($value))
7464
);
7565
}
@@ -78,19 +68,14 @@ function (string|Element $value) use ($context, $properties) : object {
7868

7969
/**
8070
* @param TObj|array $data
81-
* @param array<string, Property> $properties
8271
*
8372
* @return non-empty-string
8473
*/
85-
private function to(Context $context, array $properties, object|array $data): string
74+
private function to(Context $context, ObjectAccess $objectAccess, object|array $data): string
8675
{
8776
if (is_array($data)) {
8877
$data = (object) $data;
8978
}
90-
$isAnyPropertyQualified = any(
91-
$properties,
92-
static fn (Property $property): bool => $property->getType()->getMeta()->isQualified()->unwrapOr(false)
93-
);
9479
$defaultAction = writeChildren([]);
9580

9681
return (new XsdTypeXmlElementWriter())(
@@ -100,32 +85,31 @@ private function to(Context $context, array $properties, object|array $data): st
10085
(new XsiAttributeBuilder(
10186
$context,
10287
XsiTypeDetector::detectFromValue($context, []),
103-
includeXsiTargetNamespace: !$isAnyPropertyQualified,
88+
includeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified,
10489
)),
10590
...map_with_key(
106-
$properties,
107-
function (string $normalizePropertyName, Property $property) use ($context, $data, $defaultAction) : Closure {
91+
$objectAccess->properties,
92+
static function (string $normalizePropertyName, Property $property) use ($objectAccess, $data, $defaultAction) : Closure {
10893
$type = $property->getType();
10994
$meta = $type->getMeta();
11095
$isAttribute = $meta->isAttribute()->unwrapOr(false);
11196

11297
/** @var mixed $value */
113-
$value = $this->runLens(
114-
property($normalizePropertyName),
115-
$meta,
116-
$data,
117-
null
98+
$value = self::runLens(
99+
$objectAccess->encoderLenses[$normalizePropertyName],
100+
$data
118101
);
102+
$iso = $objectAccess->isos[$normalizePropertyName];
119103

120104
return match(true) {
121105
$isAttribute => $value ? (new AttributeBuilder(
122106
$type,
123-
$this->grabIsoForProperty($context, $property)->to($value)
107+
$iso->to($value)
124108
))(...) : $defaultAction,
125109
$property->getName() === '_' => $value
126-
? buildValue($this->grabIsoForProperty($context, $property)->to($value))
110+
? buildValue($iso->to($value))
127111
: (new NilAttributeBuilder())(...),
128-
default => $value ? raw($this->grabIsoForProperty($context, $property)->to($value)) : $defaultAction
112+
default => $value ? raw($iso->to($value)) : $defaultAction
129113
};
130114
}
131115
)
@@ -135,100 +119,46 @@ function (string $normalizePropertyName, Property $property) use ($context, $dat
135119
}
136120

137121
/**
138-
* @param array<string, Property> $properties
139-
*
140122
* @return TObj
141123
*/
142-
private function from(Context $context, array $properties, Element $data): object
124+
private function from(Context $context, ObjectAccess $objectAccess, Element $data): object
143125
{
144126
$nodes = (new DocumentToLookupArrayReader())($data);
145127
/** @var Iso<TObj, array<string, mixed>> $objectData */
146128
$objectData = object_data($this->className);
147129

148130
return $objectData->from(
149-
map(
150-
$properties,
151-
function (Property $property) use ($context, $nodes): mixed {
131+
map_with_key(
132+
$objectAccess->properties,
133+
static function (string $normalizePropertyName, Property $property) use ($objectAccess, $nodes): mixed {
152134
$type = $property->getType();
153135
$meta = $type->getMeta();
154136

155137
/** @var string|null $value */
156-
$value = $this->runLens(
157-
index($property->getName()),
158-
$meta,
138+
$value = self::runLens(
139+
$objectAccess->decoderLenses[$normalizePropertyName],
159140
$nodes,
160-
null
161141
);
142+
$iso = $objectAccess->isos[$normalizePropertyName];
162143
$defaultValue = $meta->isList()->unwrapOr(false) ? [] : null;
163144

164145
/** @psalm-suppress PossiblyNullArgument */
165146
return match(true) {
166-
$meta->isAttribute()->unwrapOr(false) => $this->grabIsoForProperty($context, $property)->from($value),
167-
default => $value !== null ? $this->grabIsoForProperty($context, $property)->from($value) : $defaultValue,
147+
$meta->isAttribute()->unwrapOr(false) => $iso->from($value),
148+
default => $value !== null ? $iso->from($value) : $defaultValue,
168149
};
169150
},
170151
)
171152
);
172153
}
173154

174-
/**
175-
* @return Iso<mixed, string>
176-
*/
177-
private function grabIsoForProperty(Context $context, Property $property): Iso
178-
{
179-
$propertyContext = $context->withType($property->getType());
180-
181-
return $context->registry->detectEncoderForContext($propertyContext)
182-
->iso($propertyContext);
183-
}
184-
185-
private function runLens(Lens $lens, TypeMeta $meta, mixed $data, mixed $default): mixed
155+
private static function runLens(Lens $lens, mixed $data, mixed $default = null): mixed
186156
{
187157
try {
188158
/** @var mixed */
189-
return $this->decorateLensForType($lens, $meta)->get($data);
159+
return $lens->get($data);
190160
} catch (Exception $e) {
191161
return $default;
192162
}
193163
}
194-
195-
/**
196-
* @template S
197-
* @template A
198-
*
199-
* @param Lens<S, A> $lens
200-
*
201-
* @return Lens<S, A>
202-
*/
203-
private function decorateLensForType(Lens $lens, TypeMeta $meta): Lens
204-
{
205-
if ($meta->isNullable()->unwrapOr(false)) {
206-
return optional($lens);
207-
}
208-
209-
if (
210-
$meta->isAttribute()->unwrapOr(false) &&
211-
$meta->use()->unwrapOr('optional') === 'optional'
212-
) {
213-
return optional($lens);
214-
}
215-
216-
return $lens;
217-
}
218-
219-
/**
220-
* @return array<string, Property>
221-
*/
222-
private function detectProperties(Context $context): array
223-
{
224-
$type = ComplexTypeBuilder::default()($context);
225-
226-
return reindex(
227-
sort_by(
228-
$type->getProperties(),
229-
static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false),
230-
),
231-
static fn (Property $property): string => PhpPropertyNameNormalizer::normalize($property->getName()),
232-
);
233-
}
234164
}

tests/Unit/Encoder/ObjectEncoderTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use PHPUnit\Framework\Attributes\CoversClass;
77
use Soap\Encoding\Encoder\Context;
8+
use Soap\Encoding\Encoder\ObjectAccess;
89
use Soap\Encoding\Encoder\ObjectEncoder;
910
use Soap\Encoding\Test\Fixture\Model\Hat;
1011
use Soap\Encoding\Test\Fixture\Model\User;
@@ -21,6 +22,7 @@
2122
use function Psl\Fun\tap;
2223

2324
#[CoversClass(ObjectEncoder::class)]
25+
#[CoversClass(ObjectAccess::class)]
2426
final class ObjectEncoderTest extends AbstractEncoderTests
2527
{
2628
public static function provideIsomorphicCases(): iterable

0 commit comments

Comments
 (0)