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
  • Metadata
  • Method Invocation
  • Injecting arguments
  • Default Converters

Was this helpful?

Export as PDF
  1. Tutorial

Lesson 4: Metadata and Method Invocation

PHP Metadata and Method Invocation

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

Metadata

Message can contain of Metadata. Metadata is just additional information stored along side to the Message's payload. It may contain things like currentUser, timestamp, contentType, messageId.

In Ecotone headers and metadata means the same. Those terms will be used interchangeably.

To test out Metadata, let's assume we just got new requirement for our Products in Shopping System.:

User who registered the product, should be able to change it's price.

Let's start by adding ChangePriceCommand

namespace App\Domain\Product;

class ChangePriceCommand
{
    private int $productId;

    private Cost $cost;

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

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

We will handle this Command in a minute. Let's first add user information for registering the product. We will do it, using Metadata. Let's get back to our Testing Class EcotoneQuickstart and add 4th argument to our CommandBus call.

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

sendWithRouting accepts 4th argument, which is associative array. Whatever we will place in here, will be available during message handling for us - This actually our Metadata. It's super simple to pass new Headers, it's matter of adding another key to the array. Now we can change our Product aggregate:

#[Aggregate]
class Product
{
    use WithAggregateEvents;

    #[Identifier]
    private int $productId;

    private Cost $cost;

    private int $userId;

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

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

    #[CommandHandler("product.register")]
    public static function register(RegisterProductCommand $command, array $metadata) : self
    {
        return new self(
            $command->getProductId(), 
            $command->getCost(), 
            // all metadata is available for us. 
            // Ecotone automatically inject it, if second param is array
            $metadata["userId"]
        );
    }

We have added second parameter $metadata to our CommandHandler. Ecotone read parameters and evaluate what should be injected. We will see soon, how can we take control of this process. We can add changePrice method now to our Aggregate:

#[CommandHandler("product.changePrice")]
public function changePrice(ChangePriceCommand $command, array $metadata) : void
{
    if ($metadata["userId"] !== $this->userId) {
        throw new \InvalidArgumentException("You are not allowed to change the cost of this product");
    }

    $this->cost = $command->getCost();
}

And let's call it with incorrect userId and see, if we get the exception.

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

    $this->commandBus->sendWithRouting(
        "product.changePrice",
        \json_encode(["productId" => 1, "cost" => 110]),
        "application/json",
        [
            "userId" => 3
        ]
    );        

Let's run our testing command:

bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!

InvalidArgumentException
                                                          
  You are not allowed to change the cost of this product 

Method Invocation

We have been just informed, that customers are registering new products in our system, which should not be a case. Therefore our next requirement is:

Only administrator should be allowed to register new Product

Let's create simple UserService which will tell us, if specific user is administrator. In our testing scenario we will suppose, that only user with id of 1 is administrator.

namespace App\Domain\Product;

class UserService
{
    public function isAdmin(int $userId) : bool
    {
        return $userId === 1;
    }
}

Now we need to think where we should call our UserService. The good place for it, would not allow for any invocation of product.register command without being administrator, otherwise our constraint may be bypassed. Ecotone does allow for auto-wire like injection for endpoints. All services registered in Depedency Container are available.

#[CommandHandler("product.register")]
public static function register(
    RegisterProductCommand $command, 
    array $metadata, 
    // Any non first class argument, will be considered an DI Service to inject
    UserService $userService
) : self
{
    $userId = $metadata["userId"];
    if (!$userService->isAdmin($userId)) {
        throw new \InvalidArgumentException("You need to be administrator in order to register new product");
    }

    return new self($command->getProductId(), $command->getCost(), $userId);
}

Great, there is no way to bypass the constraint now. The isAdmin constraint must be satisfied in order to register new product. Let's correct our testing class.

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

    $this->commandBus->sendWithRouting(
        "product.changePrice",
        \json_encode(["productId" => 1, "cost" => 110]),
        "application/json",
        [
            "userId" => 1
        ]
    );

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

Let's run our testing command:

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

Injecting arguments

Ecotone inject arguments based on Parameter Converters. Parameter converters , tells Ecotone how to resolve specific parameter and what kind of argument it is expecting. The one used for injecting services like UserService is Reference parameter converter. Let's see how could we use it in our product.register command handler.

Let's suppose UserService is registered under user-service in Dependency Container. Then we would need to set up the CommandHandlerlike below.

#[CommandHandler("product.register")]
public static function register(
    RegisterProductCommand $command, 
    array $metadata, 
    #[Reference("user-service")] UserService $userService
) : self

Reference- Does inject service from Dependency Container. If referenceName, which is name of the service in the container is not given, then it will take the class name as default.

Headers - Does inject all headers as array.

Default Converters

Ecotone, if parameter converters are not passed provides default converters. First parameter is always Payload. The second parameter, if is array then Headers converter is taken, otherwise if class type hint is provided for parameter, then Reference converter is picked. If we would want to manually configure parameters for product.register Command Handler, then it would look like this:

#[CommandHandler("product.register")]
public static function register(
    #[Payload] RegisterProductCommand $command, 
    #[Headers] array $metadata, 
    #[Reference] UserService $userService
) : self
{
    // ...
}

We could also inject specific header and let Ecotone convert it directly to specific object (if we have Converter registered):

#[CommandHandler("product.register")]
public static function register(
    #[Payload] RegisterProductCommand $command, 
    // injecting specific header and doing the conversion string to UserId
    #[Header("userId")] UserId $metadata, 
    #[Reference] UserService $userService
) : self
{
    // ...
}

Great, we have just finished Lesson 4!

In this Lesson we learned about using Metadata to provide extra information to our Message. Besides we took a look on how arguments are injected into endpoint and how we can make use of it. Now we will learn about powerful Interceptors, which can be describes as Middlewares on steroids.

PreviousLesson 3: ConvertersNextLesson 5: Interceptors

Last updated 6 months ago

Was this helpful?

Payload - Does inject payload of the . In our case it will be the command itself

Header - Does inject single header from the . There is more to be said about this, but at this very moment, it will be enough for us to know that such possibility exists in order to continue. You may read more detailed description in

message
message
Method Invocation section.