Skip to content

Commit b8f09d2

Browse files
authored
Merge pull request #99 from patchlevel/lazy-with-attribute
add lazy hydration feature
2 parents 05475f7 + 0145bae commit b8f09d2

11 files changed

Lines changed: 339 additions & 10 deletions

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,29 @@ readonly class ProfileCreated
473473
}
474474
```
475475

476+
### Lazy
477+
478+
Since PHP 8.4, it's been possible to lazy-hydrate objects.
479+
That is, the actual hydration process occurs when the object is accessed.
480+
You can define for each class whether you want it to be lazy by using the `Lazy` attribute.
481+
482+
```php
483+
use Patchlevel\Hydrator\Attribute\Lazy;
484+
485+
#[Lazy]
486+
readonly class ProfileCreated
487+
{
488+
public function __construct(
489+
public string $id,
490+
public string $name,
491+
) {
492+
}
493+
}
494+
```
495+
496+
> [!NOTE]
497+
> If you are using a PHP version older than 8.4, the attribute will be ignored.
498+
476499
### Hooks
477500

478501
Sometimes you need to do something before extract or after hydrate process.
@@ -623,6 +646,10 @@ final class ProfileCreated
623646
}
624647
```
625648

649+
> [!TIP]
650+
> Cryptography is very expensive in terms of performance,
651+
> you can combine it with lazy to improve performance and only decrypt when you actually access the object.
652+
626653
#### Configure Cryptography
627654

628655
Here we show you how to configure the cryptography.

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;
@@ -102,6 +103,7 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
102103
$this->getSubjectIdField($reflectionClass),
103104
$this->getPostHydrateCallbacks($reflectionClass),
104105
$this->getPreExtractCallbacks($reflectionClass),
106+
$this->getLazy($reflectionClass),
105107
);
106108

107109
$parentMetadataClass = $reflectionClass->getParentClass();
@@ -214,6 +216,18 @@ private function getPreExtractCallbacks(ReflectionClass $reflection): array
214216
return $methods;
215217
}
216218

219+
/** @param ReflectionClass<object> $reflection */
220+
private function getLazy(ReflectionClass $reflection): bool|null
221+
{
222+
$attributeReflectionList = $reflection->getAttributes(Lazy::class);
223+
224+
if ($attributeReflectionList === []) {
225+
return null;
226+
}
227+
228+
return $attributeReflectionList[0]->newInstance()->enabled;
229+
}
230+
217231
private function getFieldName(ReflectionProperty $reflectionProperty): string
218232
{
219233
$attributeReflectionList = $reflectionProperty->getAttributes(NormalizedName::class);
@@ -273,6 +287,7 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
273287
$parentDataSubjectIdField ?? $childDataSubjectIdField,
274288
array_merge($parent->postHydrateCallbacks(), $child->postHydrateCallbacks()),
275289
array_merge($parent->preExtractCallbacks(), $child->preExtractCallbacks()),
290+
$child->lazy() ?? $parent->lazy(),
276291
);
277292
}
278293

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: 35 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;
@@ -27,6 +28,8 @@
2728
use function is_object;
2829
use function spl_object_id;
2930

31+
use const PHP_VERSION_ID;
32+
3033
final class MetadataHydrator implements Hydrator
3134
{
3235
/** @var array<int, class-string> */
@@ -36,6 +39,7 @@ public function __construct(
3639
private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
3740
PayloadCryptographer|null $cryptographer = null,
3841
private EventDispatcherInterface|null $eventDispatcher = null,
42+
private readonly bool $defaultLazy = false,
3943
) {
4044
if (!$cryptographer) {
4145
return;
@@ -66,6 +70,33 @@ public function hydrate(string $class, array $data): object
6670
throw new ClassNotSupported($class, $e);
6771
}
6872

73+
if (PHP_VERSION_ID < 80400) {
74+
return $this->doHydrate($metadata, $data);
75+
}
76+
77+
$lazy = $metadata->lazy() ?? $this->defaultLazy;
78+
79+
if (!$lazy) {
80+
return $this->doHydrate($metadata, $data);
81+
}
82+
83+
return (new ReflectionClass($class))->newLazyProxy(
84+
function () use ($metadata, $data): object {
85+
return $this->doHydrate($metadata, $data);
86+
},
87+
);
88+
}
89+
90+
/**
91+
* @param ClassMetadata<T> $metadata
92+
* @param array<string, mixed> $data
93+
*
94+
* @return T
95+
*
96+
* @template T of object
97+
*/
98+
private function doHydrate(ClassMetadata $metadata, array $data): object
99+
{
69100
if ($this->eventDispatcher) {
70101
$data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data;
71102
}
@@ -110,7 +141,7 @@ public function hydrate(string $class, array $data): object
110141
$value = $normalizer->denormalize($value);
111142
} catch (Throwable $e) {
112143
throw new DenormalizationFailure(
113-
$class,
144+
$metadata->className(),
114145
$propertyMetadata->propertyName(),
115146
$normalizer::class,
116147
$e,
@@ -122,7 +153,7 @@ public function hydrate(string $class, array $data): object
122153
$propertyMetadata->setValue($object, $value);
123154
} catch (TypeError $e) {
124155
throw new TypeMismatch(
125-
$class,
156+
$metadata->className(),
126157
$propertyMetadata->propertyName(),
127158
$e,
128159
);
@@ -234,6 +265,7 @@ private function promotedConstructorParametersWithDefaultValue(ClassMetadata $me
234265
public static function create(
235266
iterable $guessers = [],
236267
EventDispatcherInterface|null $eventDispatcher = null,
268+
bool $defaultLazy = false,
237269
): self {
238270
$guesser = new BuiltInGuesser();
239271

@@ -250,6 +282,7 @@ public static function create(
250282
),
251283
null,
252284
$eventDispatcher,
285+
$defaultLazy,
253286
);
254287
}
255288
}

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(

0 commit comments

Comments
 (0)