Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"doctrine/dbal": "^4.0.0",
"doctrine/migrations": "^3.3.2",
"patchlevel/hydrator": "^1.8.0",
"patchlevel/hydrator": "^2.0.x-dev",
"patchlevel/worker": "^1.4.0",
"psr/cache": "^2.0.0 || ^3.0.0",
"psr/clock": "^1.0",
Expand Down
412 changes: 218 additions & 194 deletions composer.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ powered by the reliable Doctrine ecosystem and focused on developer experience.
* Automatic [snapshot](snapshots.md)-system to boost your performance
* [Split](split-stream.md) big aggregates into multiple streams
* Versioned and managed lifecycle of [subscriptions](subscription.md) like projections and processors
* Safe usage of [Personal Data](personal-data.md) with crypto-shredding
* Safe usage of [Personal Data](sensitive-data.md) with crypto-shredding
* Smooth [upcasting](upcasting.md) of old events
* Simple setup with [scheme management](store.md) and [doctrine migration](store.md)
* Built in [cli commands](cli.md) with [symfony](https://symfony.com/)
Expand Down
4 changes: 2 additions & 2 deletions docs/normalizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ final class CreateHotel
```

:::note
If you have personal data, you can use [crypto-shredding](personal-data.md).
If you have personal data, you can use [crypto-shredding](sensitive_data.md).
:::

### Aggregate
Expand Down Expand Up @@ -472,4 +472,4 @@ final class DTO
* [How to define aggregates](aggregate.md)
* [How to define events](events.md)
* [How to snapshot aggregates](snapshots.md)
* [How to work with personal data](personal-data.md)
* [How to work with personal data](sensitive-data.md)
35 changes: 17 additions & 18 deletions docs/personal-data.md → docs/sensitive-data.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Personal Data (GDPR)
# Sensitive Data

According to GDPR, personal data must be able to be deleted upon request.
But here we have the problem that our events are immutable and we cannot easily manipulate the event store.
Expand Down Expand Up @@ -42,48 +42,47 @@ final class EmailChanged

:::tip
You can use the `DataSubjectId` in aggregates for snapshots too.
:::

### PersonalData
:::
### SensitiveData

Next, you have to mark the properties that should be encrypted with the `#[PersonalData]` attribute.
Next, you have to mark the properties that should be encrypted with the `#[SensitiveData]` attribute.

```php
use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\SensitiveData;

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

:::tip
You can use the `PersonalData` in aggregates for snapshots too.
You can use the `SensitiveData` in aggregates for snapshots too.
:::

If the information could not be decrypted, then a fallback value will be used.
The default fallback value is `null`.
You can change this by setting the `fallback` parameter or using the `fallbackCallable` parameter.

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

final class ProfileChanged
{
public function __construct(
#[DataSubjectId]
public readonly Uuid $profileId,
#[PersonalData(fallback: 'unknown')]
#[SensitiveData(fallback: 'unknown')]
public readonly string $name,
#[PersonalData(fallbackCallable: [self::class, 'createAnonymousEmail'])]
#[SensitiveData(fallbackCallable: [self::class, 'createAnonymousEmail'])]
public readonly string $email,
) {
}
Expand Down Expand Up @@ -147,10 +146,10 @@ Now we have to put the whole thing together in a Personal Data Payload Cryptogra

```php
use Patchlevel\EventSourcing\Cryptography\Store\CipherKeyStore;
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;

/** @var CipherKeyStore $cipherKeyStore */
$cryptographer = PersonalDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
$cryptographer = SensitiveDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore);
```

:::tip
Expand All @@ -163,9 +162,9 @@ The last step is to integrate the cryptographer into the event store.

```php
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;

