Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,29 @@ readonly class ProfileCreated
}
```

### Lazy

Since PHP 8.4, it's been possible to lazy-hydrate objects.
That is, the actual hydration process occurs when the object is accessed.
You can define for each class whether you want it to be lazy by using the `Lazy` attribute.

```php
use Patchlevel\Hydrator\Attribute\Lazy;

#[Lazy]
readonly class ProfileCreated
{
public function __construct(
public string $id,
public string $name,
) {
}
}
```

> [!NOTE]
> If you are using a PHP version older than 8.4, the attribute will be ignored.

### Hooks

Sometimes you need to do something before extract or after hydrate process.
Expand Down Expand Up @@ -592,6 +615,10 @@ final class ProfileCreated
}
```

> [!TIP]
> Cryptography is very expensive in terms of performance,
> you can combine it with lazy to improve performance and only decrypt when you actually access the object.

#### Configure Cryptography

Here we show you how to configure the cryptography.
Expand Down
16 changes: 16 additions & 0 deletions src/Attribute/Lazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final class Lazy
{
public function __construct(
public readonly bool $enabled = true,
) {
}
}
15 changes: 15 additions & 0 deletions src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\Ignore;
use Patchlevel\Hydrator\Attribute\Lazy;
use Patchlevel\Hydrator\Attribute\NormalizedName;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\PostHydrate;
Expand Down Expand Up @@ -100,6 +101,7 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
$this->getSubjectIdField($reflectionClass),
$this->getPostHydrateCallbacks($reflectionClass),
$this->getPreExtractCallbacks($reflectionClass),
$this->getLazy($reflectionClass),
);

$parentMetadataClass = $reflectionClass->getParentClass();
Expand Down Expand Up @@ -212,6 +214,18 @@ private function getPreExtractCallbacks(ReflectionClass $reflection): array
return $methods;
}

/** @param ReflectionClass<object> $reflection */
private function getLazy(ReflectionClass $reflection): bool|null
{
$attributeReflectionList = $reflection->getAttributes(Lazy::class);

if ($attributeReflectionList === []) {
return null;
}

return $attributeReflectionList[0]->newInstance()->enabled;
}

private function getFieldName(ReflectionProperty $reflectionProperty): string
{
$attributeReflectionList = $reflectionProperty->getAttributes(NormalizedName::class);
Expand Down Expand Up @@ -271,6 +285,7 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
$parentDataSubjectIdField ?? $childDataSubjectIdField,
array_merge($parent->postHydrateCallbacks(), $child->postHydrateCallbacks()),
array_merge($parent->preExtractCallbacks(), $child->preExtractCallbacks()),
$child->lazy() ?? $parent->lazy(),
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* dataSubjectIdField: string|null,
* postHydrateCallbacks: list<CallbackMetadata>,
* preExtractCallbacks: list<CallbackMetadata>,
* lazy: bool|null,
* }
* @template T of object = object
*/
Expand All @@ -30,6 +31,7 @@ public function __construct(
private readonly string|null $dataSubjectIdField = null,
private readonly array $postHydrateCallbacks = [],
private readonly array $preExtractCallbacks = [],
private readonly bool|null $lazy = null,
) {
}

Expand Down Expand Up @@ -63,6 +65,11 @@ public function preExtractCallbacks(): array
return $this->preExtractCallbacks;
}

public function lazy(): bool|null
{
return $this->lazy;
}

public function dataSubjectIdField(): string|null
{
return $this->dataSubjectIdField;
Expand Down Expand Up @@ -94,6 +101,7 @@ public function __serialize(): array
'dataSubjectIdField' => $this->dataSubjectIdField,
'postHydrateCallbacks' => $this->postHydrateCallbacks,
'preExtractCallbacks' => $this->preExtractCallbacks,
'lazy' => $this->lazy,
];
}

