Search code examples
phpsymfonysymfony-messenger

Can't set the message handler on Symfony without attributes


I have this command

<?php

namespace App\Todo\Application\Command;


use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

class CreateTodoCommand
{
    public function __construct(
        private string $name,
        private string $text,
        private string $userId
    ) { }

    public function getName(): string
    {
        return $this->name;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getUserId(): UuidInterface
    {
        return Uuid::fromString($this->userId);
    }
}

and this handler

<?php

namespace App\Todo\Application\Command;

use App\Todo\Application\Interfaces\CommandHandlerInterface;
use App\Todo\Domain\Entity\Todo;
use App\Todo\Domain\Repository\TodoRepositoryInterface;
use App\Todo\Domain\ValueObject\Name;
use App\Todo\Domain\ValueObject\Text;
use App\Todo\Domain\ValueObject\UserId;
use Ramsey\Uuid\Uuid;


class CreateTodoCommandHandler implements CommandHandlerInterface
{
    public function __construct(private TodoRepositoryInterface $todoRepository)
    {}

    public function __invoke(CreateTodoCommand $command)
    {
        $todo = new Todo(
            Uuid::uuid4(),
            new Name($command->getName()),
            new Text($command->getTe xt()),
            new UserId($command->getUserId())
        );
    
        $this->todoRepository->save($todo);
    }
    
}

this messenger.yaml configuration

framework:
    messenger:
        # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
        # failure_transport: failed

        transports:
            # https://symfony.com/doc/current/messenger.html#transport-configuration
            async: 
                dsn: '%env(RABBITMQ_DSN)%'
                retry_strategy:
                    max_retries: 5
                    delay: 1000
                    multiplier: 2
                    max_delay: 60000
            # failed: 'doctrine://default?queue_name=failed'
            # sync: 'sync://'

        routing:
            # Route your messages to the transports
            # 'App\Message\YourMessage': async
            'App\Todo\Application\Command\CreateTodoCommand': async

and this services.yaml config

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
    # Registra el repositorio como servicio
    App\Todo\Infrastructure\Repository\DoctrineTodoRepository:
        arguments:
            $em: '@doctrine.orm.entity_manager'
        
    # Registra el handler y autowire la interfaz con su implementación
    App\Todo\Application\Command\CreateTodoCommandHandler:
        arguments:
            $todoRepository: '@App\Todo\Infrastructure\Repository\DoctrineTodoRepository'


    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

But when I try t consume the queued messages I get this error

messenger.CRITICAL: Error thrown while handling message App\Todo\Application\Command\CreateTodoCommand. Removing from transport after 5 retries. Error: "No handler for message "App\Todo\Application\Command\CreateTodoCommand"." {"class":"App\\Todo\\Application\\Command\\CreateTodoCommand","retryCount":5,"error":"No handler for message \"App\\Todo\\Application\\Command\\CreateTodoCommand\".","exception":"[object] (Symfony\\Component\\Messenger\\Exception\\NoHandlerForMessageException(code: 0): No handler for message \"App\\Todo\\Application\\Command\\CreateTodoCommand\". at /var/www/html/vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php:117)"} []

If I check the handlers with bin/console debug:messenger there is no CreateTodoCommandHandler on the list

Messenger
=========

messenger.bus.default
---------------------

 The following messages can be dispatched:

 ---------------------------------------------------------- 
  Symfony\Component\Process\Messenger\RunProcessMessage     
      handled by process.messenger.process_message_handler  
                                                            
  Symfony\Component\Console\Messenger\RunCommandMessage     
      handled by console.messenger.execute_command_handler  
                                                            
  Symfony\Component\Messenger\Message\RedispatchMessage     
      handled by messenger.redispatch_message_handler       
                                                            
 ---------------------------------------------------------- 

And I cleared cache several times

I think the config is correct, but there is no way to set the handler, there is something wrong or do I missed something?

If I use an attribute

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class CreateTodoCommandHandler implements CommandHandlerInterface

then everything work correctly.

It's a handler registration problem, and I need to set the handler without using an attribute.


Solution

  • Since you do not want to use configuration attributes, you'll need to provide the configuration manually.

    E.g.:

    # config/services.yaml
    services:
        # this config only applies to the services created by this file
        _instanceof:
            # services whose classes are instances of CommandHandlerInterface will be tagged automatically
            App\Todo\Application\Interfaces\CommandHandlerInterface:
                tags: ['messenger.message_handler' { bus: 'command.bus' }]
        # ...
    

    With the above, any class that implements CommandHandlerInterface will be tagged 'messenger.message_handler', which in turn will cause the service to be registered as a message handler for Messenger, and be directed to use the command bus.

    You may need to perform some application specific adjustments to your own use-case, but this should take you in the right direction.

    Docs: