Skip to content

Commit 678260c

Browse files
authored
Release/3.0.0 (#4)
* feat: Adopt IntegrationEvent pipeline in outbox persistence. PayloadSerializer now receives IntegrationEventRecord instead of EventRecord, and DoctrineOutboxRepository gains a required IntegrationEventTranslators parameter. Domain events without a registered translator are silently skipped, making translator registration the explicit opt-in for cross-context publication. * refactor: Move fromArray method to a new location in SerializedPayload.php.
1 parent 26c53d0 commit 678260c

22 files changed

Lines changed: 686 additions & 190 deletions

README.md

Lines changed: 223 additions & 58 deletions
Large diffs are not rendered by default.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"event-sourcing",
1414
"event-versioning",
1515
"event-persistence",
16+
"integration-events",
1617
"event-serialization",
1718
"transactional-outbox"
1819
],
@@ -31,7 +32,7 @@
3132
"php": "^8.5",
3233
"doctrine/dbal": "^4.4",
3334
"ramsey/uuid": "^4.9",
34-
"tiny-blocks/building-blocks": "^3.0",
35+
"tiny-blocks/building-blocks": "^4.0",
3536
"tiny-blocks/collection": "^2.3"
3637
},
3738
"require-dev": {

src/DoctrineOutboxRepository.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
99
use TinyBlocks\BuildingBlocks\Event\EventRecord;
1010
use TinyBlocks\BuildingBlocks\Event\EventRecords;
11+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventRecord;
12+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventTranslators;
1113
use TinyBlocks\Outbox\Exceptions\DuplicateAggregateVersion;
1214
use TinyBlocks\Outbox\Exceptions\DuplicateOutboxEvent;
1315
use TinyBlocks\Outbox\Exceptions\OutboxRequiresActiveTransaction;
@@ -22,6 +24,7 @@
2224

2325
public function __construct(
2426
private Connection $connection,
27+
private IntegrationEventTranslators $translators,
2528
private PayloadSerializers $serializers,
2629
?TableLayout $tableLayout = null
2730
) {
@@ -34,16 +37,29 @@ public function push(EventRecords $records): void
3437
throw OutboxRequiresActiveTransaction::asMissing();
3538
}
3639

37-
$records->each(actions: function (EventRecord $record): void {
38-
$payloadSerializer = $this->serializers->findFor(record: $record);
40+
$records->each(actions: function (EventRecord $eventRecord): void {
41+
$translator = $this->translators->findFor(record: $eventRecord);
42+
43+
if (is_null($translator)) {
44+
return;
45+
}
46+
47+
$integrationEventRecord = IntegrationEventRecord::from(
48+
eventRecord: $eventRecord,
49+
integrationEvent: $translator->translate(record: $eventRecord)
50+
);
51+
52+
$payloadSerializer = $this->serializers->findFor(record: $integrationEventRecord);
3953

4054
if (is_null($payloadSerializer)) {
41-
throw PayloadSerializerNotConfigured::forEventClass(eventClass: $record->event::class);
55+
throw PayloadSerializerNotConfigured::forEventClass(
56+
eventClass: $integrationEventRecord->event::class
57+
);
4258
}
4359

4460
$insert = OutboxInsert::from(
45-
record: $record,
46-
payload: $payloadSerializer->serialize(record: $record),
61+
record: $integrationEventRecord,
62+
payload: $payloadSerializer->serialize(record: $integrationEventRecord),
4763
tableLayout: $this->tableLayout
4864
);
4965

@@ -53,13 +69,16 @@ public function push(EventRecords $records): void
5369
if ($this->tableLayout->uniqueConstraint->isViolatedBy(exception: $exception)) {
5470
throw DuplicateAggregateVersion::forRecord(
5571
previous: $exception,
56-
aggregateId: $record->aggregateId->identityValue(),
57-
aggregateType: $record->aggregateType,
58-
aggregateVersion: $record->aggregateVersion->value
72+
aggregateId: $integrationEventRecord->aggregateId->identityValue(),
73+
aggregateType: $integrationEventRecord->aggregateType,
74+
aggregateVersion: $integrationEventRecord->aggregateVersion->value
5975
);
6076
}
6177

62-
throw DuplicateOutboxEvent::forRecord(eventId: $record->id, previous: $exception);
78+
throw DuplicateOutboxEvent::forRecord(
79+
eventId: $integrationEventRecord->id,
80+
previous: $exception
81+
);
6382
}
6483
});
6584
}

src/Internal/OutboxInsert.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace TinyBlocks\Outbox\Internal;
66

7-
use TinyBlocks\BuildingBlocks\Event\EventRecord;
7+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventRecord;
88
use TinyBlocks\Outbox\Schema\TableLayout;
99
use TinyBlocks\Outbox\Serialization\SerializedPayload;
1010

@@ -15,7 +15,7 @@ private function __construct(public string $sql, public array $parameters)
1515
}
1616

