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
  • Interceptor
  • Before Attribute
  • Precedence
  • Pointcut
  • Interceptor Types
  • Before Interceptor
  • - Exceptional Interceptor
  • - Payload Enriching Interceptor
  • - Header Enriching Interceptor
  • - Message Filter Interceptor
  • Around Interceptor
  • After Interceptor
  • Presend Interceptor

Was this helpful?

Export as PDF
  1. Modelling
  2. Extending Messaging (Middlewares)

Interceptors (Middlewares)

PHP Interceptors Middlewares

PreviousMessage HeadersNextAdditional Scenarios

Last updated 3 months ago

Was this helpful?

Ecotone provide possibility to handle via Interceptors. Interceptor intercepts the process of handling the message, this means we can do actions like:

  • Enriching the

  • Stopping or modify usual processing cycle

  • Calling some shared functionality or adding additional behavior

This all can be done without modifying the code itself, as we hook into the existing flows.

If you are familiar with you will find a lot of similarities.

Interceptor

class AdminVerificator
{
    #[Before(precedence: 0, pointcut: "Order\Domain\*")]
    public function isAdmin(array $payload, array $headers) : void
    {
        if ($headers["executorId"] != 1) {
            throw new \InvalidArgumentException("You need to be administrator in order to register new product");
        }
    }
}

Before Attribute

Precedence

Pointcut

Every interceptor has Pointcut attribute, which describes for specific interceptor, which endpoints it should intercept.

  • CLASS_NAME - indicates intercepting specific class or interface or class containing attribute on method or class level

  • CLASS_NAME::METHOD_NAME - indicates intercepting specific method of class

  • expression || expression - Indicating one expression or another e.g. Product\*||Order\*

  • expression && expression - Indicating one expression and another e.g. App\Domain\* && App\Attribute\RequireAdministrator

Interceptor Types

There are four types of interceptors. Each interceptor has it role and possibilities. Interceptors are called in following order:

  • Before

  • Around

  • After

  • Presend

Before Interceptor

Before Interceptor is called after message is sent to the channel, before execution of Endpoint.

- Exceptional Interceptor

#[\Attribute]
class RequireAdministrator {}

Let's create our first Before Interceptor.

class AdminVerificator
{
    #[Before(pointcut: RequireAdministrator::class)]
    public function isAdmin(array $payload, array $headers) : void
    {
        if ($headers["executorId"] != 1) {
            throw new \InvalidArgumentException("You need to be administrator in order to register new product");
        }
    }
}

Now we need to annotate our Command Handler:

#[CommandHandler]
#[RequireAdministrator] // Our Application level defined Attribute
public function changePrice(ChangePriceCommand $command) : void
{
   // do something with $command
}

Whenever we call our command handler, it will be intercepted by AdminVerificator now.

- Payload Enriching Interceptor

#[\Attribute]
class AddTimestamp {}
class TimestampService
{
    #[Before(pointcut: AddTimestamp::class)] 
    public function add(array $payload) : array
    {
        return array_merge($payload, ["timestamp" => time()]);
    }
}
class ChangePriceCommand
{
    private int $productId;
    
    private int $timestamp;
}

#[CommandHandler]
#[AddTimestamp]
public function changePrice(ChangePriceCommand $command) : void
{
   // do something with $command and timestamp
}

- Header Enriching Interceptor

#[\Attribute]
class AddExecutor {}
class TimestampService
{
    #[Before(pointcut: AddExecutor::class, changeHeaders: true)] 
    public function add() : array
    {
        return ["executorId" => 1];
    }
}

If return type is not void new modified based on previous Message will be created from the returned type. If we additionally add changeHeaders: true it will tell Ecotone, that we we want to modify Message headers instead of payload.

#[CommandHandler]
#[AddExecutor] 
public function changePrice(ChangePriceCommand $command, array $metadata) : void
{
   // do something with $command and executor id $metadata["executorId"]
}

- Message Filter Interceptor

Use Message Filter, to eliminate undesired messages based on a set of criteria. This can be done by returning null from interceptor, if the flow should proceed, then payload should be returned.

#[\Attribute]
class SendNotificationOnlyIfInterested {}
class NotificationFilter
{
    #[Before(pointcut: SendNotificationOnlyIfInterested::class, changeHeaders: true)] 
    public function filter(PriceWasChanged $event) : ?array
    {
        if ($this->isInterested($event) {
           return $event; // flow proceeds 
        }
        
        return null;  // message is eliminated, flow stops.
    }
}

If return type is not void new modified based on previous Message will be created from the returned type. If we additionally add changeHeaders=trueit will tell Ecotone, that we we want to modify Message headers instead of payload.

#[EventHandler]
#[SendNotificationOnlyIfInterested] 
public function sendNewPriceNotification(ChangePriceCommand $event) : void
{
   // do something with $event
}

Around Interceptor

The Around Interceptor have access to actual Method Invocation.This does allow for starting action before method invocation is done, and finishing it after.

Around interceptoris a good place for handling actions like Database Transactions or logic that need to access invoked object.

class TransactionInterceptor
{
    #[Around(pointcut: Ecotone\Modelling\CommandBus::class)]
    public function transactional(MethodInvocation $methodInvocation)
    {
        $this->connection->beginTransaction();
        try {
            $result = $methodInvocation->proceed();

            $this->connection->commit();
        }catch (\Throwable $exception) {
            $this->connection->rollBack();

            throw $exception;
        }

        return $result;
    }
}

As we used Command Bus interface as pointcut, we told Ecotone that it should intercept Command Bus Gateway. Now whenever we will call any method on Command Bus, it will be intercepted with transaction. The other powerful use case for Around Interceptor is intercepting Aggregate. Suppose we want to verify, if executing user has access to the Aggregate.

#[Aggregate]
#[IsOwnedByExecutor] 
class Person
{
   private string $personId;

