Skip to content

Commit 51556e1

Browse files
committed
add lazy with attribute
1 parent dc33c9d commit 51556e1

File tree

7 files changed

+209
-10
lines changed

7 files changed

+209
-10
lines changed

src/Attribute/Lazy.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
final class Lazy
11+
{
12+
public function __construct(
13+
public readonly bool $enabled = true,
14+
) {
15+
}
16+
}

src/Metadata/AttributeMetadataFactory.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Patchlevel\Hydrator\Attribute\DataSubjectId;
88
use Patchlevel\Hydrator\Attribute\Ignore;
9+
use Patchlevel\Hydrator\Attribute\Lazy;
910
use Patchlevel\Hydrator\Attribute\NormalizedName;
1011
use Patchlevel\Hydrator\Attribute\PersonalData;
1112
use Patchlevel\Hydrator\Attribute\PostHydrate;
@@ -100,6 +101,7 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
100101
$this->getSubjectIdField($reflectionClass),
101102
$this->getPostHydrateCallbacks($reflectionClass),
102103
$this->getPreExtractCallbacks($reflectionClass),
104+
$this->getLazy($reflectionClass),
103105
);
104106

105107
$parentMetadataClass = $reflectionClass->getParentClass();
@@ -212,6 +214,18 @@ private function getPreExtractCallbacks(ReflectionClass $reflection): array
212214
return $methods;
213215
}
214216

217+
/** @param ReflectionClass<object> $reflection */
218+
private function getLazy(ReflectionClass $reflection): bool|null
219+
{
220+
$attributeReflectionList = $reflection->getAttributes(Lazy::class);
221+
222+
if ($attributeReflectionList === []) {
223+
return null;
224+
}
225+
226+
return $attributeReflectionList[0]->newInstance()->lazy;
227+
}
228+
215229
private function getFieldName(ReflectionProperty $reflectionProperty): string
216230
{
217231
$attributeReflectionList = $reflectionProperty->getAttributes(NormalizedName::class);
@@ -271,6 +285,7 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
271285
$parentDataSubjectIdField ?? $childDataSubjectIdField,
272286
array_merge($parent->postHydrateCallbacks(), $child->postHydrateCallbacks()),
273287
array_merge($parent->preExtractCallbacks(), $child->preExtractCallbacks()),
288+
$child->lazy() ?? $parent->lazy(),
274289
);
275290
}
276291

src/Metadata/ClassMetadata.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* dataSubjectIdField: string|null,
1414
* postHydrateCallbacks: list<CallbackMetadata>,
1515
* preExtractCallbacks: list<CallbackMetadata>,
16+
* lazy: bool|null,
1617
* }
1718
* @template T of object = object
1819
*/
@@ -30,6 +31,7 @@ public function __construct(
3031
private readonly string|null $dataSubjectIdField = null,
3132
private readonly array $postHydrateCallbacks = [],
3233
private readonly array $preExtractCallbacks = [],
34+
private readonly bool|null $lazy = null,
3335
) {
3436
}
3537

@@ -63,6 +65,11 @@ public function preExtractCallbacks(): array
6365
return $this->preExtractCallbacks;
6466
}
6567

68+
public function lazy(): bool|null
69+
{
70+
return $this->lazy;
71+
}
72+
6673
public function dataSubjectIdField(): string|null
6774
{
6875
return $this->dataSubjectIdField;
@@ -94,6 +101,7 @@ public function __serialize(): array
94101
'dataSubjectIdField' => $this->dataSubjectIdField,
95102
'postHydrateCallbacks' => $this->postHydrateCallbacks,
96103
'preExtractCallbacks' => $this->preExtractCallbacks,
104+
'lazy' => $this->lazy,
97105
];
98106
}
99107

@@ -105,5 +113,6 @@ public function __unserialize(array $data): void
105113
$this->dataSubjectIdField = $data['dataSubjectIdField'];
106114
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
107115
$this->preExtractCallbacks = $data['preExtractCallbacks'];
116+
$this->lazy = $data['lazy'];
108117
}
109118
}

