Отправка и получение сообщений через RabbitMQ в Symfony

541f05f3e97d6eaa20b2ee06be0f64be.png

Привет, Хабр!

Итак, решили внедрить асинхронные процессы в своё Symfony-приложение? Отличный выбор! А выбор RabbitMQ для этой задачи — вообще идеален: надёжный, быстрый и отлично работающий в связке с Symfony. Наша цель — научиться отправлять сообщения (скажем, сообщения о новых котиках) в очередь и плавно обрабатывать их.

Запуск RabbitMQ

Для начала понадобится сам RabbitMQ. Можно установить его напрямую или, как в большинстве проектов, с помощью Docker.

Добавляем в docker-compose.yml сервис для RabbitMQ:

version: '3.8'
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"      # для общения с приложением
      - "15672:15672"    # для панели управления
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest

Теперь запускаем:

docker-compose up -d

Проверяем: панель управления RabbitMQ доступна по адресу http://localhost:15672 (логин и пароль — guest). Здесь можно управлять очередями, обменниками и проверять, как сообщения путешествуют по RabbitMQ.

Настройка Symfony Messenger

Теперь — к нашей основной задаче. Symfony Messenger — это мощный компонент, который облегчает отправку и получение сообщений. В нашем случае, это — сообщения о котиках. С помощью Messenger будем слать котиков в очередь и обрабатывать их с другой стороны.

Чтобы начать, нужно установить пару зависимостей:

composer require symfony/messenger symfony/amqp-messenger

В файле config/packages/messenger.yaml добавляем транспорт для RabbitMQ. Это будет «транспорт асинхронных сообщений»:

framework:
  messenger:
    transports:
      async: '%env(MESSENGER_TRANSPORT_DSN)%'  # Основной транспорт
    routing:
      'App\Message\CatMessage': async  # Назначаем маршрут для наших сообщений

Здесь мы указали, что все сообщения типа CatMessage будут отправляться в очередь «async».

Подключение к RabbitMQ

Добавляем следующую строку в файл .env:

MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f

Тут мы указываем параметры подключения к RabbitMQ — дефолтные логин и пароль, адрес хоста и название виртуального хоста (%2f — это код символа /, он обозначает корневой виртуальный хост).

Создание сообщения CatMessage

Теперь создадим сообщение. В Symfony это — объект, который будет содержать данные о котике. Начнем с простого примера:

// src/Message/CatMessage.php
namespace App\Message;

class CatMessage
{
    private string $name;
    private int $age;

    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age = $age;
    }

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

    public function getAge(): int
    {
        return $this->age;
    }
}

Теперь есть простой объект сообщения с именем и возрастом котика. Это то, что мы будем отправлять в RabbitMQ.

Обработчик сообщения

Сообщения у нас есть, но их нужно кому-то обрабатывать. Создадим обработчик, который будет получать сообщения из очереди и обрабатывать их.

// src/MessageHandler/CatMessageHandler.php
namespace App\MessageHandler;

use App\Message\CatMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class CatMessageHandler implements MessageHandlerInterface
{
    public function __invoke(CatMessage $message)
    {
        // Представим, что обработчик добавляет котика в базу данных
        echo sprintf("Котик %s, возраст %d лет, успешно обработан!\n", $message->getName(), $message->getAge());
    }
}

Наш обработчик будет просто выводить информацию о котике, но в продакшене можно здесь сделать что-то полезное, например, добавить котика в базу данных.

Отправка сообщений

Пора отправить первого котика в очередь! Для создадим контроллер, который будет принимать запрос и отправлять CatMessage через Symfony Messenger.

// src/Controller/CatController.php
namespace App\Controller;

use App\Message\CatMessage;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;

class CatController
{
    private MessageBusInterface $bus;

    public function __construct(MessageBusInterface $bus)
    {
        $this->bus = $bus;
    }

    #[Route('/send-cat', name: 'send_cat')]
    public function sendCat(): Response
    {
        $catMessage = new CatMessage('Барсик', 3);
        $this->bus->dispatch($catMessage);

        return new Response('Сообщение о котике отправлено!');
    }
}

Теперь можно перейти по адресу /send-cat, и сообщение отправится в очередь RabbitMQ. Каждый раз, когда этот маршрут вызывается, новый котик добавляется в очередь.

Запуск консумера (обработчика)

Чтобы Symfony мог начать забирать сообщения из RabbitMQ и передавать их в обработчик, нужно запустить консумер:

php bin/console messenger:consume async

Теперь при каждом новом сообщении обработчик будет выводить информацию о котике.

Обработка ошибок и ретраи

Жизнь сурова: иногда обработка сообщений может завершиться с ошибкой. Symfony Messenger позволяет настраивать ретраи и логировать ошибки.

Для этого в messenger.yaml добавим параметры:

framework:
  messenger:
    failure_transport: failed

    transports:
      async: '%env(MESSENGER_TRANSPORT_DSN)%'
      failed: 'doctrine://default?queue_name=failed'

    retry_strategy:
      async:
        max_retries: 3
        delay: 1000    # Задержка между ретраями
        multiplier: 2  # Увеличиваем задержку на 2 раза
        max_delay: 30000  # Максимальная задержка между ретраями

Здесь настроили стратегию ретраев, которая повторит отправку сообщения до трех раз с увеличением задержки. Если сообщение так и не будет обработано, оно переместится в failed-транспорт, где мы сможем его повторно отправить позже.

Инлайн-классы

Если хочется экспериментировать с инлайн-классами (например, в тестовых сценариях), вы можно создать обработчик с помощью анонимных классов:

$handler = new class implements MessageHandlerInterface {
    public function __invoke(CatMessage $message)
    {
        echo sprintf("Инлайн-обработчик: Котик %s, возраст %d лет, был обработан.\n", $message->getName(), $message->getAge());
    }
};

Инлайн-классы полезны для быстрого прототипирования, но не стоит злоупотреблять!

Если у вас есть чем дополнить или поделиться своим опытом работы с RabbitMQ в Symfony — пишите в комментариях! Всегда интересно узнать.

Также спешу напомнить, что сегодня, 12 ноября, в 20:00 пройдет открытый урок на тему «Надёжная отправка и получение сообщений через RabbitMQ в Symfony». Если интересно, узнать подробности и записаться можно на странице курса «Symfony Framework».

© Habrahabr.ru