Ecotone
SponsorBlogGithubSupport and ContactCommunity Channel
  • About
  • Installation
  • How to use
    • CQRS PHP
    • Event Handling PHP
    • Aggregates & Sagas
    • Scheduling in PHP
    • Asynchronous PHP
    • Event Sourcing PHP
    • Microservices PHP
    • Resiliency and Error Handling
    • Laravel Demos
    • Symfony Demos
      • Doctrine ORM
  • Tutorial
    • Before we start tutorial
    • Lesson 1: Messaging Concepts
    • Lesson 2: Tactical DDD
    • Lesson 3: Converters
    • Lesson 4: Metadata and Method Invocation
    • Lesson 5: Interceptors
    • Lesson 6: Asynchronous Handling
  • Enterprise
  • Modelling
    • Introduction
    • Message Bus and CQRS
      • CQRS Introduction - Commands
        • Query Handling
        • Event Handling
      • Aggregate Introduction
        • Aggregate Command Handlers
        • Aggregate Query Handlers
        • Aggregate Event Handlers
        • Advanced Aggregate creation
      • Repositories Introduction
      • Business Interface
        • Introduction
        • Business Repository
        • Database Business Interface
          • Converting Parameters
          • Converting Results
      • Saga Introduction
      • Identifier Mapping
    • Extending Messaging (Middlewares)
      • Message Headers
      • Interceptors (Middlewares)
        • Additional Scenarios
      • Intercepting Asynchronous Endpoints
      • Extending Message Buses (Gateways)
    • Event Sourcing
      • Installation
      • Event Sourcing Introduction
        • Working with Event Streams
        • Event Sourcing Aggregates
          • Working with Aggregates
          • Applying Events
          • Different ways to Record Events
        • Working with Metadata
        • Event versioning
        • Event Stream Persistence
          • Event Sourcing Repository
          • Making Stream immune to changes
          • Snapshoting
          • Persistence Strategies
          • Event Serialization and PII Data (GDPR)
      • Projection Introduction
        • Configuration
        • Choosing Event Streams for Projection
        • Executing and Managing
          • Running Projections
          • Projection CLI Actions
          • Access Event Store
        • Projections with State
        • Emitting events
    • Recovering, Tracing and Monitoring
      • Resiliency
        • Retries
        • Error Channel and Dead Letter
          • Dbal Dead Letter
        • Idempotent Consumer (Deduplication)
        • Resilient Sending
        • Outbox Pattern
        • Concurrency Handling
      • Message Handling Isolation
      • Ecotone Pulse (Service Dashboard)
    • Asynchronous Handling and Scheduling
      • Asynchronous Message Handlers
      • Asynchronous Message Bus (Gateways)
      • Delaying Messages
      • Time to Live
      • Message Priority
      • Scheduling
      • Dynamic Message Channels
    • Distributed Bus and Microservices
      • Distributed Bus
        • Distributed Bus with Service Map
          • Configuration
          • Custom Features
          • Non-Ecotone Application integration
          • Testing
        • AMQP Distributed Bus (RabbitMQ)
          • Configuration
        • Distributed Bus Interface
      • Message Consumer
      • Message Publisher
    • Business Workflows
      • The Basics - Stateless Workflows
      • Stateful Workflows - Saga
      • Handling Failures
    • Testing Support
      • Testing Messaging
      • Testing Aggregates and Sagas with Message Flows
      • Testing Event Sourcing Applications
      • Testing Asynchronous Messaging
  • Messaging and Ecotone In Depth
    • Overview
    • Multi-Tenancy Support
      • Getting Started
        • Any Framework Configuration
        • Symfony and Doctrine ORM
        • Laravel
      • Different Scenarios
        • Hooking into Tenant Switch
        • Shared and Multi Database Tenants
        • Accessing Current Tenant in Message Handler
        • Events and Tenant Propagation
        • Multi-Tenant aware Dead Letter
      • Advanced Queuing Strategies
    • Document Store
    • Console Commands
    • Messaging concepts
      • Message
      • Message Channel
      • Message Endpoints/Handlers
        • Internal Message Handler
        • Message Router
        • Splitter
      • Consumer
      • Messaging Gateway
      • Inbound/Outbound Channel Adapter
    • Method Invocation And Conversion
      • Method Invocation
      • Conversion
        • Payload Conversion
        • Headers Conversion
    • Service (Application) Configuration
    • Contributing to Ecotone
      • How Ecotone works under the hood
      • Ecotone Phases
      • Registering new Module Package
      • Demo Integration with SQS
        • Preparation
        • Inbound and Outbound Adapters and Message Channel
        • Message Consumer and Publisher
  • Modules
    • Overview
    • Symfony
      • Symfony Configuration
      • Symfony Database Connection (DBAL Module)
      • Doctrine ORM
      • Symfony Messenger Transport
    • Laravel
      • Laravel Configuration
      • Database Connection (DBAL Module)
      • Eloquent
      • Laravel Queues
      • Laravel Octane
    • Ecotone Lite
      • Logging
      • Database Connection (DBAL Module)
    • JMS Converter
    • OpenTelemetry (Tracing and Metrics)
      • Configuration
    • RabbitMQ Support
    • Kafka Support
      • Configuration
      • Message partitioning
      • Usage
    • DBAL Support
    • Amazon SQS Support
    • Redis Support
  • Other
    • Contact, Workshops and Support
