Skip to content

Commit

Permalink
Merge pull request #55 from patchlevel/hooks
Browse files Browse the repository at this point in the history
add pre extract and post hydrate hooks
  • Loading branch information
DavidBadura authored Sep 18, 2024
2 parents 89cb4fa + 06840d2 commit b8951b3
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 1 deletion.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,31 @@ readonly class ProfileCreated
}
```

### Hooks

Sometimes you need to do something before extract or after hydrate process.
For this we have the `PreExtract` and `PostHydrate` attributes.

```php
use Patchlevel\Hydrator\Attribute\PostHydrate;
use Patchlevel\Hydrator\Attribute\PreExtract;

readonly class Dto
{
#[PostHydrate]
private function postHydrate(): void
{
// do something
}

#[PreExtract]
private function preExtract(): void
{
// do something
}
}
```

### Cryptography

The library also offers the possibility to encrypt and decrypt personal data.
Expand Down
12 changes: 12 additions & 0 deletions src/Attribute/PostHydrate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
final class PostHydrate
{
}
12 changes: 12 additions & 0 deletions src/Attribute/PreExtract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
final class PreExtract
{
}
51 changes: 51 additions & 0 deletions src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Patchlevel\Hydrator\Attribute\Ignore;
use Patchlevel\Hydrator\Attribute\NormalizedName;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\PostHydrate;
use Patchlevel\Hydrator\Attribute\PreExtract;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
Expand All @@ -24,6 +26,7 @@
use ReflectionProperty;

use function array_key_exists;
use function array_merge;
use function array_values;
use function class_exists;
use function is_a;
Expand Down Expand Up @@ -80,6 +83,8 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
$reflectionClass,
$this->getPropertyMetadataList($reflectionClass),
$this->getSubjectIdField($reflectionClass),
$this->getPostHydrateCallbacks($reflectionClass),
$this->getPreExtractCallbacks($reflectionClass),
);

$parentMetadataClass = $reflectionClass->getParentClass();
Expand Down Expand Up @@ -135,6 +140,50 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra
return array_values($properties);
}

/** @return list<CallbackMetadata> */
private function getPostHydrateCallbacks(ReflectionClass $reflection): array
{
$methods = [];

foreach ($reflection->getMethods() as $reflectionMethod) {
if ($reflectionMethod->isStatic()) {
continue;
}

$attributeReflectionList = $reflectionMethod->getAttributes(PostHydrate::class);

if ($attributeReflectionList === []) {
continue;
}

$methods[] = new CallbackMetadata($reflectionMethod);
}

return $methods;
}

/** @return list<CallbackMetadata> */
private function getPreExtractCallbacks(ReflectionClass $reflection): array
{
$methods = [];

foreach ($reflection->getMethods() as $reflectionMethod) {
if ($reflectionMethod->isStatic()) {
continue;
}

$attributeReflectionList = $reflectionMethod->getAttributes(PreExtract::class);

if ($attributeReflectionList === []) {
continue;
}

$methods[] = new CallbackMetadata($reflectionMethod);
}

return $methods;
}

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

Expand Down
51 changes: 51 additions & 0 deletions src/Metadata/CallbackMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Metadata;

use ReflectionMethod;

/**
* @psalm-type serialized = array{
* className: class-string,
* method: string,
* }
*/
final class CallbackMetadata
{
public function __construct(
private readonly ReflectionMethod $reflection,
) {
}

public function reflection(): ReflectionMethod
{
return $this->reflection;
}

public function methodName(): string
{
return $this->reflection->getName();
}

public function invoke(object $object): void
{
$this->reflection->invoke($object);
}

/** @return serialized */
public function __serialize(): array
{
return [
'className' => $this->reflection->getDeclaringClass()->getName(),
'method' => $this->reflection->getName(),
];
}

/** @param serialized $data */
public function __unserialize(array $data): void
{
$this->reflection = new ReflectionMethod($data['className'], $data['method']);
}
}
24 changes: 23 additions & 1 deletion src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
* @psalm-type serialized array{
* className: class-string,
* properties: list<PropertyMetadata>,
* dataSubjectIdField: string|null
* dataSubjectIdField: string|null,
* postHydrateCallbacks: list<CallbackMetadata>,
* preExtractCallbacks: list<CallbackMetadata>,
* }
* @template T of object
*/
Expand All @@ -19,11 +21,15 @@ final class ClassMetadata
/**
* @param ReflectionClass<T> $reflection
* @param list<PropertyMetadata> $properties
* @param list<CallbackMetadata> $postHydrateCallbacks
* @param list<CallbackMetadata> $preExtractCallbacks
*/
public function __construct(
private readonly ReflectionClass $reflection,
private readonly array $properties = [],
private readonly string|null $dataSubjectIdField = null,
private readonly array $postHydrateCallbacks = [],
private readonly array $preExtractCallbacks = [],
) {
}

Expand All @@ -45,6 +51,18 @@ public function properties(): array
return $this->properties;
}

/** @return list<CallbackMetadata> */
public function postHydrateCallbacks(): array
{
return $this->postHydrateCallbacks;
}

/** @return list<CallbackMetadata> */
public function preExtractCallbacks(): array
{
return $this->preExtractCallbacks;
}

public function dataSubjectIdField(): string|null
{
return $this->dataSubjectIdField;
Expand Down Expand Up @@ -74,6 +92,8 @@ public function __serialize(): array
'className' => $this->reflection->getName(),
'properties' => $this->properties,
'dataSubjectIdField' => $this->dataSubjectIdField,
'postHydrateCallbacks' => $this->postHydrateCallbacks,
'preExtractCallbacks' => $this->preExtractCallbacks,
];
}

Expand All @@ -83,5 +103,7 @@ public function __unserialize(array $data): void
$this->reflection = new ReflectionClass($data['className']);
$this->properties = $data['properties'];
$this->dataSubjectIdField = $data['dataSubjectIdField'];
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
$this->preExtractCallbacks = $data['preExtractCallbacks'];
}
}
8 changes: 8 additions & 0 deletions src/MetadataHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ public function hydrate(string $class, array $data): object
}
}

foreach ($metadata->postHydrateCallbacks() as $callback) {
$callback->invoke($object);
}

return $object;
}

Expand All @@ -124,6 +128,10 @@ public function extract(object $object): array
try {
$metadata = $this->metadataFactory->metadata($object::class);

foreach ($metadata->preExtractCallbacks() as $callback) {
$callback->invoke($object);
}

$data = [];

foreach ($metadata->properties() as $propertyMetadata) {
Expand Down
27 changes: 27 additions & 0 deletions tests/Unit/Fixture/DtoWithHooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Fixture;

use Patchlevel\Hydrator\Attribute\PostHydrate;
use Patchlevel\Hydrator\Attribute\PreExtract;

final class DtoWithHooks
{
public bool $postHydrateCalled = false;

public bool $preExtractCalled = false;

#[PostHydrate]
private function postHydrate(): void
{
$this->postHydrateCalled = true;
}

#[PreExtract]
private function preExtract(): void
{
$this->preExtractCalled = true;
}
}
53 changes: 53 additions & 0 deletions tests/Unit/Metadata/AttributeMetadataFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\NormalizedName;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\PostHydrate;
use Patchlevel\Hydrator\Attribute\PreExtract;
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Metadata\DuplicatedFieldNameInMetadata;
use Patchlevel\Hydrator\Metadata\MissingDataSubjectId;
Expand Down Expand Up @@ -38,6 +40,8 @@ public function testEmptyObject(): void
$metadata = $metadataFactory->metadata($object::class);

self::assertCount(0, $metadata->properties());
self::assertCount(0, $metadata->preExtractCallbacks());
self::assertCount(0, $metadata->postHydrateCallbacks());
}

public function testNotFoundProperty(): void
Expand Down Expand Up @@ -358,4 +362,53 @@ public function testExtendsWithPersonalData(): void
self::assertTrue($emailPropertyMetadata->isPersonalData());
self::assertInstanceOf(EmailNormalizer::class, $emailPropertyMetadata->normalizer());
}

public function testHooks(): void
{
$object = new class {
#[PreExtract]
private function preExtract(): void
{
}

#[PostHydrate]
private function postHydrate(): void
{
}
};

$metadataFactory = new AttributeMetadataFactory();
$metadata = $metadataFactory->metadata($object::class);

$preExtract = $metadata->preExtractCallbacks();

self::assertCount(1, $preExtract);
self::assertSame('preExtract', $preExtract[0]->methodName());

$postHydrate = $metadata->postHydrateCallbacks();

self::assertCount(1, $postHydrate);
self::assertSame('postHydrate', $postHydrate[0]->methodName());
}

public function testSkipStaticHook(): void
{
$object = new class {
#[PreExtract]
private static function preExtract(): void
{
}

#[PostHydrate]
private static function postHydrate(): void
{
}
};

$metadataFactory = new AttributeMetadataFactory();
$metadata = $metadataFactory->metadata($object::class);

self::assertCount(0, $metadata->preExtractCallbacks());
self::assertCount(0, $metadata->postHydrateCallbacks());
}
}
Loading

0 comments on commit b8951b3

Please sign in to comment.