Skip to content

Commit c5f436d

Browse files
committed
Add MissingHeaders, it will be created in the HeaderSerializer, if the Registry cannot find the headers. It is a collection of missing Headers
Make the missing header configurable Add * wildcard
1 parent 9b9b482 commit c5f436d

3 files changed

Lines changed: 151 additions & 6 deletions

File tree

src/Message/MissingHeaders.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Message;
6+
7+
/**
8+
* Collects all headers whose names could not be resolved to a registered class,
9+
* e.g. because the header class was removed. It keeps the raw names and payloads
10+
* so the data is preserved and can be written back without loss.
11+
*/
12+
final class MissingHeaders
13+
{
14+
/** @param array<string, array<mixed>> $headers */
15+
public function __construct(
16+
public readonly array $headers,
17+
) {
18+
}
19+
}

src/Message/Serializer/DefaultHeadersSerializer.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@
44

55
namespace Patchlevel\EventSourcing\Message\Serializer;
66

7+
use Patchlevel\EventSourcing\Message\MissingHeaders;
78
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;
9+
use Patchlevel\EventSourcing\Metadata\Message\HeaderNameNotRegistered;
810
use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry;
911
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;
1012
use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder;
1113
use Patchlevel\Hydrator\Hydrator;
1214
use Patchlevel\Hydrator\MetadataHydrator;
1315

16+
use function in_array;
1417
use function is_array;
1518

1619
final class DefaultHeadersSerializer implements HeadersSerializer
1720
{
21+
/** @param list<string> $gracefulMissingHeaders */
1822
public function __construct(
1923
private readonly MessageHeaderRegistry $messageHeaderRegistry,
2024
private readonly Hydrator $hydrator,
2125
private readonly Encoder $encoder,
26+
private readonly array $gracefulMissingHeaders = [],
2227
) {
2328
}
2429

@@ -30,6 +35,14 @@ public function serialize(array $headers, array $options = []): string
3035
{
3136
$serializedHeaders = [];
3237
foreach ($headers as $header) {
38+
if ($header instanceof MissingHeaders) {
39+
foreach ($header->headers as $name => $payload) {
40+
$serializedHeaders[$name] = $payload;
41+
}
42+
43+
continue;
44+
}
45+
3346
$serializedHeaders[$this->messageHeaderRegistry->headerName($header::class)] = $this->hydrator->extract($header);
3447
}
3548

@@ -44,29 +57,48 @@ public function serialize(array $headers, array $options = []): string
4457
public function deserialize(string $string, array $options = []): array
4558
{
4659
$serializedHeaders = $this->encoder->decode($string, $options);
60+
$handleAllHeadersGraceful = in_array('*', $this->gracefulMissingHeaders, true);
4761

4862
$headers = [];
63+
$missingHeaders = [];
64+
4965
foreach ($serializedHeaders as $headerName => $headerPayload) {
5066
if (!is_array($headerPayload)) {
5167
throw new InvalidArgument('header payload must be an array');
5268
}
5369

54-
$headers[] = $this->hydrator->hydrate(
55-
$this->messageHeaderRegistry->headerClass($headerName),
56-
$headerPayload,
57-
);
70+
try {
71+
$headers[] = $this->hydrator->hydrate(
72+
$this->messageHeaderRegistry->headerClass($headerName),
73+
$headerPayload,
74+
);
75+
} catch (HeaderNameNotRegistered $exception) {
76+
if (!$handleAllHeadersGraceful && !in_array($headerName, $this->gracefulMissingHeaders, true)) {
77+
throw $exception;
78+
}
79+
80+
$missingHeaders[$headerName] = $headerPayload;
81+
}
82+
}
83+
84+
if ($missingHeaders !== []) {
85+
$headers[] = new MissingHeaders($missingHeaders);
5886
}
5987

6088
return $headers;
6189
}
6290

63-
/** @param list<string> $paths */
64-
public static function createFromPaths(array $paths): static
91+
/**
92+
* @param list<string> $paths
93+
* @param list<string> $gracefulMissingHeaders
94+
*/
95+
public static function createFromPaths(array $paths, array $gracefulMissingHeaders = []): static
6596
{
6697
return new self(
6798
(new AttributeMessageHeaderRegistryFactory())->create($paths),
6899
new MetadataHydrator(),
69100
new JsonEncoder(),
101+
$gracefulMissingHeaders,
70102
);
71103
}
72104

@@ -76,6 +108,7 @@ public static function createDefault(): static
76108
MessageHeaderRegistry::createWithInternalHeaders(),
77109
new MetadataHydrator(),
78110
new JsonEncoder(),
111+
[],
79112
);
80113
}
81114
}