Powered by GitBook
On this page
  • Conversion
  • First Media Type Converter
  • Ecotone JMS Converter

Was this helpful?

Export as PDF
  1. Tutorial

Lesson 3: Converters

PHP Conversion

PreviousLesson 2: Tactical DDDNextLesson 4: Metadata and Method Invocation

Last updated 7 months ago

Was this helpful?

Not having code for Lesson 3? git checkout lesson-3

Conversion

Command, queries and events are not always objects. When they travel via different asynchronous channels, they are converted to simplified format, like JSON or XML. At the level of application however we want to deal with PHP format as objects or arrays.

Moving from one format to another requires conversion. Ecotone does provide extension points in which we can integrate different Converters to do this type of conversion.

First Media Type Converter

Let's build our first converter from JSON to our PHP format. In order to do that, we will need to implement Converter interface and mark it with MediaTypeConverter().

<?php

namespace App\Domain\Product;

use Ecotone\Messaging\Attribute\MediaTypeConverter;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Conversion\MediaType;
use Ecotone\Messaging\Handler\TypeDescriptor;

#[MediaTypeConverter]
class JsonToPHPConverter implements Converter
{
    public function matches(TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType): bool
    {

    }

    public function convert($source, TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType)
    {

    }
}
  1. TypeDescriptor - Describes type in PHP format. This can be class, scalar (int, string), array etc.

  2. MediaType - Describes Media type format. This can be application/json, application/xml etc.

  3. $source - is the actual data to be converted.

Let's start with implementing matches method. Which tells us, if this converter can do conversion from one type to another.

public function matches(TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType): bool
{
    return $sourceMediaType->isCompatibleWith(MediaType::createApplicationJson()) // if source media type is JSON
        && $targetMediaType->isCompatibleWith(MediaType::createApplicationXPHP()); // and target media type is PHP
}

This will tell Ecotone that in case source media type is JSON and target media type is PHP, then it should use this converter. Now we can implement the convert method. We will do pretty naive solution, just for the proof the concept.

public function convert($source, TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType)
{
    $data = \json_decode($source, true, 512, JSON_THROW_ON_ERROR);
    // $targetType hold the class, which we will convert to
    switch ($targetType->getTypeHint()) {
        case RegisterProductCommand::class: {
            return RegisterProductCommand::fromArray($data);
        }
        case GetProductPriceQuery::class: {
            return GetProductPriceQuery::fromArray($data);
        }
        default: {
            throw new \InvalidArgumentException("Unknown conversion type");
        }
    }
}

Normally you would inject into Converter class, some kind of serializer used within your application for example JMS Serializer or Symfony Serializer to make the conversion.

And let's add fromArray method to RegisterProductCommand and GetProductPriceQuery.

class GetProductPriceQuery
{
    private int $productId;

    public function __construct(int $productId)
    {
        $this->productId = $productId;
    }

    public static function fromArray(array $data) : self
    {
        return new self($data['productId']);
    }
class RegisterProductCommand
{
    private int $productId;

    private int $cost;

    public function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;
    }