src/MetadataHydrator.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Patchlevel\Hydrator\Metadata\ClassNotFound;
1717
use Patchlevel\Hydrator\Metadata\MetadataFactory;
1818
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
19+
use ReflectionClass;
1920
use ReflectionParameter;
2021
use Symfony\Component\EventDispatcher\EventDispatcher;
2122
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -36,6 +37,7 @@ public function __construct(
3637
private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
3738
PayloadCryptographer|null $cryptographer = null,
3839
private EventDispatcherInterface|null $eventDispatcher = null,
40+
private readonly bool $defaultLazy = false,
3941
) {
4042
if (!$cryptographer) {
4143
return;
@@ -66,6 +68,31 @@ public function hydrate(string $class, array $data): object
6668
throw new ClassNotSupported($class, $e);
6769
}
6870

71+
$lazy = $metadata->lazy() === null
72+
? $this->defaultLazy
73+
: $metadata->lazy();
74+
75+
if (!$lazy) {
76+
return $this->doHydrate($metadata, $data);
77+
}
78+
79+
return (new ReflectionClass($class))->newLazyProxy(
80+
function () use ($metadata, $data): object {
81+
return $this->doHydrate($metadata, $data);
82+
},
83+
);
84+
}
85+
86+
/**
87+
* @param ClassMetadata<T> $metadata
88+
* @param array<string, mixed> $data
89+
*
90+
* @return T
91+
*
92+
* @template T of object
93+
*/
94+
private function doHydrate(ClassMetadata $metadata, array $data): object
95+
{
6996
if ($this->eventDispatcher) {
7097
$data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data;
7198
}
@@ -110,7 +137,7 @@ public function hydrate(string $class, array $data): object
110137
$value = $normalizer->denormalize($value);
111138
} catch (Throwable $e) {
112139
throw new DenormalizationFailure(
113-
$class,
140+
$metadata->className(),
114141
$propertyMetadata->propertyName(),
115142
$normalizer::class,
116143
$e,
@@ -122,7 +149,7 @@ public function hydrate(string $class, array $data): object
122149
$propertyMetadata->setValue($object, $value);
123150
} catch (TypeError $e) {
124151
throw new TypeMismatch(
125-
$class,
152+
$metadata->className(),
126153
$propertyMetadata->propertyName(),
127154
$e,
128155
);
@@ -234,6 +261,7 @@ private function promotedConstructorParametersWithDefaultValue(ClassMetadata $me
234261
public static function create(
235262
iterable $guessers = [],
236263
EventDispatcherInterface|null $eventDispatcher = null,
264+
bool $defaultLazy = false,
237265
): self {
238266
$guesser = new BuiltInGuesser();
239267

@@ -250,6 +278,7 @@ public static function create(
250278
),
251279
null,
252280
$eventDispatcher,
281+
$defaultLazy,
253282
);
254283
}
255284
}

tests/Benchmark/HydratorBench.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function benchExtract1Object(): void
6666
$this->hydrator->extract($object);
6767
}
6868

69-
#[Bench\Revs(5)]
69+
#[Bench\Revs(3)]
7070
public function benchHydrate1000Objects(): void
7171
{
7272
for ($i = 0; $i < 1_000; $i++) {
@@ -81,7 +81,7 @@ public function benchHydrate1000Objects(): void
8181
}
8282
}
8383

84-
#[Bench\Revs(5)]
84+
#[Bench\Revs(3)]
8585
public function benchExtract1000Objects(): void
8686
{
8787
$object = new ProfileCreated(
@@ -98,7 +98,7 @@ public function benchExtract1000Objects(): void
9898
}
9999
}
100100

101-
#[Bench\Revs(5)]
101+
#[Bench\Revs(3)]
102102
public function benchHydrate1000000Objects(): void
103103
{
104104
for ($i = 0; $i < 1_000_000; $i++) {
@@ -113,7 +113,7 @@ public function benchHydrate1000000Objects(): void
113113
}
114114
}
115115

