Lesson 1: Messaging Concepts

PHP Messages

Not having code for Lesson 1?

git checkout lesson-1

Key concepts / background

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.

Frameworks such as NServiceBus, Axon Framework, and Spring Integration have transformed messaging capabilities across different programming languages, paving the way for the development of robust and scalable systems. Entering this innovative arena, Ecotone introduces these advanced concepts to PHP, taking cues from its predecessors to significantly enhance messaging and system design within PHP projects.

Message

A Message is a data record containing of Payload and Message Headers (Metadata). The Payload can be of any PHP type (scalar, object, compound), and the Headers hold commonly required information such as ID, timestamp and framework specific information. Developers can also store any arbitrary key-value pairs in the headers, to pass additional meta information.

interface Message
{
    public function getPayload();

    public function getHeaders() : MessageHeaders;
}

Message Channel

Message channel abstracts communication between components. It does allow for sending and receiving messages. This decouples components from knowledge about the transport layer, as it's encapsulated within the Message Channel.

Message Endpoint

Message Endpoints are consumers and producers of messages. Consumer are not necessary asynchronous, as you may build synchronous flow, compound of multiple endpoints.

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.

Messaging Gateway

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 take your domain specific objects an convert them into a Message that is send via Message channel. To not have dependency on the Messaging Framework Ecotone provides the Gateway as interface and generates proxy class for it.

Command/Query/Event buses are implemented using Messaging Gateway.

Business Logic

You will not have to implement Messages, Message Channels or Message Endpoints directly, as those are lower level concepts. Instead you will be able to focus on your specific domain logic with an implementation based on plain PHP objects. By providing declarative configuration we will be able to connect domain-specific code to the messaging system.

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.

To The Code!

Do you remember this command from Setup part?

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.

https://github.com/ecotoneframework/quickstart-symfony/blob/lesson-1/src/EcotoneQuickstart.php
Go to "src/EcotoneQuickstart.php"

# This class is autoregistered using Symfony Autowire

this method will be run, whenever we executeecotone:quickstart. This class is auto-registered using auto-wire system, both Symfony and Laravel provides this great feature. For Lite clean and easy to use PHP-DI is taken.

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";
    }
}

Command Handler - Endpoint

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();
    }
}

First thing worth noticing is #[CommandHandler]. This attribute marks our register method in ProductService as an Endpoint, from that moment it can be found by Ecotone.

Ecotone will read method declaration and base on the first parameter type hint will know that this CommandHandler is responsible for handling RegisterProductCommand.

Ecotone make use Attributes to provide declarative configuration. In most of the scenarios we will be stating "what" we want to achieve with Attributes, and Ecotone will take care of "how". This way our application logic will stay decoupled from the technical concerns.

#[ClassReference] is a special Attribute it informs Ecotonehow 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 use the Attribute this way:

#[ClassReference("productService")
class ProductService

Query Handler - Endpoint

We also need the possibility to query ProductService for registered products and this is the role of Query Handlers. 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.

# 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();
        });
    }
(...)

Command and Query Bus - Gateways

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, therefore we don't need to bother about it.

Synchronous channels are created automatically for our Message Handlers. We will learn easily can they be replaced 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. For sending Queries we will use Query Bus. 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 Gateways out of the box like Command and Query buses. You will be able to extend them or create your own ones, if needed.

  2. We are sending command RegisterProductCommand to the CommandHandler we registered before.

  3. Same as above, but in that case we are sending query GetProductPriceQuery to the QueryHandler

As you can see we have not defined any Message Channels, Messages or Gateways, yet they all being used in this scenario. This is can happen because Ecotone is using high level abstractions so our daily development is focused on the business side of the code, yet under the hood is using powerful Messaging capabilities.

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!

Event Handling

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()));
}

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. In this scenario it injects for us Event Bus. 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 subscribe to that Event and send notification. 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 tells Ecotone to handle specific event based on declaration type hint, just like with CommandHandler.

Commands are targeting single Handler, Events on other hand can have multiple Handlers subscribing to it.

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 updated