Expand All @@ -105,5 +113,6 @@ public function __unserialize(array $data): void
$this->dataSubjectIdField = $data['dataSubjectIdField'];
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
$this->preExtractCallbacks = $data['preExtractCallbacks'];
$this->lazy = $data['lazy'];
}
}
37 changes: 35 additions & 2 deletions src/MetadataHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Patchlevel\Hydrator\Metadata\ClassNotFound;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
use ReflectionClass;
use ReflectionParameter;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand All @@ -27,6 +28,8 @@
use function is_object;
use function spl_object_id;

use const PHP_VERSION_ID;

final class MetadataHydrator implements Hydrator
{
/** @var array<int, class-string> */
Expand All @@ -36,6 +39,7 @@
private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
PayloadCryptographer|null $cryptographer = null,
private EventDispatcherInterface|null $eventDispatcher = null,
private readonly bool $defaultLazy = false,
) {
if (!$cryptographer) {
return;
Expand Down Expand Up @@ -66,6 +70,33 @@
throw new ClassNotSupported($class, $e);
}

if (PHP_VERSION_ID < 80400) {
return $this->doHydrate($metadata, $data);
}

$lazy = $metadata->lazy() ?? $this->defaultLazy;

if (!$lazy) {
return $this->doHydrate($metadata, $data);
}

return (new ReflectionClass($class))->newLazyProxy(

Check failure on line 83 in src/MetadataHydrator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

MixedReturnStatement

src/MetadataHydrator.php:83:16: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
function () use ($metadata, $data): object {
return $this->doHydrate($metadata, $data);
},
);
}

/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
*
* @return T
*
* @template T of object
*/
private function doHydrate(ClassMetadata $metadata, array $data): object
{
if ($this->eventDispatcher) {
$data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data;
}
Expand Down Expand Up @@ -110,7 +141,7 @@
$value = $normalizer->denormalize($value);
} catch (Throwable $e) {
throw new DenormalizationFailure(
$class,
$metadata->className(),
$propertyMetadata->propertyName(),
$normalizer::class,
$e,
Expand All @@ -122,7 +153,7 @@
$propertyMetadata->setValue($object, $value);
} catch (TypeError $e) {
throw new TypeMismatch(
$class,
$metadata->className(),
$propertyMetadata->propertyName(),
$e,
);
Expand Down Expand Up @@ -234,6 +265,7 @@
public static function create(
iterable $guessers = [],
EventDispatcherInterface|null $eventDispatcher = null,
bool $defaultLazy = false,
): self {
$guesser = new BuiltInGuesser();

Expand All @@ -250,6 +282,7 @@
),
null,
$eventDispatcher,
$defaultLazy,
);
}
}
8 changes: 4 additions & 4 deletions tests/Benchmark/HydratorBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function benchExtract1Object(): void
$this->hydrator->extract($object);
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchHydrate1000Objects(): void
{
for ($i = 0; $i < 1_000; $i++) {
Expand All @@ -81,7 +81,7 @@ public function benchHydrate1000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchExtract1000Objects(): void
{
$object = new ProfileCreated(
Expand All @@ -98,7 +98,7 @@ public function benchExtract1000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchHydrate1000000Objects(): void
{
for ($i = 0; $i < 1_000_000; $i++) {
Expand All @@ -113,7 +113,7 @@ public function benchHydrate1000000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchExtract1000000Objects(): void
{
$object = new ProfileCreated(
Expand Down
8 changes: 4 additions & 4 deletions tests/Benchmark/HydratorWithCryptographyBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function benchExtract1Object(): void
$this->hydrator->extract($object);
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchHydrate1000Objects(): void
{
for ($i = 0; $i < 1_000; $i++) {
Expand All @@ -102,7 +102,7 @@ public function benchHydrate1000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchExtract1000Objects(): void
{
$object = new ProfileCreated(
Expand All @@ -119,7 +119,7 @@ public function benchExtract1000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchHydrate1000000Objects(): void
{
for ($i = 0; $i < 1_000_000; $i++) {
Expand All @@ -137,7 +137,7 @@ public function benchHydrate1000000Objects(): void
}
}

#[Bench\Revs(5)]
#[Bench\Revs(3)]
public function benchExtract1000000Objects(): void
{
$object = new ProfileCreated(
Expand Down
Loading
Loading