Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58936e9
add field prefix for encrypted fields
DavidBadura Mar 20, 2025
56c3279
move encrypted field name logic into metadata
DavidBadura Mar 20, 2025
bcfeb0e
Lock file maintenance
renovate[bot] Mar 26, 2025
0eae801
Lock file maintenance
renovate[bot] Mar 27, 2025
95f482b
Lock file maintenance
renovate[bot] Mar 28, 2025
6f7b5e6
Lock file maintenance
renovate[bot] Mar 29, 2025
543df50
Sync benchmarks
DanielBadura Apr 2, 2025
40f2d29
Reduce Revs to 5
DanielBadura Apr 3, 2025
a089a74
"Fix" type annotation
DanielBadura Apr 3, 2025
4990a46
Merge pull request #78 from patchlevel/sync-benches
DavidBadura Apr 3, 2025
3d2b007
event dispatcher
DavidBadura Apr 2, 2025
de433e3
fix cs
DavidBadura Apr 2, 2025
e0b4d28
improve constructor
DavidBadura Apr 2, 2025
23f244e
try to improve performance without cryptography
DavidBadura Apr 2, 2025
7e23e6b
change parameter ordering
DavidBadura Apr 2, 2025
7730cb1
update readme
DavidBadura Apr 4, 2025
2898641
add tests
DavidBadura Apr 4, 2025
b9306c3
Merge pull request #74 from patchlevel/crypto-field-prefix
DavidBadura Apr 4, 2025
8b73304
Merge pull request #77 from patchlevel/event-dispatcher-legacy
DavidBadura Apr 6, 2025
d324ee4
make encrypted field names optional
DavidBadura Apr 10, 2025
c1d88b0
Merge pull request #79 from patchlevel/optional-encrypted-field-name
DavidBadura Apr 10, 2025
17fa692
add callable fallback for encryption
DavidBadura Apr 10, 2025
401267c
fix phpstan
DavidBadura Apr 11, 2025
6a5e93a
update signature of callback
DavidBadura Apr 11, 2025
d583af6
Merge pull request #80 from patchlevel/encrypt-callback-fallback
DavidBadura Apr 11, 2025
395d939
Merge remote-tracking branch 'origin/2.0.x' into 1.8.x-merge-up-into-…
DanielBadura Apr 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 85 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,22 +406,83 @@ readonly class Dto
}
```

### Events

Another way to intervene in the extract and hydrate process is through events.
There are two events: `PostExtract` and `PreHydrate`.
For this functionality we use the [symfony/event-dispatcher](https://symfony.com/doc/current/components/event_dispatcher.html).

```php
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Patchlevel\Hydrator\Event\PostExtract;
use Patchlevel\Hydrator\Event\PreHydrate;

$eventDispatcher = new EventDispatcher();

$eventDispatcher->addListener(
PostExtract::class,
static function (PostExtract $event): void {
// do something
}
);

$eventDispatcher->addListener(
PreHydrate::class,
static function (PreHydrate $event): void {
// do something
}
);

$hydrator = new MetadataHydrator(eventDispatcher: $eventDispatcher);
```

### Cryptography

The library also offers the possibility to encrypt and decrypt personal data.
For this purpose, a key is created for each subject ID, which is used to encrypt the personal data.

#### DataSubjectId

First we need to define what the subject id is.

```php
use Patchlevel\Hydrator\Attribute\DataSubjectId;

final class EmailChanged
{
public function __construct(
#[DataSubjectId]
public readonly string $profileId,
) {
}
}
```

> [!WARNING]
> The `DataSubjectId` must be a string. You can use a normalizer to convert it to a string.
> The Subject ID cannot be personal data.

#### PersonalData

First of all, we have to mark the fields that contain personal data.
For our example, we use events, but you can do the same with aggregates.
Next, we need to specify which fields we want to encrypt.

```php
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;

final class DTO
{
#[PersonalData]
public readonly string|null $email;
public function __construct(
#[DataSubjectId]
public readonly string $profileId,
#[PersonalData]
public readonly string|null $email,
) {
}
}
```

Expand All @@ -436,43 +497,40 @@ use Patchlevel\Hydrator\Attribute\PersonalData;
final class DTO
{
public function __construct(
#[DataSubjectId]
public readonly string $profileId,
#[PersonalData(fallback: 'unknown')]
public readonly string $email,
public readonly string $name,
) {
}
}
```

> [!DANGER]
> You have to deal with this case in your business logic such as aggregates and subscriptions.

> [!WARNING]
> You need to define a subject ID to use the personal data attribute.

#### DataSubjectId

In order for the correct key to be used, a subject ID must be defined.
Without Subject Id, no personal data can be encrypted or decrypted.
You can also use a callable as a fallback.

```php
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;

final class EmailChanged
final class ProfileCreated
{
public function __construct(
#[DataSubjectId]
public readonly string $personId,
#[PersonalData(fallback: 'unknown')]
public readonly string|null $email,
public readonly string $profileId,
#[PersonalData(fallback: 'deleted profile')]
public readonly string $name,
#[PersonalData(fallbackCallable: [self::class, 'anonymizedEmail'])]
public readonly string $email,
) {
}

public static function anonymizedEmail(string $subjectId): string
{
return sprintf('%s@anno.com', $subjectId);
}
}
```

> [!WARNING]
> A subject ID can not be a personal data.

#### Configure Cryptography

Here we show you how to configure the cryptography.
Expand All @@ -484,10 +542,14 @@ use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;

$cipherKeyStore = new InMemoryCipherKeyStore();
$cryptographer = PersonalDataPayloadCryptographer::createWithOpenssl($cipherKeyStore);
$cryptographer = PersonalDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);
```

> [!WARNING]
> We recommend to use the `useEncryptedFieldName` option to recognize encrypted fields.
> This allows data to be encrypted later without big troubles.

#### Cipher Key Store

The keys must be stored somewhere. For testing purposes, we offer an in-memory implementation.
Expand Down
6 changes: 4 additions & 2 deletions baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
<files psalm-version="6.9.1@81c8a77c0793d450fee40265cfe68891df11d505">
<file src="src/Cryptography/Cipher/OpensslCipherKeyFactory.php">
<ArgumentTypeCoercion>
<code><![CDATA[openssl_random_pseudo_bytes($this->ivLength)]]></code>
Expand All @@ -14,12 +14,14 @@
</file>
<file src="src/Cryptography/PersonalDataPayloadCryptographer.php">
<MixedArgument>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$rawData]]></code>
</MixedArgument>
<MixedAssignment>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$rawData]]></code>
<code><![CDATA[$rawData]]></code>
</MixedAssignment>
</file>
<file src="src/Metadata/AttributeMetadataFactory.php">
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-openssl": "*",
"symfony/event-dispatcher": "^5.4.29|^6.4.0|^7.0.0",
"symfony/type-info": "^7.2.4"
},
"require-dev": {
Expand Down
Loading
Loading