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
11 changes: 4 additions & 7 deletions packages/Ecotone/src/Modelling/Config/AggregrateModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,29 +196,26 @@ private function initChannelsToBridge(): void
// All channels on this route are factory or action channels, continue normal processing
continue;
}
if (count($staticChannels) > 1 || count($actionChannels) > 1) {
throw ConfigurationException::create("Message Handlers on Aggregate and Saga can be used either for single factory method and single action method together, or for multiple actions methods in {$staticRegistration->getClassName()}");
}
// Exactly one factory and one action channel, register a bridge
// register a bridge
if ($bridgeForThisFactoryMethod === null) {
$bridgeForThisFactoryMethod = [
'factoryChannel' => reset($staticChannels),
'actionChannel' => reset($actionChannels),
'actionChannels' => $actionChannels,
'routes' => [$routedKey],
];
} else {
// If we already have a bridge for this factory method, add the route to it
$bridgeForThisFactoryMethod['routes'][] = $routedKey;
Assert::isTrue(
reset($staticChannels) === $bridgeForThisFactoryMethod['factoryChannel']
&& reset($actionChannels) === $bridgeForThisFactoryMethod['actionChannel'],
&& $actionChannels === $bridgeForThisFactoryMethod['actionChannels'],
"Trying to register multiple factory methods for {$staticRegistration->getClassName()} under same route {$routedKey}"
);
}
}
if ($bridgeForThisFactoryMethod !== null) {
$this->channelsToBridge[$bridgeForThisFactoryMethod['factoryChannel']] = $channelToRegistrations[$bridgeForThisFactoryMethod['actionChannel']];
$this->channelsToCancel[] = $bridgeForThisFactoryMethod['actionChannel'];
$this->channelsToCancel += $bridgeForThisFactoryMethod['actionChannels'];
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers;

use Ecotone\Messaging\Attribute\Parameter\Payload;
use Ecotone\Modelling\Attribute\EventHandler;
use Ecotone\Modelling\Attribute\Identifier;
use Ecotone\Modelling\Attribute\Saga;

#[Saga]
class InvoiceOrder
{
private array $products;

public function __construct(
#[Identifier] private string $debtorId,
) {
$this->products = [];
}

public function products(): array
{
return $this->products;
}

#[EventHandler(endpointId: 'createWhenProductBecameBillable', identifierMapping: ['debtorId' => 'payload.debtor'])]
public static function createWhenProductBecameBillable(
#[Payload] ProductBecameBillable $event
): self {
$invoiceOrder = new self($event->debtor);
$invoiceOrder->products[] = $event->productId;

return $invoiceOrder;
}

#[EventHandler(endpointId: 'createWhenDebtorWasChanged', identifierMapping: ['debtorId' => 'payload.newDebtor'])]
public static function createWhenDebtorWasChanged(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we would go this path, then there should be still guard that there is only one factory:action for given identifier mapping (either payload or headers).
Otherwise it become confusing whatever they do relate to each other or not, and then it's not obvious if we should create instance or redirect to action.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dgafka yes, right now Ecotone guards factory:action combination and does not take identifier mapping into consideration. I think adding this into account does not introduce BC as it won't affect cases where there is already one factory:action.

#[Payload] ProductDebtorWasChanged $event
): self {
$invoiceOrder = new self($event->newDebtor);
$invoiceOrder->products[] = $event->productId;

return $invoiceOrder;
}

#[EventHandler(endpointId: 'removeProductWhenDebtorWasChanged', identifierMapping: ['debtorId' => 'payload.oldDebtor'])]
public function removeProductWhenDebtorWasChanged(
#[Payload] ProductDebtorWasChanged $event
): void {
unset($this->products[$event->productId]);
}

#[EventHandler(endpointId: 'addProductWhenDebtorWasChanged', identifierMapping: ['debtorId' => 'payload.newDebtor'])]
public function addProductWhenDebtorWasChanged(
#[Payload] ProductDebtorWasChanged $event
): void {
if (!in_array($event->productId, $this->products, true)) {
$this->products[] = $event->productId;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers;

class ProductBecameBillable
{
public function __construct(
public string $productId,
public string $debtor,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers;

class ProductDebtorWasChanged
{
public function __construct(
public string $productId,
public string $oldDebtor,
public string $newDebtor,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Modelling\Unit;

use Ecotone\Lite\EcotoneLite;
use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder;
use Ecotone\Messaging\Config\ServiceConfiguration;
use PHPUnit\Framework\TestCase;
use Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers\InvoiceOrder;
use Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers\ProductBecameBillable;
use Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers\ProductDebtorWasChanged;

class MultipleMessageHandlersTest extends TestCase
{
public function test_multiple_message_handlers(): void
{
$ecotone = EcotoneLite::bootstrapFlowTesting(
configuration: ServiceConfiguration::createWithDefaults()
->withNamespaces(['Test\Ecotone\Modelling\Fixture\MultipleMessageHandlers'])
->withExtensionObjects([
SimpleMessageChannelBuilder::createQueueChannel('invoiceOrder')
])
,
);

$ecotone->publishEvent(new ProductBecameBillable(productId: 'product-1', debtor: 'debtor-1'));

self::assertEquals(['product-1'], $ecotone->getAggregate(InvoiceOrder::class, 'debtor-1')->products());

$ecotone->publishEvent(new ProductDebtorWasChanged(productId: 'product-1', oldDebtor: 'debtor-1', newDebtor: 'debtor-2'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like the design here is that, Product is stating that Debtor was changed, but Debtor itself also hold the list and need to be synchronized (even so it's not source of truth for it, because Product is?)

I wonder if this could be modelled differently?
For example Debtor (debtor-1) would record an event ProductDebtorWasChanged (which would unassign the product), and then there would be single Event Handler on the Debtor, that would be called based on newDebtor id. This way Product itself is not really holding Debtor information and that info is not in need to be synced there.

Do you have other scenarios where we would like to trigger same Aggregate but more than one instance, based on single Event?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dgafka Example here is simplified.

In real example, Products Debtor is "calculated" based on changes in Product itself so it is changed for the Product, not for the Debtor. Later we need to assign Product in correct Invoice. That logic is not 100% applicable for Product nor Debtor so we want to introduce InvoiceOrder as something in the middle that would keep that coupling away from both Product and Debtor.

This is a typical problem for us whenever we have to "move" something. When identifier of an "orchestrator" is hard to define we'd introduce technical ACL which would do all the complex logic but this is not that efficient and hard to track.


self::assertEquals([], $ecotone->getAggregate(InvoiceOrder::class, 'debtor-1')->products());
self::assertEquals(['product-1'], $ecotone->getAggregate(InvoiceOrder::class, 'debtor-1')->products());
}
}
Loading