116-
#[Bench\Revs(5)]
116+
#[Bench\Revs(3)]
117117
public function benchExtract1000000Objects(): void
118118
{
119119
$object = new ProfileCreated(

tests/Benchmark/HydratorWithCryptographyBench.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public function benchExtract1Object(): void
8484
$this->hydrator->extract($object);
8585
}
8686

87-
#[Bench\Revs(5)]
87+
#[Bench\Revs(3)]
8888
public function benchHydrate1000Objects(): void
8989
{
9090
for ($i = 0; $i < 1_000; $i++) {
@@ -102,7 +102,7 @@ public function benchHydrate1000Objects(): void
102102
}
103103
}
104104

105-
#[Bench\Revs(5)]
105+
#[Bench\Revs(3)]
106106
public function benchExtract1000Objects(): void
107107
{
108108
$object = new ProfileCreated(
@@ -119,7 +119,7 @@ public function benchExtract1000Objects(): void
119119
}
120120
}
121121

122-
#[Bench\Revs(5)]
122+
#[Bench\Revs(3)]
123123
public function benchHydrate1000000Objects(): void
124124
{
125125
for ($i = 0; $i < 1_000_000; $i++) {
@@ -137,7 +137,7 @@ public function benchHydrate1000000Objects(): void
137137
}
138138
}
139139

140-
#[Bench\Revs(5)]
140+
#[Bench\Revs(3)]
141141
public function benchExtract1000000Objects(): void
142142
{
143143
$object = new ProfileCreated(
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Benchmark;
6+
7+
use Patchlevel\Hydrator\Hydrator;
8+
use Patchlevel\Hydrator\MetadataHydrator;
9+
use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated;
10+
use PhpBench\Attributes as Bench;
11+
12+
#[Bench\BeforeMethods('setUp')]
13+
final class HydratorWithLazyBench
14+
{
15+
private Hydrator $hydrator;
16+
17+
public function __construct()
18+
{
19+
$this->hydrator = MetadataHydrator::create(defaultLazy: true);
20+
}
21+
22+
public function setUp(): void
23+
{
24+
$object = $this->hydrator->hydrate(
25+
ProfileCreated::class,
26+
[
27+
'profileId' => '1',
28+
'name' => 'foo',
29+
'skills' => [
30+
['name' => 'php'],
31+
['name' => 'symfony'],
32+
],
33+
],
34+
);
35+
36+
$this->hydrator->extract($object);
37+
}
38+
39+
#[Bench\Revs(5)]
40+
public function benchHydrate1Object(): void
41+
{
42+
$this->hydrator->hydrate(ProfileCreated::class, [
43+
'profileId' => '1',
44+
'name' => 'foo',
45+
'skills' => [
46+
['name' => 'php'],
47+
['name' => 'symfony'],
48+
],
49+
]);
50+
}
51+
52+
#[Bench\Revs(5)]
53+
public function benchHydrate1ObjectTriggerInit(): void
54+
{
55+
$object = $this->hydrator->hydrate(ProfileCreated::class, [
56+
'profileId' => '1',
57+
'name' => 'foo',
58+
'skills' => [
59+
['name' => 'php'],
60+
['name' => 'symfony'],
61+
],
62+
]);
63+
64+
$name = $object->name;
65+
}
66+
67+
#[Bench\Revs(3)]
68+
public function benchHydrate1000Objects(): void
69+
{
70+
for ($i = 0; $i < 1_000; $i++) {
71+
$this->hydrator->hydrate(ProfileCreated::class, [
72+
'profileId' => '1',
73+
'name' => 'foo',
74+
'skills' => [
75+
['name' => 'php'],
76+
['name' => 'symfony'],
77+
],
78+
]);
79+
}
80+
}
81+
82+
#[Bench\Revs(3)]
83+
public function benchHydrate1000ObjectsTriggerInit(): void
84+
{
85+
for ($i = 0; $i < 1_000; $i++) {
86+
$object = $this->hydrator->hydrate(ProfileCreated::class, [
87+
'profileId' => '1',
88+
'name' => 'foo',
89+
'skills' => [
90+
['name' => 'php'],
91+
['name' => 'symfony'],
92+
],
93+
]);
94+
95+
$name = $object->name;
96+
}
97+
}
98+
99+
#[Bench\Revs(3)]
100+
public function benchHydrate1000000Objects(): void
101+
{
102+
for ($i = 0; $i < 1_000_000; $i++) {
103+
$this->hydrator->hydrate(ProfileCreated::class, [
104+
'profileId' => '1',
105+
'name' => 'foo',
106+
'skills' => [
107+
['name' => 'php'],
108+
['name' => 'symfony'],
109+
],
110+
]);
111+
}
112+
}
113+
114+
#[Bench\Revs(3)]
115+
public function benchHydrate1000000ObjectsTriggerInit(): void
116+
{
117+
for ($i = 0; $i < 1_000_000; $i++) {
118+
$object = $this->hydrator->hydrate(ProfileCreated::class, [
119+
'profileId' => '1',
120+
'name' => 'foo',
121+
'skills' => [
122+
['name' => 'php'],
123+
['name' => 'symfony'],
124+
],
125+
]);
126+
127+
$object = $object->name;
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)