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/Cryptography/NotSensitiveData.php b/src/Cryptography/NotSensitiveData.php
deleted file mode 100644
index d4157625..00000000
--- a/src/Cryptography/NotSensitiveData.php
+++ /dev/null
@@ -1,18 +0,0 @@
-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/MissingDataSubjectId.php b/src/Metadata/MissingDataSubjectId.php
deleted file mode 100644
index 69be40cf..00000000
--- a/src/Metadata/MissingDataSubjectId.php
+++ /dev/null
@@ -1,20 +0,0 @@
-,
* }
*/
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 {