1717
public static function from(
18-
EventRecord $record,
18+
IntegrationEventRecord $record,
1919
SerializedPayload $payload,
2020
TableLayout $tableLayout
2121
): OutboxInsert {

src/OutboxRepository.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
namespace TinyBlocks\Outbox;
66

77
use TinyBlocks\BuildingBlocks\Event\EventRecords;
8+
use TinyBlocks\BuildingBlocks\Event\IntegrationEvent;
9+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventTranslator;
810
use TinyBlocks\Outbox\Exceptions\DuplicateAggregateVersion;
911
use TinyBlocks\Outbox\Exceptions\DuplicateOutboxEvent;
1012
use TinyBlocks\Outbox\Exceptions\InvalidPayloadJson;
1113
use TinyBlocks\Outbox\Exceptions\OutboxRequiresActiveTransaction;
1214
use TinyBlocks\Outbox\Exceptions\PayloadSerializerNotConfigured;
15+
use TinyBlocks\Outbox\Serialization\PayloadSerializer;
1316

1417
/**
1518
* Producer-side contract: persists outbox records as part of the caller's open transaction.
@@ -23,14 +26,28 @@ interface OutboxRepository
2326
/**
2427
* Persists the given records as part of the caller's open transaction.
2528
*
26-
* <p>The implementation must not open or commit a transaction. It is the caller's responsibility
27-
* to ensure this call happens inside the same unit of work as the aggregate state change.</p>
29+
* <p>The input carries domain events from the aggregate's recorded-events buffer.
30+
* The implementation filters each record through the registered
31+
* {@see IntegrationEventTranslator} collection: domain events without a matching
32+
* translator are silently skipped, because the absence of a translator is the canonical
33+
* declaration that the event is internal to the bounded context and must not cross its
34+
* boundary.</p>
35+
*
36+
* <p>Matched domain events are translated into {@see IntegrationEvent} envelopes via
37+
* the Anti-Corruption Layer and only then serialized and persisted. The
38+
* {@see PayloadSerializer} operates on the integration event record, never on the
39+
* domain event directly.</p>
40+
*
41+
* <p>The implementation must not open or commit a transaction. It is the caller's
42+
* responsibility to ensure this call happens inside the same unit of work as the
43+
* aggregate state change.</p>
2844
*
2945
* @param EventRecords $records The records to persist.
3046
* @throws InvalidPayloadJson When a serializer produces an invalid JSON payload.
3147
* @throws DuplicateOutboxEvent When a record with a duplicate id already exists in the outbox.
3248
* @throws DuplicateAggregateVersion When two records share the same aggregate type, id, and aggregate version.
33-
* @throws PayloadSerializerNotConfigured When no serializer supports the event class.
49+
* @throws PayloadSerializerNotConfigured When a translator matched a domain event but no serializer supports the
50+
* produced integration event.
3451
* @throws OutboxRequiresActiveTransaction When called outside an active transaction.
3552
*/
3653
public function push(EventRecords $records): void;

src/Serialization/PayloadSerializer.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@
44

55
namespace TinyBlocks\Outbox\Serialization;
66

7-
use TinyBlocks\BuildingBlocks\Event\EventRecord;
7+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventRecord;
88

99
interface PayloadSerializer
1010
{
1111
/**
12-
* Tells whether this serializer handles the event in the given record.
12+
* Tells whether this serializer handles the integration event in the given record.
1313
*
14-
* @param EventRecord $record The record being serialized.
15-
* @return bool True if this serializer can produce the payload for the event.
14+
* @param IntegrationEventRecord $record The record being serialized.
15+
* @return bool True if this serializer can produce the payload for the integration event.
1616
*/
17-
public function supports(EventRecord $record): bool;
17+
public function supports(IntegrationEventRecord $record): bool;
1818

1919
/**
20-
* Produces the persistent payload for the event in the record.
20+
* Produces the persistent payload for the integration event in the record.
2121
*
22-
* @param EventRecord $record The record being serialized.
22+
* @param IntegrationEventRecord $record The record being serialized.
2323
* @return SerializedPayload The serialized payload.
2424
*/
25-
public function serialize(EventRecord $record): SerializedPayload;
25+
public function serialize(IntegrationEventRecord $record): SerializedPayload;
2626
}

src/Serialization/PayloadSerializerReflection.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44

55
namespace TinyBlocks\Outbox\Serialization;
66

7-
use TinyBlocks\BuildingBlocks\Event\EventRecord;
7+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventRecord;
88

99
final readonly class PayloadSerializerReflection implements PayloadSerializer
1010
{
11-
public function supports(EventRecord $record): bool
11+
public function supports(IntegrationEventRecord $record): bool
1212
{
1313
return true;
1414
}
1515

16-
public function serialize(EventRecord $record): SerializedPayload
16+
public function serialize(IntegrationEventRecord $record): SerializedPayload
1717
{
1818
return SerializedPayload::fromArray(payload: get_object_vars($record->event));
1919
}

src/Serialization/PayloadSerializers.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
namespace TinyBlocks\Outbox\Serialization;
66

7-
use TinyBlocks\BuildingBlocks\Event\EventRecord;
7+
use TinyBlocks\BuildingBlocks\Event\IntegrationEventRecord;
88
use TinyBlocks\Collection\Collection;
99

1010
final class PayloadSerializers extends Collection
1111
{
1212
/**
1313
* Returns the first payload serializer that supports the given record, or null when none matches.
1414
*
15-
* @param EventRecord $record The record whose payload serializer is being resolved.
15+
* @param IntegrationEventRecord $record The record whose payload serializer is being resolved.
1616
* @return PayloadSerializer|null The matching serializer, or null when no element supports the record.
1717
*/
18-
public function findFor(EventRecord $record): ?PayloadSerializer
18+
public function findFor(IntegrationEventRecord $record): ?PayloadSerializer
1919
{
2020
$serializer = $this->findBy(
2121
predicates: static fn(PayloadSerializer $serializer): bool => $serializer->supports(record: $record)

src/Serialization/SerializedPayload.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,6 @@ private function __construct(private string $payload)
1212
{
1313
}
1414

15-
/**
16-
* Creates a SerializedPayload from an associative array, encoding it as JSON.
17-
*
18-
* @param array<int|string, mixed> $payload The associative array to encode as the serialized payload.
19-
* @return SerializedPayload The serialized payload with the JSON-encoded representation.
20-
*/
21-
public static function fromArray(array $payload): SerializedPayload
22-
{
23-
$json = json_encode($payload, JSON_THROW_ON_ERROR);
24-
25-
return new SerializedPayload(payload: $json);
26-
}
27-
2815
/**
2916
* Creates a SerializedPayload from a raw JSON string.
3017
*
@@ -41,6 +28,19 @@ public static function from(string $payload): SerializedPayload
4128
return new SerializedPayload(payload: $payload);
4229
}
4330

31+
/**
32+
* Creates a SerializedPayload from an associative array, encoding it as JSON.
33+
*
34+
* @param array<int|string, mixed> $payload The associative array to encode as the serialized payload.
35+
* @return SerializedPayload The serialized payload with the JSON-encoded representation.
36+
*/
37+
public static function fromArray(array $payload): SerializedPayload
38+
{
39+
$json = json_encode($payload, JSON_THROW_ON_ERROR);
40+
41+
return new SerializedPayload(payload: $json);
42+
}
43+
4444
/**
4545
* Returns the SerializedPayload as its JSON string representation.
4646
*

0 commit comments

Comments
 (0)