/** @var PersonalDataPayloadCryptographer $cryptographer */
/** @var SensitiveDataPayloadCryptographer $cryptographer */
DefaultEventSerializer::createFromPaths(
[__DIR__ . '/Events'],
cryptographer: $cryptographer,
Expand All @@ -182,9 +181,9 @@ And for the snapshot store.

```php
use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore;
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;

/** @var PersonalDataPayloadCryptographer $cryptographer */
/** @var SensitiveDataPayloadCryptographer $cryptographer */
$snapshotStore = DefaultSnapshotStore::createDefault(
[
/* adapters... */
Expand Down Expand Up @@ -212,7 +211,7 @@ use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;

#[Processor('delete_personal_data')]
final class DeletePersonalDataProcessor
final class DeleteSensitiveDataProcessor
{
public function __construct(
private readonly CipherKeyStore $cipherKeyStore,
Expand Down
2 changes: 1 addition & 1 deletion docs/snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,4 @@ You still have to bring the aggregate up to date by loading the missing events f
* [How to define aggregates](aggregate.md)
* [How to store and load aggregates](repository.md)
* [How to split streams](split-stream.md)
* [How to work with personal data](personal-data.md)
* [How to work with personal data](sensitive-data.md)
6 changes: 4 additions & 2 deletions src/Message/Serializer/DefaultHeadersSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry;
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;
use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder;
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\HydratorBuilder;
use Patchlevel\Hydrator\MetadataHydrator;

use function is_array;
Expand Down Expand Up @@ -65,7 +67,7 @@ public static function createFromPaths(array $paths): static
{
return new self(
(new AttributeMessageHeaderRegistryFactory())->create($paths),
new MetadataHydrator(),
(new HydratorBuilder())->useExtension(new CoreExtension())->build(),
new JsonEncoder(),
);
}
Expand All @@ -74,7 +76,7 @@ public static function createDefault(): static
{
return new self(
MessageHeaderRegistry::createWithInternalHeaders(),
new MetadataHydrator(),
(new HydratorBuilder())->useExtension(new CoreExtension())->build(),
new JsonEncoder(),
);
}
Expand Down
44 changes: 25 additions & 19 deletions src/Serializer/DefaultEventSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,33 @@
use Patchlevel\EventSourcing\Metadata\Event\EventRegistry;
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;
use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder;
use Patchlevel\EventSourcing\Serializer\Upcast\Upcast;
use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster;
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Cryptography\CryptographyExtension;
use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\MetadataHydrator;
use Patchlevel\Hydrator\HydratorBuilder;

final class DefaultEventSerializer implements EventSerializer
{
public const CONTEXT_EVENT_NAME = 'event_name';
public const CONTEXT_EVENT_CLASS = 'event_class';

public function __construct(
private EventRegistry $eventRegistry,
private Hydrator $hydrator = new MetadataHydrator(),
private Hydrator $hydrator,
private Encoder $encoder = new JsonEncoder(),
private Upcaster|null $upcaster = null,
) {
}

/** @param array<string, mixed> $options */
public function serialize(object $event, array $options = []): SerializedEvent
{
$name = $this->eventRegistry->eventName($event::class);
$data = $this->hydrator->extract($event);

$data = $this->hydrator->extract($event, [
self::CONTEXT_EVENT_NAME => $name,
self::CONTEXT_EVENT_CLASS => $event::class,
]);

return new SerializedEvent(
$name,
Expand All @@ -40,30 +46,30 @@ public function serialize(object $event, array $options = []): SerializedEvent
public function deserialize(SerializedEvent $data, array $options = []): object
{
$payload = $this->encoder->decode($data->payload, $options);
$class = $this->eventRegistry->eventClass($data->name);

$eventName = $data->name;
if ($this->upcaster) {
$upcast = ($this->upcaster)(new Upcast($data->name, $payload));
$eventName = $upcast->eventName;
$payload = $upcast->payload;
}

$class = $this->eventRegistry->eventClass($eventName);

return $this->hydrator->hydrate($class, $payload);
return $this->hydrator->hydrate($class, $payload, [
self::CONTEXT_EVENT_NAME => $data->name,
self::CONTEXT_EVENT_CLASS => $class,
]);
}

/** @param list<string> $paths */
public static function createFromPaths(
array $paths,
Upcaster|null $upcaster = null,
PayloadCryptographer|null $cryptographer = null,
): static {
$builder = (new HydratorBuilder())
->useExtension(new CoreExtension());

if ($cryptographer) {
$builder->useExtension(new CryptographyExtension($cryptographer));
}

return new self(
(new AttributeEventRegistryFactory())->create($paths),
new MetadataHydrator(cryptographer: $cryptographer),
$builder->build(),
new JsonEncoder(),
$upcaster,
);
}
}
4 changes: 2 additions & 2 deletions src/Serializer/Normalizer/IdNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function __construct(
) {
}

public function normalize(mixed $value): string|null
public function normalize(mixed $value, array $context): string|null
{
if ($value === null) {
return null;
Expand All @@ -40,7 +40,7 @@ public function normalize(mixed $value): string|null
return $value->toString();
}

public function denormalize(mixed $value): Identifier|null
public function denormalize(mixed $value, array $context): Identifier|null
{
if ($value === null) {
return null;
Expand Down
35 changes: 35 additions & 0 deletions src/Serializer/Upcast/UpcastMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Patchlevel\EventSourcing\Serializer\Upcast;

use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;

final readonly class UpcastMiddleware implements Middleware
{
public function __construct(
private Upcaster $upcaster
) {
}

public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
$eventClass = $context[DefaultEventSerializer::CONTEXT_EVENT_CLASS] ?? null;
$eventName = $context[DefaultEventSerializer::CONTEXT_EVENT_NAME] ?? null;

if ($eventName === null || $metadata->className !== $eventClass) {
return $stack->next()->hydrate($metadata, $data, $context, $stack);
}

$upcast = ($this->upcaster)(new Upcast($eventName, $data));

Check failure on line 26 in src/Serializer/Upcast/UpcastMiddleware.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.5, ubuntu-latest)

Parameter #1 $eventName of class Patchlevel\EventSourcing\Serializer\Upcast\Upcast constructor expects string, mixed given.

return $stack->next()->hydrate($metadata, $upcast->payload, $context, $stack);
}

public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
return $stack->next()->extract($metadata, $object, $context, $stack);
}
}
16 changes: 12 additions & 4 deletions src/Snapshot/DefaultSnapshotStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataAwareMetadataFactory;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataFactory;
use Patchlevel\EventSourcing\Snapshot\Adapter\SnapshotAdapter;
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Cryptography\CryptographyExtension;
use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\MetadataHydrator;
use Patchlevel\Hydrator\HydratorBuilder;
use Throwable;

use function array_key_exists;
use function is_array;
use function sprintf;
Expand All @@ -38,7 +39,7 @@ public function __construct(
$this->adapterRepository = $adapterRepository;
}

$this->hydrator = $hydrator ?? new MetadataHydrator();
$this->hydrator = $hydrator ?? (new HydratorBuilder())->useExtension(new CoreExtension())->build();;
$this->metadataFactory = $metadataFactory ?? new AggregateRootMetadataAwareMetadataFactory();
}

Expand Down Expand Up @@ -120,9 +121,16 @@ private function version(string $aggregateClass): string|null
/** @param array<string, SnapshotAdapter> $snapshotAdapters */
public static function createDefault(array $snapshotAdapters, PayloadCryptographer|null $cryptographer = null): self
{
$builder = new HydratorBuilder();
$builder->useExtension(new CoreExtension());

if ($cryptographer) {
$builder->useExtension(new CryptographyExtension($cryptographer));
}

return new self(
new ArrayAdapterRepository($snapshotAdapters),
new MetadataHydrator(cryptographer: $cryptographer),
$builder->build(),
);
}
}
4 changes: 2 additions & 2 deletions tests/Benchmark/BasicImplementation/Events/EmailChanged.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
use Patchlevel\EventSourcing\Attribute\Event;
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\SensitiveData;

#[Event('profile.email_changed')]
final class EmailChanged
{
public function __construct(
#[DataSubjectId]
public ProfileId $profileId,
#[PersonalData]
#[SensitiveData]
public string|null $email,
) {
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Benchmark/BasicImplementation/Events/ProfileCreated.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Patchlevel\EventSourcing\Attribute\EventTag;
use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Attribute\SensitiveData;

#[Event('profile.created')]
final class ProfileCreated
Expand All @@ -18,7 +18,7 @@ public function __construct(
#[EventTag(prefix: 'profile')]
public ProfileId $profileId,
public string $name,
#[PersonalData]
#[SensitiveData]
public string|null $email,
) {
}
Expand Down
Loading
Loading