Skip to content
Closed
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
95 changes: 93 additions & 2 deletions src/Cryptography/CryptographySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
namespace Patchlevel\Hydrator\Cryptography;

use Patchlevel\Hydrator\Event\PostExtract;
use Patchlevel\Hydrator\Event\PreExtract;
use Patchlevel\Hydrator\Event\PreHydrate;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Stringable;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use function is_int;
use function is_string;

final class CryptographySubscriber implements EventSubscriberInterface
{
Expand All @@ -17,20 +22,106 @@

public function preHydrate(PreHydrate $event): void
{
$event->data = $this->cryptography->decrypt($event->metadata, $event->data);
$parentSubjectId = $event->context['subjectId'] ?? null;

Check failure on line 25 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/CryptographySubscriber.php:25:9: MixedAssignment: Unable to determine the type that $parentSubjectId is being assigned to (see https://psalm.dev/032)

$currentSubjectId = $this->subjectIdFromData($event->data, $event->metadata) ?: $parentSubjectId;

Check failure on line 27 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

RiskyTruthyFalsyComparison

src/Cryptography/CryptographySubscriber.php:27:29: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 27 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/CryptographySubscriber.php:27:9: MixedAssignment: Unable to determine the type that $currentSubjectId is being assigned to (see https://psalm.dev/032)
$event->data = $this->cryptography->decrypt($event->metadata, $event->data, $currentSubjectId);

Check failure on line 28 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

Method Patchlevel\Hydrator\Cryptography\PayloadCryptographer::decrypt() invoked with 3 parameters, 2 required.

Check failure on line 28 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

TooManyArguments

src/Cryptography/CryptographySubscriber.php:28:45: TooManyArguments: Too many arguments for method Patchlevel\Hydrator\Cryptography\PayloadCryptographer::decrypt - saw 3 (see https://psalm.dev/026)

$event->context['subjectId'] = $currentSubjectId;
}

public function preExtract(PreExtract $event): void
{
$parentSubjectId = $event->context['subjectId'] ?? null;

Check failure on line 35 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/CryptographySubscriber.php:35:9: MixedAssignment: Unable to determine the type that $parentSubjectId is being assigned to (see https://psalm.dev/032)

$currentSubjectId = $this->subjectIdFromObject($event->object, $event->metadata) ?: $parentSubjectId;

Check failure on line 37 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

RiskyTruthyFalsyComparison

src/Cryptography/CryptographySubscriber.php:37:29: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 37 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/CryptographySubscriber.php:37:9: MixedAssignment: Unable to determine the type that $currentSubjectId is being assigned to (see https://psalm.dev/032)

$event->context['subjectId'] = $currentSubjectId;
}

public function postExtract(PostExtract $event): void
{
$event->data = $this->cryptography->encrypt($event->metadata, $event->data);
$event->data = $this->cryptography->encrypt(

Check failure on line 44 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

Method Patchlevel\Hydrator\Cryptography\PayloadCryptographer::encrypt() invoked with 3 parameters, 2 required.

Check failure on line 44 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

TooManyArguments

src/Cryptography/CryptographySubscriber.php:44:45: TooManyArguments: Too many arguments for method Patchlevel\Hydrator\Cryptography\PayloadCryptographer::encrypt - saw 3 (see https://psalm.dev/026)
$event->metadata,
$event->data,
$event->context['subjectId'] ?? null
);
}

/** @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>> */
public static function getSubscribedEvents(): array
{
return [
PreHydrate::class => 'preHydrate',
PreExtract::class => 'preExtract',
PostExtract::class => 'postExtract',
];
}

private function subjectIdFromObject(object $object, ClassMetadata $metadata): string|null
{
$subjectIdField = $metadata->dataSubjectIdField();

if ($subjectIdField === null) {
return null;
}

$property = $metadata->propertyForField($subjectIdField);

$value = $property->getValue($object);

if ($value === null) {
return null;
}

$normalizer = $property->normalizer();

if ($normalizer !== null) {
$value = $normalizer->normalize($value);

Check failure on line 80 in src/Cryptography/CryptographySubscriber.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/CryptographySubscriber.php:80:13: MixedAssignment: Unable to determine the type that $value is being assigned to (see https://psalm.dev/032)
}

if ($value instanceof Stringable) {
$value = (string)$value;
}

if (is_int($value)) {
$value = (string)$value;
}

if (!is_string($value)) {
throw new UnsupportedSubjectId($metadata->className(), $subjectIdField, $value);
}

return $value;
}

/** @param array<string, mixed> $data */
private function subjectIdFromData(array $data, ClassMetadata $metadata): string|null
{
$subjectIdField = $metadata->dataSubjectIdField();

if ($subjectIdField === null) {
return null;
}

$value = $data[$subjectIdField] ?? null;

if ($value === null) {
return null;
}

if ($value instanceof Stringable) {
$value = (string)$value;
}

if (is_int($value)) {
$value = (string)$value;
}

if (!is_string($value)) {
throw new UnsupportedSubjectId($metadata->className(), $subjectIdField, $value);
}

return $value;
}
}
8 changes: 4 additions & 4 deletions src/Cryptography/PersonalDataPayloadCryptographer.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
*
* @return array<string, mixed>
*/
public function encrypt(ClassMetadata $metadata, array $data): array
public function encrypt(ClassMetadata $metadata, array $data, string|null $overrideSubjectId = null): array
{
$subjectId = $this->subjectId($metadata, $data);
$subjectId = $overrideSubjectId ?: $this->subjectId($metadata, $data);

Check failure on line 39 in src/Cryptography/PersonalDataPayloadCryptographer.php

View workflow job for this annotation

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

RiskyTruthyFalsyComparison

src/Cryptography/PersonalDataPayloadCryptographer.php:39:22: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

if ($subjectId === null) {
return $data;
Expand Down Expand Up @@ -78,9 +78,9 @@
*
* @return array<string, mixed>
*/
public function decrypt(ClassMetadata $metadata, array $data): array
public function decrypt(ClassMetadata $metadata, array $data, string|null $overrideSubjectId = null): array
{
$subjectId = $this->subjectId($metadata, $data);
$subjectId = $overrideSubjectId ?: $this->subjectId($metadata, $data);

if ($subjectId === null) {
return $data;
Expand Down
6 changes: 5 additions & 1 deletion src/Event/PostExtract.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@

final class PostExtract
{
/** @param array<string, mixed> $data */
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*/
public function __construct(
public array $data,
public readonly ClassMetadata $metadata,
public array $context = [],
) {
}
}
20 changes: 20 additions & 0 deletions src/Event/PreExtract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Event;

use Patchlevel\Hydrator\Metadata\ClassMetadata;

final class PreExtract
{
/**
* @param array<string, mixed> $context
*/
public function __construct(
public object $object,
public readonly ClassMetadata $metadata,
public array $context = [],
) {
}
}
6 changes: 5 additions & 1 deletion src/Event/PreHydrate.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@

final class PreHydrate
{
/** @param array<string, mixed> $data */
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*/
public function __construct(
public array $data,
public readonly ClassMetadata $metadata,
public array $context = [],
) {
}
}
28 changes: 28 additions & 0 deletions src/HydratorWithContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

interface HydratorWithContext extends Hydrator
{
/**
* @param class-string<T> $class
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @throws ClassNotSupported if the class is not supported or not found.
*
* @template T of object
*/
public function hydrate(string $class, array $data, array $context = []): object;

/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*/
public function extract(object $object, array $context = []): array;
}
66 changes: 47 additions & 19 deletions src/MetadataHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Patchlevel\Hydrator\Cryptography\CryptographySubscriber;
use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Event\PostExtract;
use Patchlevel\Hydrator\Event\PreExtract;
use Patchlevel\Hydrator\Event\PreHydrate;
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Guesser\ChainGuesser;
Expand All @@ -16,21 +17,20 @@
use Patchlevel\Hydrator\Metadata\ClassNotFound;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
use Patchlevel\Hydrator\Normalizer\NormalizerWithContext;
use ReflectionClass;
use ReflectionParameter;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Throwable;
use TypeError;

use function array_key_exists;
use function array_values;
use function is_object;
use function spl_object_id;

use const PHP_VERSION_ID;

final class MetadataHydrator implements Hydrator
final class MetadataHydrator implements HydratorWithContext
{
/** @var array<int, class-string> */
private array $stack = [];
Expand All @@ -55,14 +55,15 @@ public function __construct(
}

/**
* @param class-string<T> $class
* @param class-string<T> $class
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
public function hydrate(string $class, array $data): object
public function hydrate(string $class, array $data, array $context = []): object
{
try {
$metadata = $this->metadataFactory->metadata($class);
Expand All @@ -71,34 +72,38 @@ public function hydrate(string $class, array $data): object
}

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

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

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

return (new ReflectionClass($class))->newLazyProxy(
function () use ($metadata, $data): object {
return $this->doHydrate($metadata, $data);
function () use ($metadata, $data, $context): object {
return $this->doHydrate($metadata, $data, $context);
},
);
}

/**
* @param ClassMetadata<T> $metadata
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
private function doHydrate(ClassMetadata $metadata, array $data): object
private function doHydrate(ClassMetadata $metadata, array $data, array $context = []): object
{
if ($this->eventDispatcher) {
$data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data;
$event = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata, $context));

$data = $event->data;
$context = $event->context;
}

$object = $metadata->newInstance();
Expand Down Expand Up @@ -137,8 +142,13 @@ private function doHydrate(ClassMetadata $metadata, array $data): object
}

try {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->denormalize($value);
if ($normalizer instanceof NormalizerWithContext) {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->denormalize($value, $context);
} else {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->denormalize($value);
}
} catch (Throwable $e) {
throw new DenormalizationFailure(
$metadata->className(),
Expand Down Expand Up @@ -167,8 +177,12 @@ private function doHydrate(ClassMetadata $metadata, array $data): object
return $object;
}

/** @return array<string, mixed> */
public function extract(object $object): array
/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*/
public function extract(object $object, array $context = []): array
{
$objectId = spl_object_id($object);

Expand All @@ -188,6 +202,13 @@ public function extract(object $object): array
$callback->invoke($object);
}

if ($this->eventDispatcher) {
$event = $this->eventDispatcher->dispatch(new PreExtract($object, $metadata, $context));

$object = $event->object;
$context = $event->context;
}

$data = [];

foreach ($metadata->properties() as $propertyMetadata) {
Expand All @@ -202,8 +223,13 @@ public function extract(object $object): array
}

try {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->normalize($value);
if ($normalizer instanceof NormalizerWithContext) {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->normalize($value, $context);
} else {
/** @psalm-suppress MixedAssignment */
$value = $normalizer->normalize($value);
}
} catch (CircularReference $e) {
throw $e;
} catch (Throwable $e) {
Expand All @@ -225,7 +251,9 @@ public function extract(object $object): array
}

if ($this->eventDispatcher) {
return $this->eventDispatcher->dispatch(new PostExtract($data, $metadata))->data;
$event = $this->eventDispatcher->dispatch(new PostExtract($data, $metadata, $context));

$data = $event->data;
}

return $data;
Expand Down
Loading
Loading