placeOrder - Place order method make use of QueryBus to retrieve cost of each ordered product.
You could find out, that we are not using application/json for product.getCost query, ecotone/jms-converter can handle array transformation, so we do not need to use json.
You could inject service into placeOrder that will hide QueryBus implementation from the domain, or you may get this data from data store directly. We do not want to complicate the solution now, so we will use QueryBus directly.
We do not need to change or add new Repository, as our exiting one can handle any new aggregate arriving in our system.
bin/console ecotone:quickstartRunning example...Start transactionProduct with id 1 was registered!Commit transactionStart transactionProduct with id 2 was registered!Commit transactionStart transactionCommit transaction400Good job, scenario ran with success!
We want to be sure, that we do not lose any order, so we will register our order.place Command Handler to run asynchronously using RabbitMQ now.
Let's start by adding extension to Ecotone, that can handle RabbitMQ:
composer require ecotone/amqp
We also need to add our ConnectionFactory to our Dependency Container.
# Add AmqpConnectionFactory in config/services.yamlservices: _defaults: autowire: true autoconfigure: true App\: resource: '../src/*' exclude: '../src/{Kernel.php}' Bootstrap\: resource: '../bootstrap/*' exclude: '../bootstrap/{Kernel.php}'# You need to have RabbitMQ instance running on your localhost, or change DSN Enqueue\AmqpExt\AmqpConnectionFactory: class: Enqueue\AmqpExt\AmqpConnectionFactory arguments:-"amqp+lib://guest:guest@localhost:5672//"
# Add AmqpConnectionFactory in config/services.yamlservices: _defaults: autowire: true autoconfigure: true App\: resource: '../src/*' exclude: '../src/{Kernel.php}' Bootstrap\: resource: '../bootstrap/*' exclude: '../bootstrap/{Kernel.php}'# docker-compose.yml has RabbitMQ instance defined. It will be working without# addtional configuration Enqueue\AmqpExt\AmqpConnectionFactory: class: Enqueue\AmqpExt\AmqpConnectionFactory arguments:-"amqp+lib://guest:guest@rabbitmq:5672//"
# Add AmqpConnectionFactory in bootstrap/QuickStartProvider.phpnamespaceBootstrap;useIlluminate\Support\ServiceProvider;useEnqueue\AmqpExt\AmqpConnectionFactory;classQuickStartProviderextendsServiceProvider{publicfunctionregister() {$this->app->singleton(AmqpConnectionFactory::class,function () {returnnewAmqpConnectionFactory("amqp+lib://guest:guest@localhost:5672//"); }); }(...)
# Add AmqpConnectionFactory in bootstrap/QuickStartProvider.phpnamespaceBootstrap;useIlluminate\Support\ServiceProvider;useEnqueue\AmqpExt\AmqpConnectionFactory;classQuickStartProviderextendsServiceProvider{publicfunctionregister() {$this->app->singleton(AmqpConnectionFactory::class,function () {returnnewAmqpConnectionFactory("amqp+lib://guest:guest@rabbitmq:5672//"); }); }(...)
# Add AmqpConnectionFactory in bin/console.php// add additional service in containerpublicfunction__construct(){$this->container =newContainer();$this->container->set(Enqueue\AmqpExt\AmqpConnectionFactory::class,newEnqueue\AmqpExt\AmqpConnectionFactory("amqp+lib://guest:guest@localhost:5672//"));}
# Add AmqpConnectionFactory in bin/console.php // add additional service in containerpublicfunction__construct(){$this->container =newContainer();$this->container->set(Enqueue\AmqpExt\AmqpConnectionFactory::class,newEnqueue\AmqpExt\AmqpConnectionFactory("amqp+lib://guest:guest@rabbitmq:5672//"));}
We register our AmqpConnectionFactory under the class name Enqueue\AmqpLib\AmqpConnectionFactory. This will help Ecotone resolve it automatically, without any additional configuration.
Let's add our first AMQP Backed Channel (RabbitMQ Channel), in order to do it, we need to create our first Application Context.
Application Context is a non-constructor class, responsible for extending Ecotone with extra configurations, that will help the framework act in a specific way. In here we want to tell Ecotone about AMQP Channel with specific name.
Let's create new class App\Infrastructure\MessagingConfiguration.
We do it by adding Asynchronous annotation with channelName used for asynchronous endpoint.
Endpoints using Asynchronous are required to have endpointId defined, the name can be anything as long as it's not the same as routing key (order.place).
We have new asynchronous endpoint available orders. Name comes from the message channel name.
You may wonder why it is not place_order_endpoint, it's because via single asynchronous channel we can handle multiple endpoints, if needed. This is further explained in asynchronous section.
Let's change orderId in our testing command, so we can place new order.
After running our testing command bin/console ecotone:quickstartwe should get an exception:
AggregateNotFoundException: Aggregate App\Domain\Order\Order:getTotalPrice was not found for indentifie rs {"orderId":990}
That's fine, we have registered order.place Command Handler to run asynchronously, so we need to run our asynchronous endpoint in order to handle Command Message. If you did not received and exception, it's probably because orderId was not changed and we already registered such order.
Let's run our asynchronous endpoint
Like we can see, it ran our Command Handler and placed the order.
We can change our testing command to run only Query Handlerand check, if the order really exists now.
bin/console ecotone:quickstart -vvvRunning example...400Good job, scenario ran with success!
There is one thing we can change.
As in asynchronous scenario we may not have access to the context of executor to enrich the message,, we can change our AddUserIdService Interceptor to perform the action before sending it to asynchronous channel.
This Interceptor is registered as Before Interceptor which is before execution of our Command Handler, but what we want to achieve is, to call this interceptor before message will be send to the asynchronous channel. For this there is Presend Interceptor available.
Change Before annotation to Presend annotation and we are done.
Ecotone will do it best to handle serialization and deserialization of your headers.
Now if non-administrator will try to execute this, exception will be thrown, before the Message will be put to the asynchronous channel. Thanks to Presend interceptor, we can validate messages, before they will go asynchronous, to prevent sending incorrect messages.
The final code is available as lesson-7:
git checkout lesson-7
We made it through, Congratulations!
We have successfully registered asynchronous Command Handler and safely placed the order.
We have finished last lesson. You may now apply the knowledge in real project or check more advanced usages starting here Modelling Overview.