Symfony Messenger moves work such as email delivery, report generation, and webhook calls from the request path into handlers that a worker can run later. A queue setup gives Messenger a transport, routes a message class to that transport, and leaves the worker responsible for processing the message outside the original request.

Doctrine Messenger uses the Doctrine database connection many Symfony applications already have, so it avoids a separate Redis or RabbitMQ broker for a local smoke test. The same async transport name can later point at another supported DSN when the application moves to a dedicated broker.

A suitable starting project already has symfony/messenger, a message class, and a handler. The sample message class is App\Message\GenerateReport, the transport is async, and the proof is a queue count that drops from one to zero after messenger:consume handles the message.

Steps to configure a Symfony Messenger queue:

  1. Install the Doctrine transport bridge when the project does not already include it.
    $ composer require symfony/doctrine-messenger

    The doctrine://default DSN uses the active Doctrine connection. Use the matching bridge package instead when the queue backend is Redis, AMQP, Beanstalkd, or another supported transport.

  2. Set the Messenger transport DSN in the environment file used by the application.
    # .env.local
    MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=false

    Configure DATABASE_URL first when doctrine://default has no database connection to use.
    Related: How to configure a Doctrine database in Symfony

  3. Define the async transport and a failed-message transport in /config/packages/messenger.yaml.
    framework:
        messenger:
            failure_transport: failed
    
            transports:
                async:
                    dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                    retry_strategy:
                        max_retries: 3
                        multiplier: 2
                failed: 'doctrine://default?queue_name=failed'
  4. Route the application message class to the async transport.
    framework:
        messenger:
            routing:
                App\Message\GenerateReport: async

    Messages without a matching routing rule are handled synchronously when they are dispatched.
    Related: How to handle a Symfony Messenger message

  5. Clear the Symfony cache after changing Messenger configuration or adding the test command and handler.
    $ php bin/console cache:clear
    
     // Clearing the cache for the dev environment with debug true
    
     [OK] Cache for the "dev" environment (debug=true) was successfully cleared.
  6. Confirm the active Messenger configuration includes the async transport and message route.
    $ php bin/console debug:config framework messenger
    
    Current configuration for "framework.messenger"
    ===============================================
    
    failure_transport: failed
    transports:
        async:
            dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
    ##### snipped #####
    routing:
        App\Message\GenerateReport:
            - async
    ##### snipped #####
  7. Confirm Symfony can find a handler for the routed message class.
    $ php bin/console debug:messenger
    
    Messenger
    =========
    
    messenger.bus.default
    ---------------------
    
     The following messages can be dispatched:
    
     ------------------------------------------------------------------------
      App\Message\GenerateReport
          handled by App\MessageHandler\GenerateReportHandler
    ##### snipped #####
  8. Prepare the Doctrine transport storage.
    $ php bin/console messenger:setup-transports
    
     [OK] The "async" transport was set up successfully.
    
     [OK] The "failed" transport was set up successfully.

    With auto_setup=false, the table is not created implicitly when the first message is sent. Run setup or a database migration during deployment before workers start.

  9. Dispatch one message through the application path that uses the routed message class.
    $ php bin/console app:queue-report quarterly
    Queued report "quarterly".

    The sample command dispatches App\Message\GenerateReport through MessageBusInterface. Use the controller, command, or service that dispatches the message in your application.
    Related: How to create a Symfony console command
    Related: How to handle a Symfony Messenger message

  10. Check that the message is waiting in the async transport.
    $ php bin/console messenger:stats async
     ----------- -------
      Transport   Count
     ----------- -------
      async       1
     ----------- -------
  11. Run one worker pass for the async transport.
    $ php bin/console messenger:consume async --limit=1 -vv
    
     [OK] Consuming messages from transport "async".
    
    INFO [messenger] Received message App\Message\GenerateReport
    INFO [messenger] Message App\Message\GenerateReport handled by App\MessageHandler\GenerateReportHandler::__invoke
    INFO [messenger] App\Message\GenerateReport was handled successfully (acknowledging to transport).
    INFO [messenger] Worker stopped due to maximum count of 1 messages processed

    Remove --limit=1 for a long-running worker. Use a process manager such as systemd or Supervisor for production workers so they restart after deploys or failures.

  12. Verify the async transport is empty after the worker handles the message.
    $ php bin/console messenger:stats async
     ----------- -------
      Transport   Count
     ----------- -------
      async       0
     ----------- -------
  13. Check the handler side effect when using the sample handler.
    $ cat var/report.log
    handled quarterly

    For a real handler, verify the domain effect the handler owns, such as a sent email record, generated file, API call, or updated database row.