Skip to content

Commit c31024a

Browse files
committed
use aggregate version from its property for command handlers
1 parent 82ee991 commit c31024a

19 files changed

+371
-11
lines changed

packages/Ecotone/src/Modelling/AggregateFlow/CallAggregate/CallAggregateResultToMessageConverter.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,17 @@ public function convertToMessage(Message $requestMessage, mixed $result): ?Messa
3939

4040
if ($this->isCommandHandler) {
4141
$calledAggregate = $requestMessage->getHeaders()->containsKey(AggregateMessage::CALLED_AGGREGATE_INSTANCE) ? $requestMessage->getHeaders()->get(AggregateMessage::CALLED_AGGREGATE_INSTANCE) : null;
42-
$versionBeforeHandling = $requestMessage->getHeaders()->containsKey(AggregateMessage::TARGET_VERSION) ? $requestMessage->getHeaders()->get(AggregateMessage::TARGET_VERSION) : null;
4342

44-
if (is_null($versionBeforeHandling) && $this->aggregateVersionProperty) {
45-
if ($this->isFactoryMethod) {
46-
$versionBeforeHandling = 0;
47-
} else {
48-
$versionBeforeHandling = $this->propertyReaderAccessor->getPropertyValue(PropertyPath::createWith($this->aggregateVersionProperty), $calledAggregate);
49-
$versionBeforeHandling = is_null($versionBeforeHandling) ? 0 : $versionBeforeHandling;
50-
}
51-
52-
$resultMessage = $resultMessage->setHeader(AggregateMessage::TARGET_VERSION, $versionBeforeHandling);
43+
if ($this->isFactoryMethod) {
44+
$versionBeforeHandling = 0;
45+
} elseif ($this->aggregateVersionProperty) {
46+
$versionBeforeHandling = $this->propertyReaderAccessor->getPropertyValue(PropertyPath::createWith($this->aggregateVersionProperty), $calledAggregate);
47+
$versionBeforeHandling = is_null($versionBeforeHandling) ? 0 : $versionBeforeHandling;
48+
} else {
49+
$versionBeforeHandling = $requestMessage->getHeaders()->containsKey(AggregateMessage::TARGET_VERSION) ? $requestMessage->getHeaders()->get(AggregateMessage::TARGET_VERSION) : null;
5350
}
51+
52+
$resultMessage = $resultMessage->setHeader(AggregateMessage::TARGET_VERSION, $versionBeforeHandling);
5453
}
5554

5655
if (! is_null($result)) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
class Audit
6+
{
7+
public function __construct(
8+
public string $auditId,
9+
public ?Certificate $certificate = null,
10+
) {
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Modelling\Attribute\NamedEvent;
6+
7+
#[NamedEvent(self::NAME)]
8+
class AuditConducted
9+
{
10+
public const NAME = 'cycle.auditConducted';
11+
12+
public function __construct(public string $cycleId, public string $auditId)
13+
{
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
class Certificate
6+
{
7+
public function __construct(public string $certificateId)
8+
{
9+
}
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Modelling\Attribute\NamedEvent;
6+
7+
#[NamedEvent(self::NAME)]
8+
class CertificateIssued
9+
{
10+
public const NAME = 'cycle.certificateIssued';
11+
12+
public function __construct(public string $cycleId, public string $certificateId)
13+
{
14+
}
15+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Messaging\Attribute\Parameter\Header;
6+
use Ecotone\Messaging\Attribute\Parameter\Payload;
7+
use Ecotone\Modelling\Attribute\CommandHandler;
8+
use Ecotone\Modelling\Attribute\EventSourcingHandler;
9+
use Ecotone\Modelling\Attribute\EventSourcingSaga;
10+
use Ecotone\Modelling\Attribute\Identifier;
11+
use Ecotone\Modelling\Attribute\QueryHandler;
12+
use Ecotone\Modelling\WithAggregateVersioning;
13+
use Ecotone\Modelling\WithEvents;
14+
15+
#[EventSourcingSaga(withInternalEventRecorder: true)]
16+
class Cycle
17+
{
18+
use WithEvents;
19+
use WithAggregateVersioning;
20+
21+
#[Identifier]
22+
private string $cycleId;
23+
24+
/**
25+
* @var list<string>
26+
*/
27+
private array $audits = [];
28+
29+
/**
30+
* @var list<string>
31+
*/
32+
private array $certificates = [];
33+
34+
#[CommandHandler(
35+
routingKey: 'cycle.submitAnAudit',
36+
outputChannelName: 'cycle.validateAnAudit',
37+
identifierMetadataMapping: ['cycleId' => 'cycleId'],
38+
)]
39+
public static function startCycleBySubmittingAnAudit(
40+
#[Header('cycleId')] string $cycleId
41+
): self {
42+
$cycle = new self();
43+
$cycle->recordThat(new CycleStarted($cycleId));
44+
45+
return $cycle;
46+
}
47+
48+
#[CommandHandler(
49+
routingKey: 'cycle.submitAnAudit',
50+
outputChannelName: 'cycle.validateAnAudit',
51+
identifierMetadataMapping: ['cycleId' => 'cycleId'],
52+
)]
53+
public function submitAnAudit(#[Header('audit')] Audit $audit): Audit
54+
{
55+
return $audit;
56+
}
57+
58+
#[CommandHandler(
59+
routingKey: 'cycle.validateAnAudit',
60+
outputChannelName: 'cycle.conductAnAudit'
61+
)]
62+
public function validateAnAudit(#[Header('audit')] Audit $audit): Audit
63+
{
64+
// some validations
65+
66+
return $audit;
67+
}
68+
69+
#[CommandHandler(
70+
routingKey: 'cycle.conductAnAudit',
71+
outputChannelName: 'cycle.issueACertificate'
72+
)]
73+
public function conductAnAudit(#[Payload] Audit $audit): ?Certificate
74+
{
75+
$this->recordThat(new AuditConducted($this->cycleId, $audit->auditId));
76+
77+
return $audit->certificate;
78+
}
79+
80+
#[CommandHandler(
81+
routingKey: 'cycle.issueACertificate',
82+
)]
83+
public function issueACertificate(#[Payload] Certificate $certificate): void
84+
{
85+
$this->recordThat(new CertificateIssued($this->cycleId, $certificate->certificateId));
86+
}
87+
88+
#[QueryHandler('cycle.conductedAudits')]
89+
public function conductedAudits(): array
90+
{
91+
return $this->audits;
92+
}
93+
94+
#[QueryHandler('cycle.issuedCertificates')]
95+
public function issuedCertificates(): array
96+
{
97+
return $this->certificates;
98+
}
99+
100+
#[EventSourcingHandler]
101+
public function applyCycleStarted(CycleStarted $event): void
102+
{
103+
$this->cycleId = $event->cycleId;
104+
}
105+
106+
#[EventSourcingHandler]
107+
public function applyAuditConducted(AuditConducted $event): void
108+
{
109+
$this->audits[] = $event->auditId;
110+
}
111+
112+
#[EventSourcingHandler]
113+
public function applyCertificateIssued(CertificateIssued $event): void
114+
{
115+
$this->certificates[] = $event->certificateId;
116+
}
117+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Messaging\Attribute\BusinessMethod;
6+
use Ecotone\Messaging\Attribute\Parameter\Header;
7+
use Ecotone\Modelling\Attribute\Identifier;
8+
9+
interface CycleGateway
10+
{
11+
#[BusinessMethod('cycle.submitAnAudit')]
12+
public function submitAnAudit(#[Header('cycleId')] $cycleId, #[Header('audit')] Audit $audit): void;
13+
14+
#[BusinessMethod('cycle.conductedAudits')]
15+
public function conductedAudits(#[Identifier] $cycleId): array;
16+
17+
#[BusinessMethod('cycle.issuedCertificates')]
18+
public function issuedCertificates(#[Identifier] $cycleId): array;
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Modelling\Attribute\NamedEvent;
6+
7+
#[NamedEvent(self::NAME)]
8+
class CycleStarted
9+
{
10+
public const NAME = 'cycle.started';
11+
12+
public function __construct(public string $cycleId)
13+
{
14+
}
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow;
4+
5+
use Ecotone\Messaging\Attribute\Converter;
6+
7+
class EventsConverters
8+
{
9+
#[Converter]
10+
public function convertFromCycleStarted(CycleStarted $event): array
11+
{
12+
return [
13+
'cycleId' => $event->cycleId,
14+
];
15+
}
16+
17+
#[Converter]
18+
public function convertToCycleStarted(array $payload): CycleStarted
19+
{
20+
return new CycleStarted($payload['cycleId']);
21+
}
22+
23+
#[Converter]
24+
public function convertFromAuditConducted(AuditConducted $event): array
25+
{
26+
return [
27+
'cycleId' => $event->cycleId,
28+
'auditId' => $event->auditId,
29+
];
30+
}
31+
32+
#[Converter]
33+
public function convertToAuditConducted(array $payload): AuditConducted
34+
{
35+
return new AuditConducted($payload['cycleId'], $payload['auditId']);
36+
}
37+
38+
#[Converter]
39+
public function convertFromCertificateIssued(CertificateIssued $event): array
40+
{
41+
return [
42+
'cycleId' => $event->cycleId,
43+
'certificateId' => $event->certificateId,
44+
];
45+
}
46+
47+
#[Converter]
48+
public function convertToCertificateIssued(array $payload): CertificateIssued
49+
{
50+
return new CertificateIssued($payload['cycleId'], $payload['certificateId']);
51+
}
52+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Test\Ecotone\EventSourcing\Integration;
4+
5+
use Ecotone\Lite\EcotoneLite;
6+
use Ecotone\Messaging\Config\ModulePackageList;
7+
use Ecotone\Messaging\Config\ServiceConfiguration;
8+
use Test\Ecotone\EventSourcing\EventSourcingMessagingTestCase;
9+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\Audit;
10+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\AuditConducted;
11+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\Certificate;
12+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\CertificateIssued;
13+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\Cycle;
14+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\CycleGateway;
15+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\CycleStarted;
16+
use Test\Ecotone\EventSourcing\Fixture\StatefulEventSourcedWorkflow\EventsConverters;
17+
18+
class StatefulEventSourcedWorkflowTest extends EventSourcingMessagingTestCase
19+
{
20+
public function test_stateful_event_sourced_workflow(): void
21+
{
22+
$ecotone = EcotoneLite::bootstrapFlowTestingWithEventStore(
23+
classesToResolve: [
24+
Cycle::class,
25+
CycleGateway::class,
26+
EventsConverters::class,
27+
],
28+
containerOrAvailableServices: [
29+
new EventsConverters(),
30+
self::getConnectionFactory(),
31+
],
32+
configuration: ServiceConfiguration::createWithDefaults()
33+
->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::EVENT_SOURCING_PACKAGE, ModulePackageList::DBAL_PACKAGE]))
34+
->withNamespaces(['Test\Ecotone\Modelling\Fixture\StatefulEventSourcedWorkflow']),
35+
pathToRootCatalog: __DIR__ . '/../../',
36+
runForProductionEventStore: true,
37+
);
38+
39+
$cycleGateway = $ecotone->getGateway(CycleGateway::class);
40+
41+
$cycleGateway->submitAnAudit(cycleId: 'foo', audit: new Audit('123', new Certificate('234')));
42+
43+
self::assertEquals(
44+
[
45+
new CycleStarted('foo'),
46+
new AuditConducted('foo', '123'),
47+
new CertificateIssued('foo', '234'),
48+
],
49+
$ecotone->getRecordedEvents()
50+
);
51+
52+
$cycleGateway->submitAnAudit(cycleId: 'foo', audit: new Audit('678'));
53+
54+
self::assertEquals(
55+
[
56+
new AuditConducted('foo', '678'),
57+
],
58+
$ecotone->getRecordedEvents()
59+
);
60+
61+
self::assertEquals(['123', '678'], $cycleGateway->conductedAudits('foo'));
62+
self::assertEquals(['234'], $cycleGateway->issuedCertificates('foo'));
63+
}
64+
}

packages/Symfony/config/services.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ services:
1616
# this creates a service per class whose id is the fully-qualified class name
1717
Fixture\:
1818
resource: '../tests/Fixture/*'
19+
exclude:
20+
- '../tests/Fixture/User/User.php'
1921

2022
Fixture\Car\CarService:
2123
public: true

packages/Symfony/tests/phpunit/AmqpMessengerIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Ecotone\SymfonyBundle\Messenger\SymfonyMessengerMessageChannelBuilder;
1212
use Fixture\MessengerConsumer\AmqpExampleCommand;
1313
use Fixture\MessengerConsumer\AmqpMessengerAsyncCommandHandler;
14+
use PHPUnit\Framework\Attributes\After;
1415
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
1516

1617
/**
@@ -25,6 +26,11 @@ final class AmqpMessengerIntegrationTest extends WebTestCase
2526
private string $channelName = 'amqp_async';
2627
private FlowTestSupport $messaging;
2728

29+
protected function tearDown(): void
30+
{
31+
restore_exception_handler();
32+
}
33+
2834
protected function setUp(): void
2935
{
3036
$this->messaging = EcotoneLite::bootstrapFlowTesting(

packages/Symfony/tests/phpunit/EcotoneLiteWithSymfonyContainerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
*/
2424
final class EcotoneLiteWithSymfonyContainerTest extends KernelTestCase
2525
{
26+
protected function tearDown(): void
27+
{
28+
restore_exception_handler();
29+
}
30+
2631
public function test_when_messaging_configured_in_container_replacing_it_with_test_one()
2732
{
2833
$ecotoneTestSupport = EcotoneLite::bootstrapFlowTesting(

0 commit comments

Comments
 (0)