   #[CommandHandler]
   public function changeAddress(ChangeAddress $command) : void
   {
      // change address
   }
   
   public function hasPersonId(string $personId) : bool
   {
      return $this->personId === $personId;
   }
}

We have placed @IsOwnerOfPerson annotation as the top of class. For interceptor pointcut it means, that each endpoint defined in this class should be intercepted. No need to add it on each Command Handler now.

#[\Attribute]
class IsOwnedByExecutor {}
class IsOwnerVerificator
{
    #[Around(pointcut: IsOwnedByExecutor::class)] 
    public function isOwner(MethodInvocation $methodInvocation, Person $person, #[Headers] array $metadata)
    {
        if (!$person->hasPersonId($metadata["executoId"]) {
           throw new \InvalidArgumentException("No access to do this action!");
        }
        return $methodInvocation->proceed();
    }
}

We've passed the executd Aggregate instance - Person to our Interceptor. This way we can get the context of the executed object in order to perform specific logic.

After Interceptor

namespace Order\ReadModel;

class OrderService
{
   #[QueryHandler]
   public function getOrderDetails(GetOrderDetailsQuery $query) : array
   {
      return ["orderId" => $query->getOrderId()]
   }
}   
class AddResultSet
{
    #[After(pointcut: "Order\ReadModel\*") 
    public function add(array $payload) : array
    {
        return ["result" => $payload];
    }
}

We will intercept all endpoints within Order\ReadModel namespace, by adding result coming from the endpoint under result key.

Presend Interceptor

Presend Interceptor can used to verify if data is correct before sending to asynchronous channel, or we may want to check if user has enough permissions to do given action. This will keep our asynchronous channel free of incorrect messages.

class VerifyIfAuthenticated
{
    #[Presend(pointcut: Ecotone\Modelling\Attribute\CommandHandler::class)] 
    public function verify(#[Header("executorId")] ?string $executorId) : void
    {
        if (!$executorId) {
            throw new \InvalidArgumentException("User must be logged");
        }
    }
}



class IsEventAlreadyHandled
{
    private Storage $storage;

    #[Presend(pointcut: Ecotone\Modelling\Attribute\EventHandler::class)] 
    public function verify($payload, #[Header("messageId")] string $messageId)
    {
        if ($this->storage->isHandled($messageId)) {
            return null;
        }
        
        return $payload;
    }
}

Presend can't intercept Gateways like (Command/Event/Query) buses, however in context of Gateways using Before Interceptor lead to same behaviour, therefore can be used instead.

Type of Interceptor more about it

Precedence defines ordering of called interceptors. The lower the value is, the quicker Interceptor will be called. It's safe to stay with range between -1000 and 1000, as numbers bellow -1000 and higher than 1000 are used by Ecotone. The precedence is done within a specific .

NAMESPACE* - Indicating all starting with namespace prefix e.g. App\Domain\*

Before interceptor is called before endpoint is executed. Before interceptors can used in order to stop the flow, throw an exception or enrich the To understand it better, let's follow an example, where we will intercept Command Handler with verification if executor is an administrator. Let's start by creating Attribute called RequireAdministrator in new namepace.

We are using in here which is looking for #[RequireAdministrator] annotation in each of registered . The void return type is expected in here. It tells Ecotonethat, this Before Interceptor is not modifying the Message and message will be passed through. The message flow however can be interrupted by throwing exception.

Our Command Handler is using ChangePriceCommandclass and our AdminVerificator interceptor is using array $payload. They are both referencing payload of the , yet if we define a class as type hint, Ecotone will do the Conversion for us.

If return type is not void new Message will be created from the returned type. Let's follow an example, where we will enrich payload with timestamp.

Suppose we want to add executor Id, but as this is not part of our Command, we want add it to our Headers.

After interceptor is called after endpoint execution has finished. It does work exactly the same as After interceptor can used to for example to enrich QueryHandler result.

Presend Interceptor is called before Message is actually send to the channel. In synchronous channel there is no difference between Before and Presend. The difference is seen when the channel is .

cross cutting concerns
message
Aspect Oriented Programming
Endpoints
Message.
Message
Message
Message
asynchronous
Interceptor Types section
interceptor type
Endpoints
Pointcut
Before Interceptor.
Presend Interceptor is called exactly before message is sent to the channel.