From 3f6b860b29cc20a361431c2c36ae5f88e1e6bf49 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 18 Nov 2025 12:22:44 +0100 Subject: [PATCH 1/2] decouple cryptography --- baseline.xml | 10 +- .../CryptographyMetadataFactory.php | 84 +++++++ .../DuplicateSubjectIdIdentifier.php | 3 +- .../MissingDataSubjectId.php | 0 src/Cryptography/SensitiveDataInfo.php | 14 ++ .../SensitiveDataPayloadCryptographer.php | 104 +++++---- .../SubjectIdAndSensitiveDataConflict.php | 3 +- src/Cryptography/SubjectIdFieldMapping.php | 14 ++ src/Metadata/AttributeMetadataFactory.php | 74 +------ src/Metadata/ClassMetadata.php | 28 +-- src/Metadata/PropertyMetadata.php | 55 +---- .../HydratorWithCryptographyBench.php | 7 +- .../CryptographyMetadataFactoryTest.php | 208 ++++++++++++++++++ .../SensitiveDataPayloadCryptographerTest.php | 7 +- .../Metadata/AttributeMetadataFactoryTest.php | 207 ----------------- 15 files changed, 419 insertions(+), 399 deletions(-) create mode 100644 src/Cryptography/CryptographyMetadataFactory.php rename src/{Metadata => Cryptography}/DuplicateSubjectIdIdentifier.php (87%) rename src/{Metadata => Cryptography}/MissingDataSubjectId.php (100%) create mode 100644 src/Cryptography/SensitiveDataInfo.php rename src/{Metadata => Cryptography}/SubjectIdAndSensitiveDataConflict.php (84%) create mode 100644 src/Cryptography/SubjectIdFieldMapping.php create mode 100644 tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php diff --git a/baseline.xml b/baseline.xml index a1fae711..5db3e39e 100644 --- a/baseline.xml +++ b/baseline.xml @@ -34,11 +34,6 @@ - - - sensitiveDataFallbackCallable]]> - - @@ -93,6 +88,11 @@ + + + + + diff --git a/src/Cryptography/CryptographyMetadataFactory.php b/src/Cryptography/CryptographyMetadataFactory.php new file mode 100644 index 00000000..d8839b0a --- /dev/null +++ b/src/Cryptography/CryptographyMetadataFactory.php @@ -0,0 +1,84 @@ +metadataFactory->metadata($class); + + $subjectIdMapping = []; + + foreach ($metadata->properties as $property) { + $isSubjectId = false; + $attributeReflectionList = $property->reflection->getAttributes(DataSubjectId::class); + + if ($attributeReflectionList) { + $subjectIdIdentifier = $attributeReflectionList[0]->newInstance()->name; + + if (array_key_exists($subjectIdIdentifier, $subjectIdMapping)) { + throw new DuplicateSubjectIdIdentifier( + $metadata->className(), + $metadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName(), + $property->propertyName(), + $subjectIdIdentifier, + ); + } + + $subjectIdMapping[$subjectIdIdentifier] = $property->fieldName; + + $isSubjectId = true; + } + + $sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection); + + if (!$sensitiveDataInfo) { + continue; + } + + if ($isSubjectId) { + throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName()); + } + + $property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo; + } + + if ($subjectIdMapping !== []) { + $metadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping); + } + + return $metadata; + } + + private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null + { + $attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class); + + if ($attributeReflectionList === []) { + return null; + } + + $attribute = $attributeReflectionList[0]->newInstance(); + + return new SensitiveDataInfo( + $attribute->subjectIdName, + $attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback, + ); + } +} diff --git a/src/Metadata/DuplicateSubjectIdIdentifier.php b/src/Cryptography/DuplicateSubjectIdIdentifier.php similarity index 87% rename from src/Metadata/DuplicateSubjectIdIdentifier.php rename to src/Cryptography/DuplicateSubjectIdIdentifier.php index 780d9da2..fcc1d247 100644 --- a/src/Metadata/DuplicateSubjectIdIdentifier.php +++ b/src/Cryptography/DuplicateSubjectIdIdentifier.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Patchlevel\Hydrator\Metadata; +namespace Patchlevel\Hydrator\Cryptography; +use Patchlevel\Hydrator\Metadata\MetadataException; use RuntimeException; use function sprintf; diff --git a/src/Metadata/MissingDataSubjectId.php b/src/Cryptography/MissingDataSubjectId.php similarity index 100% rename from src/Metadata/MissingDataSubjectId.php rename to src/Cryptography/MissingDataSubjectId.php diff --git a/src/Cryptography/SensitiveDataInfo.php b/src/Cryptography/SensitiveDataInfo.php new file mode 100644 index 00000000..baddde40 --- /dev/null +++ b/src/Cryptography/SensitiveDataInfo.php @@ -0,0 +1,14 @@ +extras[SubjectIdFieldMapping::class] ?? null; + + if (!$mapping instanceof SubjectIdFieldMapping) { + return $data; + } + + $subjectIds = $this->getSubjectIds($metadata, $mapping, $data); + foreach ($metadata->properties as $propertyMetadata) { - if (!$propertyMetadata->isSensitiveData()) { + $sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null; + + if (!$sensitiveDataInfo instanceof SensitiveDataInfo) { continue; } - $subjectId = $this->subjectId($propertyMetadata, $metadata, $data); + $subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null; + + if ($subjectId === null) { + throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName); + } try { $cipherKey = $this->cipherKeyStore->get($subjectId); @@ -51,7 +67,7 @@ public function encrypt(ClassMetadata $metadata, array $data): array } $targetFieldName = $this->useEncryptedFieldName - ? $propertyMetadata->encryptedFieldName() + ? self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName : $propertyMetadata->fieldName; $data[$targetFieldName] = $this->cipher->encrypt( @@ -76,12 +92,26 @@ public function encrypt(ClassMetadata $metadata, array $data): array */ public function decrypt(ClassMetadata $metadata, array $data): array { + $mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null; + + if (!$mapping instanceof SubjectIdFieldMapping) { + return $data; + } + + $subjectIds = $this->getSubjectIds($metadata, $mapping, $data); + foreach ($metadata->properties as $propertyMetadata) { - if (!$propertyMetadata->isSensitiveData()) { + $sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null; + + if (!$sensitiveDataInfo instanceof SensitiveDataInfo) { continue; } - $subjectId = $this->subjectId($propertyMetadata, $metadata, $data); + $subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null; + + if ($subjectId === null) { + throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName); + } try { $cipherKey = $this->cipherKeyStore->get($subjectId); @@ -89,9 +119,9 @@ public function decrypt(ClassMetadata $metadata, array $data): array $cipherKey = null; } - if ($this->useEncryptedFieldName && array_key_exists($propertyMetadata->encryptedFieldName(), $data)) { - $rawData = $data[$propertyMetadata->encryptedFieldName()]; - unset($data[$propertyMetadata->encryptedFieldName()]); + if ($this->useEncryptedFieldName && array_key_exists(self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName, $data)) { + $rawData = $data[self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName]; + unset($data[self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName]); } elseif (!$this->useEncryptedFieldName || $this->fallbackToFieldName) { $rawData = $data[$propertyMetadata->fieldName]; } else { @@ -99,7 +129,7 @@ public function decrypt(ClassMetadata $metadata, array $data): array } if (!$cipherKey) { - $data[$propertyMetadata->fieldName] = $this->fallback($propertyMetadata, $subjectId, $rawData); + $data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData); continue; } @@ -109,54 +139,50 @@ public function decrypt(ClassMetadata $metadata, array $data): array $rawData, ); } catch (DecryptionFailed) { - $data[$propertyMetadata->fieldName] = $this->fallback($propertyMetadata, $subjectId, $rawData); + $data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData); } } return $data; } - /** @param array $data */ - private function subjectId(PropertyMetadata $propertyMetadata, ClassMetadata $metadata, array $data): string + /** + * @param array $data + * + * @return array + */ + private function getSubjectIds(ClassMetadata $metadata, SubjectIdFieldMapping $mapping, array $data): array { - if (!$propertyMetadata->isSensitiveData()) { - throw new NotSensitiveData($metadata->className(), $propertyMetadata->propertyName()); - } - - $sensitiveDataSubjectIdName = $propertyMetadata->sensitiveDataSubjectIdName; - - if (!$metadata->hasSubjectIdIdentifier($sensitiveDataSubjectIdName)) { - throw new MissingSubjectId($metadata->className(), $propertyMetadata->propertyName()); - } + $result = []; - $fieldName = $metadata->getSubjectIdFieldName($sensitiveDataSubjectIdName); + foreach ($mapping->nameToField as $name => $fieldName) { + if (!array_key_exists($fieldName, $data)) { + throw new MissingSubjectId($metadata->className(), $fieldName); + } - if (!array_key_exists($fieldName, $data)) { - throw new MissingSubjectId($metadata->className(), $fieldName); - } + $subjectId = $data[$fieldName]; - $subjectId = $data[$fieldName]; + if (is_int($subjectId)) { + $subjectId = (string)$subjectId; + } - if (is_int($subjectId)) { - $subjectId = (string)$subjectId; - } + if (!is_string($subjectId)) { + throw new UnsupportedSubjectId($metadata->className(), $fieldName, $subjectId); + } - if (!is_string($subjectId)) { - throw new UnsupportedSubjectId($metadata->className(), $fieldName, $subjectId); + $result[$name] = $subjectId; } - return $subjectId; + return $result; } - private function fallback(PropertyMetadata $propertyMetadata, string $subjectId, mixed $rawData): mixed + private function fallback(SensitiveDataInfo $sensitiveDataInfo, string $subjectId, mixed $rawData): mixed { - $callback = $propertyMetadata->sensitiveDataFallbackCallable(); - - if (!$callback) { - return $propertyMetadata->sensitiveDataFallback; + if ($sensitiveDataInfo->fallback instanceof Closure) { + return ($sensitiveDataInfo->fallback)($subjectId, $rawData); } - return $callback($subjectId, $rawData); + return $sensitiveDataInfo->fallback; } /** @param non-empty-string $method */ diff --git a/src/Metadata/SubjectIdAndSensitiveDataConflict.php b/src/Cryptography/SubjectIdAndSensitiveDataConflict.php similarity index 84% rename from src/Metadata/SubjectIdAndSensitiveDataConflict.php rename to src/Cryptography/SubjectIdAndSensitiveDataConflict.php index ee507167..5d86317d 100644 --- a/src/Metadata/SubjectIdAndSensitiveDataConflict.php +++ b/src/Cryptography/SubjectIdAndSensitiveDataConflict.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Patchlevel\Hydrator\Metadata; +namespace Patchlevel\Hydrator\Cryptography; +use Patchlevel\Hydrator\Metadata\MetadataException; use RuntimeException; use function sprintf; diff --git a/src/Cryptography/SubjectIdFieldMapping.php b/src/Cryptography/SubjectIdFieldMapping.php new file mode 100644 index 00000000..516dba1b --- /dev/null +++ b/src/Cryptography/SubjectIdFieldMapping.php @@ -0,0 +1,14 @@ + $nameToField */ + public function __construct( + public readonly array $nameToField, + ) { + } +} diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index def68bfb..eb4b1e28 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -4,13 +4,11 @@ namespace Patchlevel\Hydrator\Metadata; -use Patchlevel\Hydrator\Attribute\DataSubjectId; use Patchlevel\Hydrator\Attribute\Ignore; use Patchlevel\Hydrator\Attribute\Lazy; use Patchlevel\Hydrator\Attribute\NormalizedName; use Patchlevel\Hydrator\Attribute\PostHydrate; use Patchlevel\Hydrator\Attribute\PreExtract; -use Patchlevel\Hydrator\Attribute\SensitiveData; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; use Patchlevel\Hydrator\Guesser\Guesser; use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; @@ -63,11 +61,7 @@ public function metadata(string $class): ClassMetadata throw new ClassNotFound($class); } - $classMetadata = $this->getClassMetadata($reflectionClass); - - $this->validate($classMetadata); - - return $classMetadata; + return $this->getClassMetadata($reflectionClass); } /** @@ -136,8 +130,6 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra $reflectionProperty, $fieldName, $this->getNormalizer($reflectionProperty), - $this->getSubjectId($reflectionProperty), - ...$this->getSensitiveData($reflectionProperty), ); } @@ -251,74 +243,14 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla $properties[$property->fieldName] = $property; } - $mergedClassMetadata = new ClassMetadata( + return new ClassMetadata( $parent->reflection, array_values($properties), array_merge($parent->postHydrateCallbacks, $child->postHydrateCallbacks), array_merge($parent->preExtractCallbacks, $child->preExtractCallbacks), $child->lazy ?? $parent->lazy, + array_merge($parent->extras, $child->extras), ); - - $this->validate($mergedClassMetadata); - - return $mergedClassMetadata; - } - - private function getSubjectId(ReflectionProperty $reflectionProperty): string|null - { - $attributeReflectionList = $reflectionProperty->getAttributes(DataSubjectId::class); - - if (!$attributeReflectionList) { - return null; - } - - return $attributeReflectionList[0]->newInstance()->name; - } - - /** @return array{string|null, mixed, (callable(string, mixed):mixed)|null} */ - private function getSensitiveData(ReflectionProperty $reflectionProperty): array - { - $attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class); - - if ($attributeReflectionList === []) { - return [null, null, null]; - } - - $attribute = $attributeReflectionList[0]->newInstance(); - - return [$attribute->subjectIdName, $attribute->fallback, $attribute->fallbackCallable]; - } - - private function validate(ClassMetadata $metadata): void - { - $subjectIds = []; - - foreach ($metadata->properties as $property) { - if ($property->isSensitiveData() && $property->isSubjectId()) { - throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName()); - } - - if ($property->isSensitiveData() && !$metadata->hasSubjectIdIdentifier($property->sensitiveDataSubjectIdName)) { - throw new MissingDataSubjectId($metadata->className()); - } - - if (!$property->isSubjectId()) { - continue; - } - - $subjectIdIdentifier = $property->subjectIdName; - - if (array_key_exists($subjectIdIdentifier, $subjectIds)) { - throw new DuplicateSubjectIdIdentifier( - $metadata->className(), - $subjectIds[$subjectIdIdentifier], - $property->propertyName(), - $subjectIdIdentifier, - ); - } - - $subjectIds[$subjectIdIdentifier] = $property->propertyName(); - } } private function getNormalizer(ReflectionProperty $reflectionProperty): Normalizer|null diff --git a/src/Metadata/ClassMetadata.php b/src/Metadata/ClassMetadata.php index e04d2a33..0d8ee334 100644 --- a/src/Metadata/ClassMetadata.php +++ b/src/Metadata/ClassMetadata.php @@ -5,7 +5,6 @@ namespace Patchlevel\Hydrator\Metadata; use ReflectionClass; -use RuntimeException; /** * @psalm-type serialized array{ @@ -14,6 +13,7 @@ * postHydrateCallbacks: list, * preExtractCallbacks: list, * lazy: bool|null, + * extras: array, * } * @template T of object = object */ @@ -24,6 +24,7 @@ final class ClassMetadata * @param list $properties * @param list $postHydrateCallbacks * @param list $preExtractCallbacks + * @param array $extras */ public function __construct( public readonly ReflectionClass $reflection, @@ -31,6 +32,7 @@ public function __construct( public readonly array $postHydrateCallbacks = [], public readonly array $preExtractCallbacks = [], public readonly bool|null $lazy = null, + public array $extras = [], ) { } @@ -51,28 +53,6 @@ public function propertyForField(string $name): PropertyMetadata throw PropertyMetadataNotFound::withName($name); } - public function hasSubjectIdIdentifier(string $subjectIdIdentifier): bool - { - foreach ($this->properties as $property) { - if ($property->subjectIdName === $subjectIdIdentifier) { - return true; - } - } - - return false; - } - - public function getSubjectIdFieldName(string $subjectIdIdentifier): string - { - foreach ($this->properties as $property) { - if ($property->subjectIdName === $subjectIdIdentifier) { - return $property->fieldName; - } - } - - throw new RuntimeException('No subject id'); - } - /** @return T */ public function newInstance(): object { @@ -88,6 +68,7 @@ public function __serialize(): array 'postHydrateCallbacks' => $this->postHydrateCallbacks, 'preExtractCallbacks' => $this->preExtractCallbacks, 'lazy' => $this->lazy, + 'extras' => $this->extras, ]; } @@ -99,5 +80,6 @@ public function __unserialize(array $data): void $this->postHydrateCallbacks = $data['postHydrateCallbacks']; $this->preExtractCallbacks = $data['preExtractCallbacks']; $this->lazy = $data['lazy']; + $this->extras = $data['extras']; } } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index 1b7e5ead..dfd611fd 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -4,41 +4,27 @@ namespace Patchlevel\Hydrator\Metadata; -use Closure; -use InvalidArgumentException; use Patchlevel\Hydrator\Normalizer\Normalizer; use ReflectionProperty; -use function str_starts_with; - /** * @psalm-type serialized = array{ * className: class-string, * property: string, * fieldName: string, * normalizer: Normalizer|null, - * subjectIdName: string|null, - * sensitiveDataSubjectIdName: string|null, - * sensitiveDataFallback: mixed + * extras: array, * } */ final class PropertyMetadata { - private const ENCRYPTED_PREFIX = '!'; - - /** @param (callable(string, mixed):mixed)|null $sensitiveDataFallbackCallable */ + /** @param array $extras */ public function __construct( public readonly ReflectionProperty $reflection, public readonly string $fieldName, public readonly Normalizer|null $normalizer = null, - public readonly string|null $subjectIdName = null, - public readonly string|null $sensitiveDataSubjectIdName = null, - public readonly mixed $sensitiveDataFallback = null, - public readonly mixed $sensitiveDataFallbackCallable = null, + public array $extras = [], ) { - if (str_starts_with($fieldName, self::ENCRYPTED_PREFIX)) { - throw new InvalidArgumentException('fieldName must not start with !'); - } } public function propertyName(): string @@ -46,11 +32,6 @@ public function propertyName(): string return $this->reflection->getName(); } - public function encryptedFieldName(): string - { - return self::ENCRYPTED_PREFIX . $this->fieldName; - } - public function setValue(object $object, mixed $value): void { $this->reflection->setValue($object, $value); @@ -61,28 +42,6 @@ public function getValue(object $object): mixed return $this->reflection->getValue($object); } - /** @phpstan-assert-if-true !null $this->sensitiveDataSubjectIdName */ - public function isSensitiveData(): bool - { - return $this->sensitiveDataSubjectIdName !== null; - } - - /** @phpstan-assert-if-true !null $this->subjectIdName */ - public function isSubjectId(): bool - { - return $this->subjectIdName !== null; - } - - /** @return (Closure(string, mixed):mixed)|null */ - public function sensitiveDataFallbackCallable(): Closure|null - { - if ($this->sensitiveDataFallbackCallable) { - return ($this->sensitiveDataFallbackCallable)(...); - } - - return null; - } - /** @return serialized */ public function __serialize(): array { @@ -91,9 +50,7 @@ public function __serialize(): array 'property' => $this->reflection->getName(), 'fieldName' => $this->fieldName, 'normalizer' => $this->normalizer, - 'subjectIdName' => $this->subjectIdName, - 'sensitiveDataSubjectIdName' => $this->sensitiveDataSubjectIdName, - 'sensitiveDataFallback' => $this->sensitiveDataFallback, + 'extras' => $this->extras, ]; } @@ -103,8 +60,6 @@ public function __unserialize(array $data): void $this->reflection = new ReflectionProperty($data['className'], $data['property']); $this->fieldName = $data['fieldName']; $this->normalizer = $data['normalizer']; - $this->subjectIdName = $data['subjectIdName']; - $this->sensitiveDataSubjectIdName = $data['sensitiveDataSubjectIdName']; - $this->sensitiveDataFallback = $data['sensitiveDataFallback']; + $this->extras = $data['extras']; } } diff --git a/tests/Benchmark/HydratorWithCryptographyBench.php b/tests/Benchmark/HydratorWithCryptographyBench.php index c090f33b..446ec49d 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/HydratorWithCryptographyBench.php @@ -4,10 +4,12 @@ namespace Patchlevel\Hydrator\Tests\Benchmark; +use Patchlevel\Hydrator\Cryptography\CryptographyMetadataFactory; use Patchlevel\Hydrator\Cryptography\CryptographySubscriber; use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer; use Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore; use Patchlevel\Hydrator\Hydrator; +use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\MetadataHydrator; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileId; @@ -31,7 +33,10 @@ public function __construct() SensitiveDataPayloadCryptographer::createWithDefaultSettings($this->store), )); - $this->hydrator = MetadataHydrator::create(eventDispatcher: $eventDispatcher); + $this->hydrator = new MetadataHydrator( + metadataFactory: new CryptographyMetadataFactory(new AttributeMetadataFactory()), + eventDispatcher: $eventDispatcher, + ); } public function setUp(): void diff --git a/tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php b/tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php new file mode 100644 index 00000000..7a07c309 --- /dev/null +++ b/tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php @@ -0,0 +1,208 @@ +metadata($event::class); + + self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); + $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; + self::assertInstanceOf(SubjectIdFieldMapping::class, $subjectIdFieldMapping); + self::assertEquals(['default' => '_id'], $subjectIdFieldMapping->nameToField); + + $property = $metadata->propertyForField('_name'); + + self::assertArrayHasKey(SensitiveDataInfo::class, $property->extras); + $sensitiveDataInfo = $property->extras[SensitiveDataInfo::class]; + self::assertInstanceOf(SensitiveDataInfo::class, $sensitiveDataInfo); + + self::assertSame('default', $sensitiveDataInfo->subjectIdName); + self::assertSame('fallback', $sensitiveDataInfo->fallback); + } + + public function testSubjectIdAndSensitiveDataConflict(): void + { + $event = new class ('name') { + public function __construct( + #[DataSubjectId] + #[SensitiveData] + public string $name, + ) { + } + }; + + $this->expectException(SubjectIdAndSensitiveDataConflict::class); + + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + $metadataFactory->metadata($event::class); + } + + public function testMultipleDataSubjectIdWithSameIdentifier(): void + { + $event = new class ('id', 'name') { + public function __construct( + #[DataSubjectId] + public string $id, + #[DataSubjectId] + public string $name, + ) { + } + }; + + $this->expectException(DuplicateSubjectIdIdentifier::class); + + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + $metadataFactory->metadata($event::class); + } + + public function testSensitiveDataWithMultipleDataSubjectIdWithDifferentNames(): void + { + $event = new class ('fooId', 'fooName', 'barId', 'barName') { + public function __construct( + #[DataSubjectId(name: 'foo')] + #[NormalizedName('_fooId')] + public string $fooId, + #[SensitiveData('fallback', subjectIdName: 'foo')] + #[NormalizedName('_fooName')] + public string $fooName, + #[DataSubjectId(name: 'bar')] + #[NormalizedName('_barId')] + public string $barId, + #[SensitiveData('fallback', subjectIdName: 'bar')] + #[NormalizedName('_barName')] + public string $barName, + ) { + } + }; + + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + $metadata = $metadataFactory->metadata($event::class); + + self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); + $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; + self::assertInstanceOf(SubjectIdFieldMapping::class, $subjectIdFieldMapping); + self::assertEquals(['foo' => '_fooId', 'bar' => '_barId'], $subjectIdFieldMapping->nameToField); + + $property = $metadata->propertyForField('_fooName'); + + self::assertArrayHasKey(SensitiveDataInfo::class, $property->extras); + $sensitiveDataInfo = $property->extras[SensitiveDataInfo::class]; + self::assertInstanceOf(SensitiveDataInfo::class, $sensitiveDataInfo); + + self::assertSame('foo', $sensitiveDataInfo->subjectIdName); + self::assertSame('fallback', $sensitiveDataInfo->fallback); + + $property = $metadata->propertyForField('_barName'); + + self::assertArrayHasKey(SensitiveDataInfo::class, $property->extras); + $sensitiveDataInfo = $property->extras[SensitiveDataInfo::class]; + self::assertInstanceOf(SensitiveDataInfo::class, $sensitiveDataInfo); + + self::assertSame('bar', $sensitiveDataInfo->subjectIdName); + self::assertSame('fallback', $sensitiveDataInfo->fallback); + } + + public function testDuplicateSubjectIdIdentifiers(): void + { + $event = new class ('fooId', 'fooName', 'barId', 'barName') { + public function __construct( + #[DataSubjectId(name: 'foo')] + #[NormalizedName('_fooId')] + public string $fooId, + #[SensitiveData('fallback', subjectIdName: 'foo')] + #[NormalizedName('_fooName')] + public string $fooName, + #[DataSubjectId(name: 'foo')] + #[NormalizedName('_barId')] + public string $barId, + #[SensitiveData('fallback', subjectIdName: 'foo')] + #[NormalizedName('_barName')] + public string $barName, + ) { + } + }; + + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + + $this->expectException(DuplicateSubjectIdIdentifier::class); + $this->expectExceptionMessageMatches('/Duplicate subject id identifier found\. Used foo for .*::fooId and .*::barId\./'); + $metadataFactory->metadata($event::class); + } + + public function testExtendsWithSensitiveData(): void + { + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + $metadata = $metadataFactory->metadata(ParentWithSensitiveDataDto::class); + + self::assertCount(2, $metadata->properties); + + self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); + $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; + self::assertInstanceOf(SubjectIdFieldMapping::class, $subjectIdFieldMapping); + self::assertEquals(['default' => 'profileId'], $subjectIdFieldMapping->nameToField); + + $property = $metadata->propertyForField('email'); + + self::assertArrayHasKey(SensitiveDataInfo::class, $property->extras); + $sensitiveDataInfo = $property->extras[SensitiveDataInfo::class]; + self::assertInstanceOf(SensitiveDataInfo::class, $sensitiveDataInfo); + + self::assertSame('default', $sensitiveDataInfo->subjectIdName); + self::assertSame(null, $sensitiveDataInfo->fallback); + } + + public function testExtendsWithSensitiveDataWithName(): void + { + $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); + $metadata = $metadataFactory->metadata(ParentWithSensitiveDataWithIdentifierDto::class); + + self::assertCount(2, $metadata->properties); + + self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); + $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; + self::assertInstanceOf(SubjectIdFieldMapping::class, $subjectIdFieldMapping); + self::assertEquals(['profile' => 'profileId'], $subjectIdFieldMapping->nameToField); + + $property = $metadata->propertyForField('email'); + + self::assertArrayHasKey(SensitiveDataInfo::class, $property->extras); + $sensitiveDataInfo = $property->extras[SensitiveDataInfo::class]; + self::assertInstanceOf(SensitiveDataInfo::class, $sensitiveDataInfo); + + self::assertSame('profile', $sensitiveDataInfo->subjectIdName); + self::assertSame(null, $sensitiveDataInfo->fallback); + } +} diff --git a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php index 331d2692..b3d78918 100644 --- a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php +++ b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php @@ -9,6 +9,7 @@ use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey; use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory; use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed; +use Patchlevel\Hydrator\Cryptography\CryptographyMetadataFactory; use Patchlevel\Hydrator\Cryptography\MissingSubjectId; use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer; use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists; @@ -422,6 +423,10 @@ public function testCreateWithOpenssl(): void /** @param class-string $class */ private function metadata(string $class): ClassMetadata { - return (new AttributeMetadataFactory())->metadata($class); + $factory = new CryptographyMetadataFactory( + new AttributeMetadataFactory(), + ); + + return $factory->metadata($class); } } diff --git a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php index 74f02a93..d247f082 100644 --- a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php +++ b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php @@ -4,19 +4,14 @@ namespace Patchlevel\Hydrator\Tests\Unit\Metadata; -use Patchlevel\Hydrator\Attribute\DataSubjectId; use Patchlevel\Hydrator\Attribute\Lazy; use Patchlevel\Hydrator\Attribute\NormalizedName; use Patchlevel\Hydrator\Attribute\PostHydrate; use Patchlevel\Hydrator\Attribute\PreExtract; -use Patchlevel\Hydrator\Attribute\SensitiveData; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\ClassNotFound; use Patchlevel\Hydrator\Metadata\DuplicatedFieldNameInMetadata; -use Patchlevel\Hydrator\Metadata\DuplicateSubjectIdIdentifier; -use Patchlevel\Hydrator\Metadata\MissingDataSubjectId; use Patchlevel\Hydrator\Metadata\PropertyMetadataNotFound; -use Patchlevel\Hydrator\Metadata\SubjectIdAndSensitiveDataConflict; use Patchlevel\Hydrator\Normalizer\EnumNormalizer; use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\BrokenParentDto; @@ -27,10 +22,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\IdNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\IgnoreDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\IgnoreParentDto; -use Patchlevel\Hydrator\Tests\Unit\Fixture\MissingSubjectIdDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto; -use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithSensitiveDataDto; -use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithSensitiveDataWithIdentifierDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithGeneric; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; @@ -347,205 +339,6 @@ public function testIgnoreNotFoundProperty(): void $metadata->propertyForField('email'); } - public function testSensitiveData(): void - { - $event = new class ('id', 'name') { - public function __construct( - #[DataSubjectId] - #[NormalizedName('_id')] - public string $id, - #[SensitiveData('fallback')] - #[NormalizedName('_name')] - public string $name, - ) { - } - }; - - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata($event::class); - - self::assertCount(2, $metadata->properties); - - self::assertSame(false, $metadata->propertyForField('_id')->isSensitiveData()); - self::assertSame(true, $metadata->propertyForField('_id')->isSubjectId()); - self::assertSame('default', $metadata->propertyForField('_id')->subjectIdName); - self::assertSame(null, $metadata->propertyForField('_id')->sensitiveDataFallback); - self::assertSame(null, $metadata->propertyForField('_id')->sensitiveDataSubjectIdName); - - self::assertSame(true, $metadata->propertyForField('_name')->isSensitiveData()); - self::assertSame(false, $metadata->propertyForField('_name')->isSubjectId()); - self::assertSame('fallback', $metadata->propertyForField('_name')->sensitiveDataFallback); - self::assertSame('default', $metadata->propertyForField('_name')->sensitiveDataSubjectIdName); - } - - public function testMissingDataSubjectId(): void - { - $this->expectException(MissingDataSubjectId::class); - - $metadataFactory = new AttributeMetadataFactory(); - $metadataFactory->metadata(MissingSubjectIdDto::class); - } - - public function testSubjectIdAndSensitiveDataConflict(): void - { - $event = new class ('name') { - public function __construct( - #[DataSubjectId] - #[SensitiveData] - public string $name, - ) { - } - }; - - $this->expectException(SubjectIdAndSensitiveDataConflict::class); - - $metadataFactory = new AttributeMetadataFactory(); - $metadataFactory->metadata($event::class); - } - - public function testMultipleDataSubjectIdWithSameIdentifier(): void - { - $event = new class ('id', 'name') { - public function __construct( - #[DataSubjectId] - public string $id, - #[DataSubjectId] - public string $name, - ) { - } - }; - - $this->expectException(DuplicateSubjectIdIdentifier::class); - - $metadataFactory = new AttributeMetadataFactory(); - $metadataFactory->metadata($event::class); - } - - public function testSensitiveDataWithMultipleDataSubjectIdWithDifferentNames(): void - { - $event = new class ('fooId', 'fooName', 'barId', 'barName') { - public function __construct( - #[DataSubjectId(name: 'foo')] - #[NormalizedName('_fooId')] - public string $fooId, - #[SensitiveData('fallback', subjectIdName: 'foo')] - #[NormalizedName('_fooName')] - public string $fooName, - #[DataSubjectId(name: 'bar')] - #[NormalizedName('_barId')] - public string $barId, - #[SensitiveData('fallback', subjectIdName: 'bar')] - #[NormalizedName('_barName')] - public string $barName, - ) { - } - }; - - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata($event::class); - - self::assertCount(4, $metadata->properties); - - $fooIdProperty = $metadata->propertyForField('_fooId'); - self::assertFalse($fooIdProperty->isSensitiveData()); - self::assertSame(null, $fooIdProperty->sensitiveDataFallback); - self::assertTrue($fooIdProperty->isSubjectId()); - self::assertSame('foo', $fooIdProperty->subjectIdName); - - $fooNameProperty = $metadata->propertyForField('_fooName'); - self::assertSame(true, $fooNameProperty->isSensitiveData()); - self::assertSame('fallback', $fooNameProperty->sensitiveDataFallback); - self::assertSame('foo', $fooNameProperty->sensitiveDataSubjectIdName); - - $barIdProperty = $metadata->propertyForField('_barId'); - self::assertFalse($barIdProperty->isSensitiveData()); - self::assertSame(null, $barIdProperty->sensitiveDataFallback); - self::assertTrue($barIdProperty->isSubjectId()); - self::assertSame('bar', $barIdProperty->subjectIdName); - - $barNameProperty = $metadata->propertyForField('_barName'); - self::assertSame(true, $barNameProperty->isSensitiveData()); - self::assertSame('fallback', $barNameProperty->sensitiveDataFallback); - self::assertSame('bar', $barNameProperty->sensitiveDataSubjectIdName); - } - - public function testDuplicateSubjectIdIdentifiers(): void - { - $event = new class ('fooId', 'fooName', 'barId', 'barName') { - public function __construct( - #[DataSubjectId(name: 'foo')] - #[NormalizedName('_fooId')] - public string $fooId, - #[SensitiveData('fallback', subjectIdName: 'foo')] - #[NormalizedName('_fooName')] - public string $fooName, - #[DataSubjectId(name: 'foo')] - #[NormalizedName('_barId')] - public string $barId, - #[SensitiveData('fallback', subjectIdName: 'foo')] - #[NormalizedName('_barName')] - public string $barName, - ) { - } - }; - - $metadataFactory = new AttributeMetadataFactory(); - - $this->expectException(DuplicateSubjectIdIdentifier::class); - $this->expectExceptionMessageMatches('/Duplicate subject id identifier found\. Used foo for .*::fooId and .*::barId\./'); - $metadataFactory->metadata($event::class); - } - - public function testExtendsWithSensitiveData(): void - { - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata(ParentWithSensitiveDataDto::class); - - self::assertCount(2, $metadata->properties); - - $idPropertyMetadata = $metadata->propertyForField('profileId'); - - self::assertSame('profileId', $idPropertyMetadata->propertyName()); - self::assertSame('profileId', $idPropertyMetadata->fieldName); - self::assertTrue($idPropertyMetadata->isSubjectId()); - self::assertFalse($idPropertyMetadata->isSensitiveData()); - self::assertInstanceOf(IdNormalizer::class, $idPropertyMetadata->normalizer); - - $emailPropertyMetadata = $metadata->propertyForField('email'); - - self::assertSame('email', $emailPropertyMetadata->propertyName()); - self::assertSame('email', $emailPropertyMetadata->fieldName); - self::assertFalse($emailPropertyMetadata->isSubjectId()); - self::assertTrue($emailPropertyMetadata->isSensitiveData()); - self::assertInstanceOf(EmailNormalizer::class, $emailPropertyMetadata->normalizer); - } - - public function testExtendsWithSensitiveDataWithName(): void - { - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata(ParentWithSensitiveDataWithIdentifierDto::class); - - self::assertCount(2, $metadata->properties); - - $idPropertyMetadata = $metadata->propertyForField('profileId'); - - self::assertSame('profileId', $idPropertyMetadata->propertyName()); - self::assertSame('profileId', $idPropertyMetadata->fieldName); - self::assertTrue($idPropertyMetadata->isSubjectId()); - self::assertFalse($idPropertyMetadata->isSensitiveData()); - self::assertInstanceOf(IdNormalizer::class, $idPropertyMetadata->normalizer); - - $emailPropertyMetadata = $metadata->propertyForField('email'); - - self::assertSame('email', $emailPropertyMetadata->propertyName()); - self::assertSame('email', $emailPropertyMetadata->fieldName); - self::assertFalse($emailPropertyMetadata->isSubjectId()); - self::assertTrue($emailPropertyMetadata->isSensitiveData()); - self::assertNull($emailPropertyMetadata->sensitiveDataFallback); - self::assertSame('profile', $emailPropertyMetadata->sensitiveDataSubjectIdName); - self::assertInstanceOf(EmailNormalizer::class, $emailPropertyMetadata->normalizer); - } - public function testHooks(): void { $object = new class { From 6960275566a2ec792b7492385fd68256e965c62f Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 19 Nov 2025 16:23:02 +0100 Subject: [PATCH 2/2] remove unused exceptions --- src/Cryptography/MissingDataSubjectId.php | 20 -------------------- src/Cryptography/NotSensitiveData.php | 18 ------------------ 2 files changed, 38 deletions(-) delete mode 100644 src/Cryptography/MissingDataSubjectId.php delete mode 100644 src/Cryptography/NotSensitiveData.php diff --git a/src/Cryptography/MissingDataSubjectId.php b/src/Cryptography/MissingDataSubjectId.php deleted file mode 100644 index 69be40cf..00000000 --- a/src/Cryptography/MissingDataSubjectId.php +++ /dev/null @@ -1,20 +0,0 @@ -