Lesson 1: Messaging Concepts
PHP Messages
Not having code for Lesson 1?
git checkout lesson-1
In this first lesson, we will learn fundamental blocks in messaging architecture and we will start building back-end for Shopping System using CQRS.
Ecotone from the ground is built around messaging to provide a simple model for building integration solutions while maintaining the separation of concerns that is essential for producing maintainable, testable code. To achieve that, fundamental messaging blocks are implemented using Enterprise Integration Patterns (EIP). In order to use higher level of abstraction, CQRS and DDD support, we need to briefly get known with those patterns. This will help us understand, how specific parts of Ecotone does work together.

A message is a generic wrapper for any PHP type (scalar, object, compound) with metadata used by the framework while handling that object.
The payload can be of any type, and the headers (metadata) hold commonly required information such as ID, timestamp and framework specific information. Headers are also used for passing values to and from connected transports.
Developers can also store any arbitrary key-value pairs in the headers, to pass needed meta information.
interface Message
{
public function getPayload();
public function getHeaders() : MessageHeaders;
}
Message channel abstracts communication between components. It does allow for sending and receiving messages.
A message channel may follow either point-to-point or publish-subscribe semantics.
With a point-to-point channel, only one consumer can receive each message sent to the channel.
Publish-subscribe channels, broadcast each message to all subscribers on the channel.
Pollable channels extends Message Channels with capability of buffering Messages within a queue. The advantage of buffering is that it allows for throttling the inbound messages and preventing of message loss.
.jpg?alt=media&token=ced81213-df13-4838-ba54-c71c0d459ee1)
Message Endpoints are consumers and producers of messages. Consumer are not necessary asynchronous, as you may build synchronous flow, compound of multiple endpoints.
You will not have to implement them directly, as you should not even have to build messages and invoke or receive message directly from the Message channel. Instead you will be able to focus on your specific domain model with with an implementation based on plain PHP objects. By providing declarative configuration, you can "connect” your domain-specific code to the messaging infrastructure provided by
Ecotone
. If you are familiar with Symfony Messager/Simplebus, for now you can think of Endpoint as a Message Handler, that can be connected to asynchronous or synchronous transport.
The Messaging Gateway encapsulates messaging-specific code (The code required to send or receive a Message) and separates it from the rest of the application code.
It does build from domain specific objects a Message that is send via Message channel.
To not have dependency on the
Ecotone
API — including the gateway class, Ecotone
provides the Gateway as interface. Framework generates a proxy for any interface and internally invokes the gateway methods. By using dependency injection, you can then expose the interface to your business methods.Command/Query/Event buses are implemented using Messaging Gateway.
Great, now when we know fundamental blocks of
Ecotone
and Messaging Architecture, we can start implementing our Shopping System!
If you did not understand something, do not worry, we will see how does it apply in practice in next step. bin/console ecotone:quickstart
"Running example...
Hello World
Good job, scenario ran with success!"
If yes and this command does return above output, then we are ready to go.
Symfony
Laravel
Lite
https://github.com/ecotoneframework/quickstart-symfony/blob/lesson-1/src/EcotoneQuickstart.php
Go to "src/EcotoneQuickstart.php"
# This class is autoregistered using Symfony Autowire
https://github.com/ecotoneframework/quickstart-laravel/blob/lesson-1/app/EcotoneQuickstart.php
Go to "app/EcotoneQuickstart.php"
# This class is autoregistered using Laravel Autowire
https://github.com/ecotoneframework/quickstart-lite/blob/lesson-1/src/EcotoneQuickstart.php
Go to "src/EcotoneQuickstart.php"
# This class is autoregistered using PHP-DI
Thanks to that, we will avoid writing configuration files for service registrations during this tutorial.
And we will be able to fully focus on what can
Ecotone
provides to us. <?php
namespace App;
class EcotoneQuickstart
{
public function run() : void
{
echo "Hello World";
}
}
We will start by creating
Command Handler
.
Command Handler is place where we will put our business logic.
Let's create namespace App\Domain\Product
and inside RegisterProductCommand,
command for registering new product:<?php
namespace App\Domain\Product;
class RegisterProductCommand
{
private int $productId;
private int $cost;
public function __construct(int $productId, int $cost)
{
$this->productId = $productId;
$this->cost = $cost;
}
public function getProductId() : int
{
return $this->productId;
}
public function getCost() : int
{
return $this->cost;
}
}
private int $productId;
Describing types, will help us in later lessons with automatic conversion. Just remember right now, that it's worth to keep the types defined.
Let's register a Command Handler now by creating class
App\Domain\Product\ProductService
<?php
namespace App\Domain\Product;
use Ecotone\Modelling\Attribute\CommandHandler;
class ProductService
{
private array $registeredProducts = [];
#[CommandHandler]
public function register(RegisterProductCommand $command) : void
{
$this->registeredProducts[$command->getProductId()] = $command->getCost();
}
}
Ecotone
will read method declaration and base on the first parameter type hint will know that this CommandHandler
is responsible for handling RegisterProductCommand.
PHP 8 Introduced neat feature called
Attributes
You may now know it under the name Annotations. Annotations were userland solution and Attributes are built in into language feature. Ecotone make use of it to decouple your code from the framework.
Thank you Benjamin Eberlei and Martin Schroder for introducing this feature :)
#[ClassReference]
is a special Attribute it informs Ecotone
how this service is registered in Depedency Container
. As a default it takes the class name, which is compatible with auto-wiring system. If ProductService
would be registered in Dependency Container
as productService
, we would do this:#[ClassReference("productService")
class ProductService
We also need possibility to query our
ProductService
for registered products.
This is the role of Query Handlers
. They do query the state and return it to us.
Let's starts with GetProductPriceQuery
class. This query will tell us what is the price of specific product.<?php
namespace App\Domain\Product;
class GetProductPriceQuery
{
private int $productId;
public function __construct(int $productId)
{
$this->productId = $productId;
}
public function getProductId() : int
{
return $this->productId;
}
}
We also need Handler for this query. Let's add
Query Handler
to the ProductService
<?php
namespace App\Domain\Product;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;
class ProductService
{
private array $registeredProducts = [];
#[CommandHandler]
public function register(RegisterProductCommand $command) : void
{
$this->registeredProducts[$command->getProductId()] = $command->getCost();
}
#[QueryHandler]
public function getPrice(GetProductPriceQuery $query) : int
{
return $this->registeredProducts[$query->getProductId()];
}
}
Some CQRS frameworks expects Handlers be defined as a class, not method. This is somehow limiting and producing a lot of boilerplate.
Ecotone
does allow for full flexibility, if you want to have only one handler per class, so be it, if more just annotate next methods.Laravel
Symfony
Lite
# As default auto wire of Laravel creates new service instance each time
# service is requested from Depedency Container, we need to register
# ProductService as singleton.
# Go to bootstrap/QuickStartProvider.php and register our ProductService
namespace Bootstrap;
use App\Domain\Product\ProductService;
use Illuminate\Support\ServiceProvider;
class QuickStartProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ProductService::class, function(){
return new ProductService();
});
}
(...)
Everything is set up by the framework, please continue...
Everything is set up, please continue...
It's time to call our endpoints.
You may remember that endpoints need to be connected using Message Channels and we did not do anything like this yet. Thankfully
Ecotone
does create synchronous channels for us, so we don't need to bother about it. In case of Query And Command Handlers, point to point channels are created automatically.
You will learn how to easily replace them with asynchronous channels in next lessons.
We need to create
Message
and send it to correct Message Channel
.
In order to do it we will use Messaging Gateway. Message Gateways are responsible for creating Message
from given parameters and send it to the correct channel
.
For sending Commands we will use Command Bus Gateway
.
For sending Queries we will use Query Bus Gateway
.
Let's inject and call Query and Command bus into EcotoneQuickstart class. <?php
namespace App;
use App\Domain\Product\GetProductPriceQuery;
use App\Domain\Product\RegisterProductCommand;
use Ecotone\Modelling\CommandBus;
use Ecotone\Modelling\QueryBus;
class EcotoneQuickstart
{
private CommandBus $commandBus;
private QueryBus $queryBus;
// 1
public function __construct(CommandBus $commandBus, QueryBus $queryBus)
{
$this->commandBus = $commandBus;
$this->queryBus = $queryBus;
}
public function run() : void
{
// 2
$this->commandBus->send(new RegisterProductCommand(1, 100));
// 3
echo $this->queryBus->send(new GetProductPriceQuery(1));
}
}
- 1.
Gateways
are auto registered in Dependency Container and available for auto-wire.Ecotone
comes with few set up Gateways. Command and Query buses are available instantly to you. You will be able to extend them or create your own ones, if needed. - 2.We are sending command
RegisterProductCommand
to theCommandHandler
we registered before. - 3.Same as above, but in that case we are sending query
GetProductPriceQuery
to theQueryHandler
If you run our testing command now, you should see the result.
bin/console ecotone:quickstart
Running example...
100
Good job, scenario ran with success!
We want to notify, when new product is registered in the system.
In order to do it, we will make use of
Event Bus Gateway
which can publish events.
Let's start by creating ProductWasRegisteredEvent.
<?php
namespace App\Domain\Product;
class ProductWasRegisteredEvent
{
private int $productId;
public function __construct(int $productId)
{
$this->productId = $productId;
}
public function getProductId() : int
{
return $this->productId;
}
}
As you can see
Ecotone
does not really care what class Command/Query/Event is. It does not require to implement any interfaces neither prefix or suffix the class name.
In fact commands, queries and events can be of any type and we will see it in next Lessons.
In the tutorial however we use Command/Query/Event suffixes to clarify the distinction.Let's inject
EventBus
into our CommandHandler
in order to publish ProductWasRegisteredEvent
after product was registered.use Ecotone\Modelling\EventBus;
#[CommandHandler]
public function register(RegisterProductCommand $command, EventBus $eventBus) : void
{
$this->registeredProducts[$command->getProductId()] = $command->getCost();
$eventBus->publish(new ProductWasRegisteredEvent($command->getProductId()));
}
You may wonder, how
EventBus
is injected into the Command Handler's method.
Ecotone
does control method invocation for endpoints, if you have type hinted for specific class, framework will look in Dependency Container for specific service in order to inject it automatically. It does work similar to auto-wire system. If you want to know more, check chapter about Method Invocation.Now, when our event is published, whenever new product is registered, we want to listen for it and notify. Let's create new class and annotate method with
EventHandler
. <?php
namespace App\Domain\Product;
use Ecotone\Modelling\Attribute\EventHandler;
class ProductNotifier
{
#[EventHandler] // 1
public function notifyAbout(ProductWasRegisteredEvent $event) : void
{
echo "Product with id {$event->getProductId()} was registered!\n";
}
}
- 1.
EventHandler
tellsEcotone
to handle specific event based on declaration type hint, just like withCommandHandler.
AsCommands
arepoint to point,
which means they are targeting one Handler,Events
on other side arepublish subscribe
, which means, there may be multiple Handlers for specific event.
You may use interfaces or abstract classes for your first parameter type hint.
You may even type hint using
Union Types
(ProductWasRegistered|ProductWasSent).
This allows for listening to more than one event within specific method, which in result decrease boilerplate in the business code.Ecotone will resolve connection between published event and Event Handler.
If you run our testing command now, you should see the result.
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
Great, we have just finished Lesson 1.
In this lesson we have learned basic of Messaging and CQRS.
That was the longest lesson, as it had to introduce new concepts. Incoming lessons will be much shorter :)
We are ready for Lesson 2!
Last modified 2yr ago