Skip to content

Commit 5c53fa3

Browse files
committed
decouple cryptography
1 parent 73307cc commit 5c53fa3

10 files changed

Lines changed: 167 additions & 151 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Cryptography;
4+
5+
use Patchlevel\Hydrator\Attribute\DataSubjectId;
6+
use Patchlevel\Hydrator\Attribute\SensitiveData;
7+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
8+
use Patchlevel\Hydrator\Metadata\MetadataFactory;
9+
use ReflectionProperty;
10+
11+
final class CryptographyMetadataFactory implements MetadataFactory
12+
{
13+
public function __construct(
14+
private readonly MetadataFactory $metadataFactory
15+
) {
16+
}
17+
18+
public function metadata(string $class): ClassMetadata
19+
{
20+
$metadata = $this->metadataFactory->metadata($class);
21+
22+
$subjectIdMapping = [];
23+
24+
foreach ($metadata->properties as $property) {
25+
$isSubjectId = false;
26+
$attributeReflectionList = $property->reflection->getAttributes(DataSubjectId::class);
27+
28+
if ($attributeReflectionList) {
29+
$subjectIdIdentifier = $attributeReflectionList[0]->newInstance()->name;
30+
31+
if (array_key_exists($subjectIdIdentifier, $subjectIdMapping)) {
32+
throw new DuplicateSubjectIdIdentifier(
33+
$metadata->className(),
34+
$subjectIdMapping[$subjectIdIdentifier],
35+
$property->propertyName(),
36+
$subjectIdIdentifier,
37+
);
38+
}
39+
40+
$subjectIdMapping[$subjectIdIdentifier] = $property->fieldName;
41+
42+
$isSubjectId = true;
43+
}
44+
45+
$sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection);
46+
47+
if (!$sensitiveDataInfo) {
48+
continue;
49+
}
50+
51+
if ($isSubjectId) {
52+
throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName());
53+
}
54+
55+
$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
56+
}
57+
58+
if ($subjectIdMapping !== []) {
59+
$metadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping);
60+
}
61+
62+
return $metadata;
63+
}
64+
65+
private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null
66+
{
67+
$attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class);
68+
69+
if ($attributeReflectionList === []) {
70+
return null;
71+
}
72+
73+
$attribute = $attributeReflectionList[0]->newInstance();
74+
75+
return new SensitiveDataInfo(
76+
$attribute->subjectIdName,
77+
$attribute->fallback,
78+
$attribute->fallbackCallable
79+
);
80+
}
81+
}

src/Metadata/DuplicateSubjectIdIdentifier.php renamed to src/Cryptography/DuplicateSubjectIdIdentifier.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace Patchlevel\Hydrator\Metadata;
5+
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Patchlevel\Hydrator\Metadata\MetadataException;
78
use RuntimeException;
89

910
use function sprintf;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
final class SensitiveDataInfo
8+
{
9+
public function __construct(
10+
public readonly string|null $subjectIdName = null,
11+
public readonly mixed $fallback = null,
12+
public readonly mixed $fallbackCallable = null,
13+
) {
14+
}
15+
}

