Skip to content

Commit 9de60ae

Browse files
authored
Merge pull request #107 from patchlevel/add-multiple-subject-id-identifiers
2 parents d9846e8 + 930329b commit 9de60ae

24 files changed

Lines changed: 446 additions & 258 deletions

README.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ There are two events: `PostExtract` and `PreHydrate`.
528528
For this functionality we use the [symfony/event-dispatcher](https://symfony.com/doc/current/components/event_dispatcher.html).
529529

530530
```php
531-
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
531+
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;
532532
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
533533
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
534534
use Patchlevel\Hydrator\MetadataHydrator;
@@ -557,8 +557,8 @@ $hydrator = new MetadataHydrator(eventDispatcher: $eventDispatcher);
557557

558558
### Cryptography
559559

560-
The library also offers the possibility to encrypt and decrypt personal data.
561-
For this purpose, a key is created for each subject ID, which is used to encrypt the personal data.
560+
The library also offers the possibility to encrypt and decrypt sensitive data, e.g. personal data of customers.
561+
For this purpose, a key is created for each subject ID, which is used to encrypt the sensitive data.
562562

563563
#### DataSubjectId
564564

@@ -579,42 +579,62 @@ final class EmailChanged
579579

580580
> [!WARNING]
581581
> The `DataSubjectId` must be a string. You can use a normalizer to convert it to a string.
582-
> The Subject ID cannot be personal data.
582+
> The Subject ID cannot be sensitive data.
583583
584-
#### PersonalData
584+
First we need to define what the subject id is.
585585

586-
Next, we need to specify which fields we want to encrypt.
586+
```php
587+
use Patchlevel\Hydrator\Attribute\DataSubjectId;
588+
589+
final class EmailChanged
590+
{
591+
public function __construct(
592+
#[DataSubjectId(name: 'profile')]
593+
public readonly string $profileId,
594+
) {
595+
}
596+
}
597+
```
598+
599+
You can also use multiple data subject id's in one event by defining the name of the subject id's.
587600

588601
```php
589602
use Patchlevel\Hydrator\Attribute\DataSubjectId;
590-
use Patchlevel\Hydrator\Attribute\PersonalData;
603+
use Patchlevel\Hydrator\Attribute\SensitiveData;
591604

592605
final class DTO
593606
{
594607
public function __construct(
595-
#[DataSubjectId]
596-
public readonly string $profileId,
597-
#[PersonalData]
598-
public readonly string|null $email,
608+
#[DataSubjectId(name: 'profile1')]
609+
public readonly string $profile1Id,
610+
#[SensitiveData(subjectIdName: 'profile1')]
611+
public readonly string|null $email1,
612+
#[DataSubjectId(name: 'profile2')]
613+
public readonly string $profile2Id,
614+
#[SensitiveData(subjectIdName: 'profile2')]
615+
public readonly string|null $email2,
599616
) {
600617
}
601618
}
602619
```
603620

621+
> [!NOTE]
622+
> The default name of `DataSubjectId` is `default`.
623+
604624
If the information could not be decrypted, then a fallback value is inserted.
605625
The default fallback value is `null`.
606626
You can change this by setting the `fallback` parameter.
607627
In this case `unknown` is added:
608628

609629
```php
610-
use Patchlevel\Hydrator\Attribute\PersonalData;
630+
use Patchlevel\Hydrator\Attribute\SensitiveData;
611631

612632
final class DTO
613633
{
614634
public function __construct(
615635
#[DataSubjectId]
616636
public readonly string $profileId,
617-
#[PersonalData(fallback: 'unknown')]
637+
#[SensitiveData(fallback: 'unknown')]
618638
public readonly string $name,
619639
) {
620640
}
@@ -625,16 +645,16 @@ You can also use a callable as a fallback.
625645

626646
```php
627647
use Patchlevel\Hydrator\Attribute\DataSubjectId;
628-
use Patchlevel\Hydrator\Attribute\PersonalData;
648+
use Patchlevel\Hydrator\Attribute\SensitiveData;
629649

630650
final class ProfileCreated
631651
{
632652
public function __construct(
633653
#[DataSubjectId]
634654
public readonly string $profileId,
635-
#[PersonalData(fallback: 'deleted profile')]
655+
#[SensitiveData(fallback: 'deleted profile')]
636656
public readonly string $name,
637-
#[PersonalData(fallbackCallable: [self::class, 'anonymizedEmail'])]
657+
#[SensitiveData(fallbackCallable: [self::class, 'anonymizedEmail'])]
638658
public readonly string $email,
639659
) {
640660
}
@@ -655,13 +675,13 @@ final class ProfileCreated
655675
Here we show you how to configure the cryptography.
656676

657677
```php
658-
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
678+
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;
659679
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
660680
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
661681
use Patchlevel\Hydrator\MetadataHydrator;
662682

663683
$cipherKeyStore = new InMemoryCipherKeyStore();
664-
$cryptographer = PersonalDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
684+
$cryptographer = SensitiveDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
665685
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);
666686
```
667687

baseline.xml

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<files psalm-version="6.12.0@cf420941d061a57050b6c468ef2c778faf40aee2">
3-
<file src="src/Attribute/PersonalData.php">
3+
<file src="src/Attribute/SensitiveData.php">
44
<MixedPropertyTypeCoercion>
55
<code><![CDATA[$fallbackCallable]]></code>
66
</MixedPropertyTypeCoercion>
@@ -17,7 +17,7 @@
1717
<code><![CDATA[list<string>]]></code>
1818
</MoreSpecificReturnType>
1919
</file>
20-
<file src="src/Cryptography/PersonalDataPayloadCryptographer.php">
20+
<file src="src/Cryptography/SensitiveDataPayloadCryptographer.php">
2121
<MixedArgument>
2222
<code><![CDATA[$rawData]]></code>
2323
</MixedArgument>
@@ -30,12 +30,6 @@
3030
</MixedAssignment>
3131
</file>
3232
<file src="src/Metadata/AttributeMetadataFactory.php">
33-
<InvalidReturnStatement>
34-
<code><![CDATA[[false, null]]]></code>
35-
</InvalidReturnStatement>
36-
<InvalidReturnType>
37-
<code><![CDATA[array{bool, mixed, (callable(string, mixed):mixed)|null}]]></code>
38-
</InvalidReturnType>
3933
<PossiblyNullReference>
4034
<code><![CDATA[guess]]></code>
4135
</PossiblyNullReference>
@@ -47,7 +41,7 @@
4741
</file>
4842
<file src="src/Metadata/PropertyMetadata.php">
4943
<RiskyTruthyFalsyComparison>
50-
<code><![CDATA[$this->personalDataFallbackCallable]]></code>
44+
<code><![CDATA[$this->sensitiveDataFallbackCallable]]></code>
5145
</RiskyTruthyFalsyComparison>
5246
</file>
5347
<file src="src/MetadataHydrator.php">
@@ -82,23 +76,6 @@
8276
<code><![CDATA[$value]]></code>
8377
</MixedArgumentTypeCoercion>
8478
</file>
85-
<file src="tests/Benchmark/tideways.php">
86-
<ClassMustBeFinal>
87-
<code><![CDATA[CompiledMetadataHydrator]]></code>
88-
</ClassMustBeFinal>
89-
<MixedMethodCall>
90-
<code><![CDATA[normalize]]></code>
91-
<code><![CDATA[normalize]]></code>
92-
</MixedMethodCall>
93-
<MixedReturnTypeCoercion>
94-
<code><![CDATA[array]]></code>
95-
<code><![CDATA[match (true) {
96-
$object instanceof ProfileCreated => $this->extractProfileCreated($object),
97-
$object instanceof Skill => $this->extractSkill($object),
98-
default => throw new InvalidArgumentException('Unknown object type'),
99-
}]]></code>
100-
</MixedReturnTypeCoercion>
101-
</file>
10279
<file src="tests/Unit/Cryptography/Cipher/OpensslCipherTest.php">
10380
<MixedAssignment>
10481
<code><![CDATA[$return]]></code>

phpstan-baseline.neon

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ parameters:
1616
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Cryptography\\Cipher\\Cipher\:\:decrypt\(\) expects string, mixed given\.$#'
1717
identifier: argument.type
1818
count: 1
19-
path: src/Cryptography/PersonalDataPayloadCryptographer.php
19+
path: src/Cryptography/SensitiveDataPayloadCryptographer.php
2020

2121
-
2222
message: '#^Method Patchlevel\\Hydrator\\Guesser\\BuiltInGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
@@ -54,12 +54,6 @@ parameters:
5454
count: 1
5555
path: src/Metadata/AttributeMetadataFactory.php
5656

57-
-
58-
message: '#^Method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:getPersonalData\(\) should return array\{bool, mixed, \(callable\(string, mixed\)\: mixed\)\|null\} but returns array\{false, null\}\.$#'
59-
identifier: return.type
60-
count: 1
61-
path: src/Metadata/AttributeMetadataFactory.php
62-
6357
-
6458
message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:findNormalizerOnClass\(\) expects class\-string, string given\.$#'
6559
identifier: argument.type

src/Attribute/DataSubjectId.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
#[Attribute(Attribute::TARGET_PROPERTY)]
1010
final class DataSubjectId
1111
{
12+
public function __construct(
13+
public readonly string $name = 'default',
14+
) {
15+
}
1216
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
use InvalidArgumentException;
99

1010
#[Attribute(Attribute::TARGET_PROPERTY)]
11-
final class PersonalData
11+
final class SensitiveData
1212
{
1313
/** @var (callable(string, mixed):mixed)|null */
1414
public readonly mixed $fallbackCallable;
1515

1616
public function __construct(
1717
public readonly mixed $fallback = null,
1818
callable|null $fallbackCallable = null,
19+
public readonly string $subjectIdName = 'default',
1920
) {
2021
$this->fallbackCallable = $fallbackCallable;
2122

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
use RuntimeException;
8+
9+
use function sprintf;
10+
11+
final class NotSensitiveData extends RuntimeException
12+
{
13+
/** @param class-string $class */
14+
public function __construct(string $class, string $fieldName)
15+
{
16+
parent::__construct(sprintf('Trying to get subject id for %s::%s which is not marked as sensitive data.', $class, $fieldName));
17+
}
18+
}

src/Cryptography/PersonalDataPayloadCryptographer.php renamed to src/Cryptography/SensitiveDataPayloadCryptographer.php

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use function is_int;
1919
use function is_string;
2020

21-
final class PersonalDataPayloadCryptographer implements PayloadCryptographer
21+
final class SensitiveDataPayloadCryptographer implements PayloadCryptographer
2222
{
2323
public function __construct(
2424
private readonly CipherKeyStore $cipherKeyStore,
@@ -36,24 +36,20 @@ public function __construct(
3636
*/
3737
public function encrypt(ClassMetadata $metadata, array $data): array
3838
{
39-
$subjectId = $this->subjectId($metadata, $data);
40-
41-
if ($subjectId === null) {
42-
return $data;
43-
}
44-
45-
try {
46-
$cipherKey = $this->cipherKeyStore->get($subjectId);
47-
} catch (CipherKeyNotExists) {
48-
$cipherKey = ($this->cipherKeyFactory)();
49-
$this->cipherKeyStore->store($subjectId, $cipherKey);
50-
}
51-
5239
foreach ($metadata->properties() as $propertyMetadata) {
53-
if (!$propertyMetadata->isPersonalData()) {
40+
if (!$propertyMetadata->isSensitiveData()) {
5441
continue;
5542
}
5643

44+
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
45+
46+
try {
47+
$cipherKey = $this->cipherKeyStore->get($subjectId);
48+
} catch (CipherKeyNotExists) {
49+
$cipherKey = ($this->cipherKeyFactory)();
50+
$this->cipherKeyStore->store($subjectId, $cipherKey);
51+
}
52+
5753
$targetFieldName = $this->useEncryptedFieldName
5854
? $propertyMetadata->encryptedFieldName()
5955
: $propertyMetadata->fieldName();
@@ -80,23 +76,19 @@ public function encrypt(ClassMetadata $metadata, array $data): array
8076
*/
8177
public function decrypt(ClassMetadata $metadata, array $data): array
8278
{
83-
$subjectId = $this->subjectId($metadata, $data);
84-
85-
if ($subjectId === null) {
86-
return $data;
87-
}
88-
89-
try {
90-
$cipherKey = $this->cipherKeyStore->get($subjectId);
91-
} catch (CipherKeyNotExists) {
92-
$cipherKey = null;
93-
}
94-
9579
foreach ($metadata->properties() as $propertyMetadata) {
96-
if (!$propertyMetadata->isPersonalData()) {
80+
if (!$propertyMetadata->isSensitiveData()) {
9781
continue;
9882
}
9983

84+
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
85+
86+
try {
87+
$cipherKey = $this->cipherKeyStore->get($subjectId);
88+
} catch (CipherKeyNotExists) {
89+
$cipherKey = null;
90+
}
91+
10092
if ($this->useEncryptedFieldName && array_key_exists($propertyMetadata->encryptedFieldName(), $data)) {
10193
$rawData = $data[$propertyMetadata->encryptedFieldName()];
10294
unset($data[$propertyMetadata->encryptedFieldName()]);
@@ -125,14 +117,20 @@ public function decrypt(ClassMetadata $metadata, array $data): array
125117
}
126118

127119
/** @param array<string, mixed> $data */
128-
private function subjectId(ClassMetadata $metadata, array $data): string|null
120+
private function subjectId(PropertyMetadata $propertyMetadata, ClassMetadata $metadata, array $data): string
129121
{
130-
$fieldName = $metadata->dataSubjectIdField();
122+
if (!$propertyMetadata->isSensitiveData()) {
123+
throw new NotSensitiveData($metadata->className(), $propertyMetadata->propertyName());
124+
}
131125

132-
if ($fieldName === null) {
133-
return null;
126+
$sensitiveDataSubjectIdName = $propertyMetadata->sensitiveDataSubjectIdName();
127+
128+
if (!$metadata->hasSubjectIdIdentifier($sensitiveDataSubjectIdName)) {
129+
throw new MissingSubjectId($metadata->className(), $propertyMetadata->propertyName());
134130
}
135131

132+
$fieldName = $metadata->getSubjectIdFieldName($sensitiveDataSubjectIdName);
133+
136134
if (!array_key_exists($fieldName, $data)) {
137135
throw new MissingSubjectId($metadata->className(), $fieldName);
138136
}
@@ -152,10 +150,10 @@ private function subjectId(ClassMetadata $metadata, array $data): string|null
152150

153151
private function fallback(PropertyMetadata $propertyMetadata, string $subjectId, mixed $rawData): mixed
154152
{
155-
$callback = $propertyMetadata->personalDataFallbackCallback();
153+
$callback = $propertyMetadata->sensitiveDataFallbackCallable();
156154

157155
if (!$callback) {
158-
return $propertyMetadata->personalDataFallback();
156+
return $propertyMetadata->sensitiveDataFallback();
159157
}
160158

161159
return $callback($subjectId, $rawData);

0 commit comments

Comments
 (0)