symfonysymfony-messenger

Handling messages from different namespaces in Symfony Messenger with RMQ


I'm building an application with micro services approach. For communication between services I use Symfony Messenger with RMQ transport. Basically everything works fine, but all my services has to be in same namespace. Once I tried to separate them into their own namespaces like App\Mail, App\Auth and so on messenger was complaining about lack of Event classes because whole namesapce is provided within header of message sent to RMQ. Is there any way I could map events from two different namespaces?

For instance Auth app dispatches event UserRegistered so message has type of App\Auth\Event\UserRegistered. I want to handle that event in my Mail app but messenger can't consume it because my Event and Handler are under App\Mail namespace, so it can't find App\Auth\Event\UserRegistered class in "Mail" app.

Example error I'm getting:

In Serializer.php line 85:

  Could not decode message: Could not denormalize object of type App\Event\UserRequestedPasswordReset, no supporting normalizer found.

In this exact example I'm sending event UserRequestedPasswordReset from app that is under App namespace, and I'm trying to consume it with application under App\Mail namespace.

I wasn't able to find anything helpful in documentation or over the internet. I was trying to alias App\Event\UserRequestedPasswordReset to App\Mail\Event\UserRequestedPasswordReset in container but no luck. I'm guessing that it's something doable with Denormalizers, but also couldn't find anything helpful over internet.

Communication itself is working, messages are sent to RMQ and received in other services. My setup for RMQ is: I have multiple queues, one for each service. I have fanout exchange with those queues binded. Whenever I'm producing event I'm publishing it to exchange to populate it to all queues, so interested services can handle them.

Example messenger configuration in one of my services. Besides event I'm using messenger to handle CQRS commands and queries, so I'm using three different buses.

messenger:
        default_bus: messenger.bus.commands
        buses:
            messenger.bus.commands:
                middleware:
#                    - validation
#                    - doctrine_transaction
            messenger.bus.queries:
                middleware:
#                    - validation
            messenger.bus.events:
                default_middleware: allow_no_handlers
                middleware:
#                    - validation
        transports:
            events:
                dsn: "%env(MESSENGER_AMQP_DSN)%"
                options:
                    exchange:
                        name: ecommerce_events
                        type: fanout
                    queue:
                        name: ecommerce_auth

        routing:
            'App\Event\UserCreated': events
            'App\Event\UserModified': events
            'App\Event\UserChangedPassword': events
            'App\Event\UserRequestedPasswordReset': events

I would like to keep my applications in different namespaces and still be able to handle events from other services


Solution

  • So after digging into topic I was able to find solution.

    I just needed to create custom serializer and then during encoding I was stripping off the namespace and then during decoding I was providing map for type to actual event class. Here is my code

    class EventsSerializer extends Serializer
    {
        public function encode(Envelope $envelope): array
        {
            $data = parent::encode($envelope);
            $data['headers']['type'] = $this->parseType($data['headers']['type']);
    
            return $data;
        }
    
        private function parseType(string $type)
        {
            return end(explode('\\', $type));
        }
    
        public function decode(array $encodedEnvelope): Envelope
        {
            $translatedType = $this->translateType($encodedEnvelope['headers']['type']);
            $encodedEnvelope['headers']['type'] = $translatedType;
    
            return parent::decode($encodedEnvelope);
        }
    
        private function translateType($type)
        {
            $map = [
                'UserCreated' => UserCreated::class,
                'UserRequestedPasswordReset' => UserRequestedPasswordReset::class
            ];
    
            return $map[$type] ?? $type;
        }
    }
    

    In messenger configuration:

    framework:
      messenger:
        serializer:
          default_serializer: AppBundle\Serializer\EventsSerializer
    

    Please bare in mind that this is more like proof of concept and it probably can be enhanced, but it's working.