src/Cryptography/SensitiveDataPayloadCryptographer.php

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,21 @@ public function __construct(
3636
*/
3737
public function encrypt(ClassMetadata $metadata, array $data): array
3838
{
39+
$mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null;
40+
41+
if (!$mapping) {
42+
return $data;
43+
}
44+
45+
$subjectIds = $this->getSubjectIds($metadata, $mapping);
46+
3947
foreach ($metadata->properties as $propertyMetadata) {
40-
if (!$propertyMetadata->isSensitiveData()) {
48+
$sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;
49+
50+
if (!$sensitiveDataInfo) {
4151
continue;
4252
}
4353

44-
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
45-
4654
try {
4755
$cipherKey = $this->cipherKeyStore->get($subjectId);
4856
} catch (CipherKeyNotExists) {
@@ -116,11 +124,17 @@ public function decrypt(ClassMetadata $metadata, array $data): array
116124
return $data;
117125
}
118126

127+
/** @return list<string> */
128+
private function getSubjectIds(ClassMetadata $metadata, SubjectIdFieldMapping $mapping): array
129+
{
130+
131+
}
132+
119133
/** @param array<string, mixed> $data */
120134
private function subjectId(PropertyMetadata $propertyMetadata, ClassMetadata $metadata, array $data): string
121135
{
122-
if (!$propertyMetadata->isSensitiveData()) {
123-
throw new NotSensitiveData($metadata->className(), $propertyMetadata->propertyName());
136+
if (!array_key_exists($sensitiveDataInfo->fieldName, $mapping->subjectIdMapping)) {
137+
throw new MissingSubjectId($metadata->className(), $propertyMetadata->propertyName());
124138
}
125139

126140
$sensitiveDataSubjectIdName = $propertyMetadata->sensitiveDataSubjectIdName;
@@ -187,4 +201,27 @@ public static function createWithDefaultSettings(
187201
true,
188202
);
189203
}
204+
205+
206+
/** @phpstan-assert-if-true !null $this->sensitiveDataSubjectIdName */
207+
public function isSensitiveData(): bool
208+
{
209+
return $this->sensitiveDataSubjectIdName !== null;
210+
}
211+
212+
/** @phpstan-assert-if-true !null $this->subjectIdName */
213+
public function isSubjectId(): bool
214+
{
215+
return $this->subjectIdName !== null;
216+
}
217+
218+
/** @return (Closure(string, mixed):mixed)|null */
219+
public function sensitiveDataFallbackCallable(): Closure|null
220+
{
221+
if ($this->sensitiveDataFallbackCallable) {
222+
return ($this->sensitiveDataFallbackCallable)(...);
223+
}
224+
225+
return null;
226+
}
190227
}

src/Metadata/SubjectIdAndSensitiveDataConflict.php renamed to src/Cryptography/SubjectIdAndSensitiveDataConflict.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace Patchlevel\Hydrator\Metadata;
5+
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Patchlevel\Hydrator\Metadata\MetadataException;
78
use RuntimeException;
89

910
use function sprintf;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Cryptography;
4+
5+
final class SubjectIdFieldMapping
6+
{
7+
/** @param array<string, string> $nameToField */
8+
public function __construct(
9+
public readonly array $nameToField,
10+
) {
11+
}
12+
}

src/Metadata/AttributeMetadataFactory.php

Lines changed: 3 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
namespace Patchlevel\Hydrator\Metadata;
66

7-
use Patchlevel\Hydrator\Attribute\DataSubjectId;
87
use Patchlevel\Hydrator\Attribute\Ignore;
98
use Patchlevel\Hydrator\Attribute\Lazy;
109
use Patchlevel\Hydrator\Attribute\NormalizedName;
1110
use Patchlevel\Hydrator\Attribute\PostHydrate;
1211
use Patchlevel\Hydrator\Attribute\PreExtract;
13-
use Patchlevel\Hydrator\Attribute\SensitiveData;
1412
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
1513
use Patchlevel\Hydrator\Guesser\Guesser;
1614
use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
@@ -73,11 +71,7 @@ public function metadata(string $class): ClassMetadata
7371
throw new ClassNotFound($class);
7472
}
7573

76-
$classMetadata = $this->getClassMetadata($reflectionClass);
77-
78-
$this->validate($classMetadata);
79-
80-
return $classMetadata;
74+
return $this->getClassMetadata($reflectionClass);
8175
}
8276

8377
/**
@@ -157,8 +151,6 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra
157151
$reflectionProperty,
158152
$fieldName,
159153
$this->getNormalizer($reflectionProperty),
160-
$this->getSubjectId($reflectionProperty),
161-
...$this->getSensitiveData($reflectionProperty),
162154
);
163155
}
164156

@@ -272,74 +264,14 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
272264
$properties[$property->fieldName] = $property;
273265
}
274266

275-
$mergedClassMetadata = new ClassMetadata(
267+
return new ClassMetadata(
276268
$parent->reflection,
277269
array_values($properties),
278270
array_merge($parent->postHydrateCallbacks, $child->postHydrateCallbacks),
279271
array_merge($parent->preExtractCallbacks, $child->preExtractCallbacks),
280272
$child->lazy ?? $parent->lazy,
273+
array_merge($parent->extras, $child->extras),
281274
);
282-
283-
$this->validate($mergedClassMetadata);
284-
285-
return $mergedClassMetadata;
286-
}
287-
288-
private function getSubjectId(ReflectionProperty $reflectionProperty): string|null
289-
{
290-
$attributeReflectionList = $reflectionProperty->getAttributes(DataSubjectId::class);
291-
292-
if (!$attributeReflectionList) {
293-
return null;
294-
}
295-
296-
return $attributeReflectionList[0]->newInstance()->name;
297-
}
298-
299-
/** @return array{string|null, mixed, (callable(string, mixed):mixed)|null} */
300-
private function getSensitiveData(ReflectionProperty $reflectionProperty): array
301-
{
302-
$attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class);
303-
304-
if ($attributeReflectionList === []) {
305-
return [null, null, null];
306-
}
307-
308-
$attribute = $attributeReflectionList[0]->newInstance();
309-
310-
return [$attribute->subjectIdName, $attribute->fallback, $attribute->fallbackCallable];
311-
}
312-
313-
private function validate(ClassMetadata $metadata): void
314-
{
315-
$subjectIds = [];
316-
317-
foreach ($metadata->properties as $property) {
318-
if ($property->isSensitiveData() && $property->isSubjectId()) {
319-
throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName());
320-
}
321-
322-
if ($property->isSensitiveData() && !$metadata->hasSubjectIdIdentifier($property->sensitiveDataSubjectIdName)) {
323-
throw new MissingDataSubjectId($metadata->className());
324-
}
325-
326-
if (!$property->isSubjectId()) {
327-
continue;
328-
}
329-
330-
$subjectIdIdentifier = $property->subjectIdName;
331-
332-
if (array_key_exists($subjectIdIdentifier, $subjectIds)) {
333-
throw new DuplicateSubjectIdIdentifier(
334-
$metadata->className(),
335-
$subjectIds[$subjectIdIdentifier],
336-
$property->propertyName(),
337-
$subjectIdIdentifier,
338-
);
339-
}
340-
341-
$subjectIds[$subjectIdIdentifier] = $property->propertyName();
342-
}
343275
}
344276