tests/Unit/Message/Serializer/DefaultHeadersSerializerTest.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
use DateTimeImmutable;
88
use Patchlevel\EventSourcing\Aggregate\AggregateHeader;
9+
use Patchlevel\EventSourcing\Message\MissingHeaders;
910
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;
1011
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;
12+
use Patchlevel\EventSourcing\Metadata\Message\HeaderNameNotRegistered;
1113
use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder;
1214
use Patchlevel\EventSourcing\Store\ArchivedHeader;
15+
use Patchlevel\EventSourcing\Store\Header\StreamNameHeader;
1316
use Patchlevel\Hydrator\MetadataHydrator;
1417
use PHPUnit\Framework\Attributes\CoversClass;
1518
use PHPUnit\Framework\TestCase;
@@ -54,4 +57,94 @@ public function testDeserialize(): void
5457
$deserializedMessage,
5558
);
5659
}
60+
61+
public function testDeserializeUnknownHeadersAsMissingHeaders(): void
62+
{
63+
$serializer = new DefaultHeadersSerializer(
64+
(new AttributeMessageHeaderRegistryFactory())->create([
65+
__DIR__ . '/../../Fixture',
66+
]),
67+
new MetadataHydrator(),
68+
new JsonEncoder(),
69+
['removed', 'alsoRemoved'],
70+
);
71+
72+
$deserializedMessage = $serializer->deserialize('{"streamName":{"streamName":"profile-1"},"removed":{"foo":"bar"},"alsoRemoved":{"baz":1}}');
73+
74+
self::assertEquals(
75+
[
76+
new StreamNameHeader('profile-1'),
77+
new MissingHeaders([
78+
'removed' => ['foo' => 'bar'],
79+
'alsoRemoved' => ['baz' => 1],
80+
]),
81+
],
82+
$deserializedMessage,
83+
);
84+
}
85+
86+
public function testDeserializeUnknownHeaderNotConfiguredCrashes(): void
87+
{
88+
$serializer = new DefaultHeadersSerializer(
89+
(new AttributeMessageHeaderRegistryFactory())->create([
90+
__DIR__ . '/../../Fixture',
91+
]),
92+
new MetadataHydrator(),
93+
new JsonEncoder(),
94+
['removed'],
95+
);
96+
97+
$this->expectException(HeaderNameNotRegistered::class);
98+
99+
$serializer->deserialize('{"streamName":{"streamName":"profile-1"},"removed":{"foo":"bar"},"notListed":{"baz":1}}');
100+
}
101+
102+
public function testDeserializeWildcardHandlesAllUnknownHeaders(): void
103+
{
104+
$serializer = new DefaultHeadersSerializer(
105+
(new AttributeMessageHeaderRegistryFactory())->create([
106+
__DIR__ . '/../../Fixture',
107+
]),
108+
new MetadataHydrator(),
109+
new JsonEncoder(),
110+
['*'],
111+
);
112+
113+
$deserializedMessage = $serializer->deserialize('{"streamName":{"streamName":"profile-1"},"removed":{"foo":"bar"},"alsoRemoved":{"baz":1}}');
114+
115+
self::assertEquals(
116+
[
117+
new StreamNameHeader('profile-1'),
118+
new MissingHeaders([
119+
'removed' => ['foo' => 'bar'],
120+
'alsoRemoved' => ['baz' => 1],
121+
]),
122+
],
123+
$deserializedMessage,
124+
);
125+
}
126+
127+
public function testSerializeMissingHeadersRoundTrip(): void
128+
{
129+
$serializer = new DefaultHeadersSerializer(
130+
(new AttributeMessageHeaderRegistryFactory())->create([
131+
__DIR__ . '/../../Fixture',
132+
]),
133+
new MetadataHydrator(),
134+
new JsonEncoder(),
135+
);
136+
137+
$content = $serializer->serialize([
138+
new StreamNameHeader('profile-1'),
139+
new MissingHeaders([
140+
'removed' => ['foo' => 'bar'],
141+
'alsoRemoved' => ['baz' => 1],
142+
]),
143+
]);
144+
145+
self::assertEquals(
146+
'{"streamName":{"streamName":"profile-1"},"removed":{"foo":"bar"},"alsoRemoved":{"baz":1}}',
147+
$content,
148+
);
149+
}
57150
}

0 commit comments

Comments
 (0)