Skip to content

Commit 3b96b86

Browse files
committed
Merge remote-tracking branch 'origin/2.0.x' into 1.13.x-merge-up-into-2.0.x_BVhYvWnC
2 parents dc6fe9a + 5c4e315 commit 3b96b86

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+896
-1082
lines changed

README.md

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpatchlevel%2Fhydrator%2F1.13.x)](https://dashboard.stryker-mutator.io/reports/github.com/patchlevel/hydrator/1.13.x)
1+
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpatchlevel%2Fhydrator%2F2.0.x)](https://dashboard.stryker-mutator.io/reports/github.com/patchlevel/hydrator/2.0.x)
22
[![Latest Stable Version](https://poser.pugx.org/patchlevel/hydrator/v)](//packagist.org/packages/patchlevel/hydrator)
33
[![License](https://poser.pugx.org/patchlevel/hydrator/license)](//packagist.org/packages/patchlevel/hydrator)
44

@@ -527,7 +527,7 @@ There are two events: `PostExtract` and `PreHydrate`.
527527
For this functionality we use the [symfony/event-dispatcher](https://symfony.com/doc/current/components/event_dispatcher.html).
528528

529529
```php
530-
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
530+
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;
531531
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
532532
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
533533
use Patchlevel\Hydrator\MetadataHydrator;
@@ -556,8 +556,8 @@ $hydrator = new MetadataHydrator(eventDispatcher: $eventDispatcher);
556556

557557
### Cryptography
558558

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

562562
#### DataSubjectId
563563

@@ -578,42 +578,62 @@ final class EmailChanged
578578

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

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

587600
```php
588601
use Patchlevel\Hydrator\Attribute\DataSubjectId;
589-
use Patchlevel\Hydrator\Attribute\PersonalData;
602+
use Patchlevel\Hydrator\Attribute\SensitiveData;
590603

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

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

608628
```php
609-
use Patchlevel\Hydrator\Attribute\PersonalData;
629+
use Patchlevel\Hydrator\Attribute\SensitiveData;
610630

611631
final class DTO
612632
{
613633
public function __construct(
614634
#[DataSubjectId]
615635
public readonly string $profileId,
616-
#[PersonalData(fallback: 'unknown')]
636+
#[SensitiveData(fallback: 'unknown')]
617637
public readonly string $name,
618638
) {
619639
}
@@ -624,16 +644,16 @@ You can also use a callable as a fallback.
624644

625645
```php
626646
use Patchlevel\Hydrator\Attribute\DataSubjectId;
627-
use Patchlevel\Hydrator\Attribute\PersonalData;
647+
use Patchlevel\Hydrator\Attribute\SensitiveData;
628648

629649
final class ProfileCreated
630650
{
631651
public function __construct(
632652
#[DataSubjectId]
633653
public readonly string $profileId,
634-
#[PersonalData(fallback: 'deleted profile')]
654+
#[SensitiveData(fallback: 'deleted profile')]
635655
public readonly string $name,
636-
#[PersonalData(fallbackCallable: [self::class, 'anonymizedEmail'])]
656+
#[SensitiveData(fallbackCallable: [self::class, 'anonymizedEmail'])]
637657
public readonly string $email,
638658
) {
639659
}
@@ -654,13 +674,13 @@ final class ProfileCreated
654674
Here we show you how to configure the cryptography.
655675

656676
```php
657-
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
677+
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;
658678
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
659679
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
660680
use Patchlevel\Hydrator\MetadataHydrator;
661681

662682
$cipherKeyStore = new InMemoryCipherKeyStore();
663-
$cryptographer = PersonalDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
683+
$cryptographer = SensitiveDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
664684
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);
665685
```
666686

phpstan-baseline.neon

Lines changed: 14 additions & 20 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,24 +54,12 @@ 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
6660
count: 3
6761
path: src/Metadata/AttributeMetadataFactory.php
6862

69-
-
70-
message: '#^Property Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:\$guesser \(Patchlevel\\Hydrator\\Guesser\\Guesser\|null\) is never assigned null so it can be removed from the property type\.$#'
71-
identifier: property.unusedType
72-
count: 1
73-
path: src/Metadata/AttributeMetadataFactory.php
74-
7563
-
7664
message: '#^Property Patchlevel\\Hydrator\\Metadata\\ClassMetadata\<T of object \= object\>\:\:\$reflection \(ReflectionClass\<T of object \= object\>\) does not accept ReflectionClass\<object\>\.$#'
7765
identifier: assign.propertyType
@@ -103,10 +91,10 @@ parameters:
10391
path: src/Normalizer/ObjectNormalizer.php
10492

10593
-
106-
message: '#^Method Patchlevel\\Hydrator\\Normalizer\\ReflectionTypeUtil\:\:classStringInstanceOf\(\) should return class\-string\<T\> but returns class\-string\<T\>\|\(string&T\)\.$#'
107-
identifier: return.type
94+
message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\ChildWithSensitiveDataWithIdentifierDto\:\:\$email is never read, only written\.$#'
95+
identifier: property.onlyWritten
10896
count: 1
109-
path: src/Normalizer/ReflectionTypeUtil.php
97+
path: tests/Unit/Fixture/ChildWithSensitiveDataWithIdentifierDto.php
11098

11199
-
112100
message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:postHydrate\(\) is unused\.$#'
@@ -121,13 +109,19 @@ parameters:
121109
path: tests/Unit/Fixture/DtoWithHooks.php
122110

123111
-
124-
message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:454\:\:postHydrate\(\) is unused\.$#'
112+
message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\IdNormalizer\:\:\$idClass \(class\-string\<Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\Id\>\|null\) does not accept string\.$#'
113+
identifier: assign.propertyType
114+
count: 1
115+
path: tests/Unit/Fixture/IdNormalizer.php
116+
117+
-
118+
message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:344\:\:postHydrate\(\) is unused\.$#'
125119
identifier: method.unused
126120
count: 1
127121
path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php
128122

129123
-
130-
message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:454\:\:preExtract\(\) is unused\.$#'
124+
message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:344\:\:preExtract\(\) is unused\.$#'
131125
identifier: method.unused
132126
count: 1
133127
path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php
@@ -139,13 +133,13 @@ parameters:
139133
path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php
140134

141135
-
142-
message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:482\:\:postHydrate\(\) is unused\.$#'
136+
message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:372\:\:postHydrate\(\) is unused\.$#'
143137
identifier: method.unused
144138
count: 1
145139
path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php
146140

147141
-
148-
message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:482\:\:preExtract\(\) is unused\.$#'
142+
message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:372\:\:preExtract\(\) is unused\.$#'
149143
identifier: method.unused
150144
count: 1
151145
path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ services:
1313
-
1414
class: Patchlevel\Hydrator\Tests\Architecture\FinalClassesTest
1515
tags:
16-
- phpat.test
16+
- phpat.test

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
}

src/Attribute/PersonalData.php renamed to src/Attribute/SensitiveData.php

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

0 commit comments

Comments
 (0)