345277
private function getNormalizer(ReflectionProperty $reflectionProperty): Normalizer|null

src/Metadata/ClassMetadata.php

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Patchlevel\Hydrator\Metadata;
66

77
use ReflectionClass;
8-
use RuntimeException;
98

109
/**
1110
* @psalm-type serialized array{
@@ -14,6 +13,7 @@
1413
* postHydrateCallbacks: list<CallbackMetadata>,
1514
* preExtractCallbacks: list<CallbackMetadata>,
1615
* lazy: bool|null,
16+
* extras: array<string, mixed>,
1717
* }
1818
* @template T of object = object
1919
*/
@@ -24,13 +24,15 @@ final class ClassMetadata
2424
* @param list<PropertyMetadata> $properties
2525
* @param list<CallbackMetadata> $postHydrateCallbacks
2626
* @param list<CallbackMetadata> $preExtractCallbacks
27+
* @param array<string, mixed> $extras
2728
*/
2829
public function __construct(
2930
public readonly ReflectionClass $reflection,
3031
public readonly array $properties = [],
3132
public readonly array $postHydrateCallbacks = [],
3233
public readonly array $preExtractCallbacks = [],
3334
public readonly bool|null $lazy = null,
35+
public array $extras = [],
3436
) {
3537
}
3638

@@ -51,28 +53,6 @@ public function propertyForField(string $name): PropertyMetadata
5153
throw PropertyMetadataNotFound::withName($name);
5254
}
5355

54-
public function hasSubjectIdIdentifier(string $subjectIdIdentifier): bool
55-
{
56-
foreach ($this->properties as $property) {
57-
if ($property->subjectIdName === $subjectIdIdentifier) {
58-
return true;
59-
}
60-
}
61-
62-
return false;
63-
}
64-
65-
public function getSubjectIdFieldName(string $subjectIdIdentifier): string
66-
{
67-
foreach ($this->properties as $property) {
68-
if ($property->subjectIdName === $subjectIdIdentifier) {
69-
return $property->fieldName;
70-
}
71-
}
72-
73-
throw new RuntimeException('No subject id');
74-
}
75-
7656
/** @return T */
7757
public function newInstance(): object
7858
{
@@ -88,6 +68,7 @@ public function __serialize(): array
8868
'postHydrateCallbacks' => $this->postHydrateCallbacks,
8969
'preExtractCallbacks' => $this->preExtractCallbacks,
9070
'lazy' => $this->lazy,
71+
'extras' => $this->extras,
9172
];
9273
}
9374

@@ -99,5 +80,6 @@ public function __unserialize(array $data): void
9980
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
10081
$this->preExtractCallbacks = $data['preExtractCallbacks'];
10182
$this->lazy = $data['lazy'];
83+
$this->extras = $data['extras'];
10284
}
10385
}

0 commit comments

Comments
 (0)