    public static function fromArray(array $data) : self
    {
        return new self($data['productId'], $data['cost']);
    }

Let's run our testing command:

bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!

If we call our testing command now, everything is going fine, but we still send PHP objects instead of JSON, therefore there was not need for Conversion. In order to start sending Commands and Queries in different format, we need to provide our handlers with routing key. This is because we do not deal with Object anymore, therefore we can't do the routing based on them.

You may think of routing key, as a message name used to route the message to specific handler. This is very powerful concept, which allows for high level of decoupling.

#[CommandHandler("product.register")]
public static function register(RegisterProductCommand $command) : self
{
    return new self($command->getProductId(), $command->getCost());
}

#[QueryHandler("product.getCost")] 
public function getCost(GetProductPriceQuery $query) : int
{
    return $this->cost;
}

Let's change our Testing class, so we call buses with JSON format.

(...)

public function run() : void
{
    $this->commandBus->sendWithRouting("product.register", \json_encode(["productId" => 1, "cost" => 100]), "application/json");

    echo $this->queryBus->sendWithRouting("product.getCost", \json_encode(["productId" => 1]), "application/json");
}

We make use of different method now sendWithRouting. It takes as first argument routing key to which we want to send the message. The second argument describes the format of message we send. Third is the data to send itself, in this case command formatted as JSON.

Let's run our testing command:

bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!

Ecotone JMS Converter

Normally we don't want to deal with serialization and deserialization, or we want to make the need for configuration minimal. This is because those are actually time consuming tasks, which are more often than not a copy/paste code, which we need to maintain.

composer require ecotone/jms-converter

Let's remove __construct and fromArray methods from RegisterProductCommand GetProductPriceQuery, and the JsonToPHPConverter class completely, as we won't need it anymore.

JMS creates cache to speed up serialization process. In case of problems with running this test command, try to remove your cache. Let's run our testing command:

bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!

Do you wonder, how come, that we just deserialized our Command and Query classes without any additional code? JMS Module reads properties and deserializes according to type hint or docblock for arrays. It's pretty straight forward and logical:

Conversion Table examples:
Source => Converts too

private int $productId => int

private string $data => string

private \stdClass $data => \stdClass
 
/**
* @var \stdClass[] 
*/
private array $data => array<\stdClass>

Let's imagine we found out, that we have bug in our software. Our system users have registered product with negative price, which in result lowered the bill.

Product should be registered only with positive cost

We could put constraint in Product, validating the Cost amount. But this would assure us only in that place, that this constraint is met. Instead we want to be sure, that the Cost is correct, whenever we make use of it, so we can avoid potential future bugs. This way we will know, that whenever we will deal with Cost object, we will now it's correct. To achieve that we will create Value Object named Cost that will handle the validation, during the construction.

namespace App\Domain\Product;

class Cost
{
    private int $amount;

    public function __construct(int $amount)
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("The cost cannot be negative or zero, {$amount} given.");
        }
        
        $this->amount = $amount;
    }

    public function getAmount() : int
    {
        return $this->amount;
    }
    
    public function __toString()
    {
        return (string)$this->amount;
    }
}

Great, but where to convert the integer to the Cost class? We really don't want to burden our business logic with conversions. Ecotone JMS does provide extension points, so we can tell him, how to convert specific classes.

Normally you will like to delegate conversion to Converters, as we want to get our domain classes converted as fast as we can. The business logic should stay clean, so it can focus on the domain problems, not technical problems.

Let's create class App\Infrastructure\Converter\CostConverter. We will put it in different namespace, to separate it from the domain.

namespace App\Infrastructure\Converter;

use App\Domain\Product\Cost;
use Ecotone\Messaging\Attribute\Converter;

class CostConverter
{
    #[Converter]
    public function convertFrom(Cost $cost) : int
    {
        return $cost->getAmount();
    }

    #[Converter]
    public function convertTo(int $amount) : Cost
    {
        return new Cost($amount);
    }
}

We mark the methods with Converter attribute, so Ecotone can read parameter type and return type in order to know, how he can convert from scalar/array to specific class and vice versa. Let's change our command and aggregate class, so it can use the Cost directly.

class RegisterProductCommand
{
    private int $productId;

    private Cost $cost;

    public function getProductId() : int
    {
        return $this->productId;
    }

    public function getCost() : Cost
    {
        return $this->cost;
    }
}

The $cost class property will be automatically converted from integer to Cost by JMS Module.

class Product
{
    use WithAggregateEvents;

    #[Identifier]
    private int $productId;

    private Cost $cost;

    private function __construct(int $productId, Cost $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;

        $this->recordThat(new ProductWasRegisteredEvent($productId));
    }

    #[CommandHandler("product.register")]
    public static function register(RegisterProductCommand $command) : self
    {
        return new self($command->getProductId(), $command->getCost());
    }

    #[QueryHandler("product.getCost")]
    public function getCost(GetProductPriceQuery $query) : Cost
    {
        return $this->cost;
    }
}

Let's run our testing command:

bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!

In this Lesson we learned how to make use of Converters. The command which we send from outside (to the Command Bus) is still the same, as before. We changed the internals of the domain, without affecting consumers of our API. In next Lesson we will learn and Method Invocation and Metadata

Great, we just finished Lesson 3!

Ecotone comes with integration with to solve this problem. It introduces a way to write to reuse Converters and write them only, when that's actually needed. Therefore let's replace our own written Converter with JMS one. Let's download the Converter using .

To get more information, read

Media Type
JMS Serializer
Composer
Native Conversion