Dlaczego zadania w tle mają znaczenie w Symfony
Większość aplikacji Symfony zaczyna od prostego wzorca: użytkownik wysyła żądanie, kontroler wykonuje pracę, a odpowiedź wraca do przeglądarki. To działa przy odczycie karty produktu. Rozsypuje się, gdy wysyłasz faktury, skalujesz obrazy, wołasz zewnętrzne API albo synchronizujesz dane z systemem magazynowym.
Jeśli te zadania zostają w żądaniu HTTP, użytkownicy dłużej czekają, pojawiają się timeouty, a jedna wolna zależność potrafi zatrzymać cały checkout. Symfony Messenger rozwiązuje to przez wysłanie wiadomości teraz i przetworzenie jej później, w workerze poza żądaniem webowym.
Czym faktycznie jest Symfony Messenger
Messenger to magistrala wiadomości. Aplikacja tworzy mały obiekt opisujący pracę do wykonania, a potem wysyła go do magistrali. Transport (najczęściej Redis, Doctrine albo RabbitMQ) zapisuje wiadomość. Osobny worker konsoli pobiera kolejkę i uruchamia handler.
Ten podział daje trzy praktyczne korzyści:
- Szybsze odpowiedzi dla użytkowników.
- Automatyczne ponawianie, gdy zewnętrzna usługa chwilowo nie działa.
- Wyraźne oddzielenie „przyjmij pracę” od „dokończ pracę”.
Minimalny przykład
Zacznij od wiadomości, która niesie tylko dane potrzebne handlerowi:
namespace App\Message;
final class SendOrderConfirmation
{
public function __construct(
public readonly int $orderId,
) {
}
}
Następnie napisz handler, który wykonuje właściwą pracę:
namespace App\MessageHandler;
use App\Message\SendOrderConfirmation;
use App\Repository\OrderRepository;
use App\Service\Mailer\OrderMailer;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class SendOrderConfirmationHandler
{
public function __construct(
private OrderRepository $orders,
private OrderMailer $mailer,
) {
}
public function __invoke(SendOrderConfirmation $message): void
{
$order = $this->orders->get($message->orderId);
$this->mailer->sendConfirmation($order);
}
}
W kontrolerze albo serwisie wyślij wiadomość zamiast wysyłać maila bezpośrednio:
use App\Message\SendOrderConfirmation;
use Symfony\Component\Messenger\MessageBusInterface;
public function placeOrder(MessageBusInterface $bus, int $orderId): void
{
// Najpierw zapisz zamówienie, potem kolejkuj efekty uboczne.
$bus->dispatch(new SendOrderConfirmation($orderId));
}
Konfiguracja transportu asynchronicznego
W config/packages/messenger.yaml skieruj ciężkie wiadomości do prawdziwej kolejki:
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 10000
failed: 'doctrine://default?queue_name=failed'
routing:
'App\Message\SendOrderConfirmation': async
Typowy DSN Redis wygląda jak redis://localhost:6379/messages. Dla zespołów już pracujących na MySQL lub PostgreSQL transport Doctrine to dobry start. Redis albo RabbitMQ warto wdrożyć, gdy rośnie wolumen.
Jak prawidłowo uruchamiać workery
Workery to procesy długożyjące. Uruchamiaj je przez Supervisor, systemd albo orchestrator kontenerów:
php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M
--time-limit i --memory-limit wymuszają czysty restart, zanim wycieki pamięci staną się incydentem produkcyjnym. Po każdym deployu restartuj workery, żeby załadowały nowy kod.
Ponawianie, awarie i obserwowalność
Tymczasowe błędy sieciowe powinny wracać do kolejki. Błędy trwałe powinny się zatrzymać. Messenger wspiera exponential backoff przez retry_strategy. Wiadomości, które nadal padają, trafiają do failure transport, gdzie możesz je podejrzeć i ponowić:
php bin/console messenger:failed:show
php bin/console messenger:failed:retry
Dodawaj strukturalne logi w handlerach. Loguj klasę wiadomości, id encji i numer próby. Dzięki temu awarie kolejki diagnozuje się dużo łatwiej niż po ogólnym wpisie „mail failed”.
Zasady projektowe, które utrzymują Messenger w ryzach
Trzymaj wiadomości małe i serializowalne. Preferuj identyfikatory zamiast pełnych grafów encji. Duże obiekty w kolejce bolą przy każdej zmianie modelu domenowego.
Rób handlery idempotentne. Mail z potwierdzeniem nie powinien wyjść dwa razy tylko dlatego, że worker padł po sukcesie, a przed potwierdzeniem wiadomości. Zapisz flagę „confirmation sent” albo użyj unikalnego id wiadomości u dostawcy.
Wysyłaj wiadomość po commicie transakcji. Jeśli zakolejujesz job, a potem transakcja się wycofa, worker może przetworzyć zamówienie, którego nigdy nie było. Użyj middleware Doctrine albo dispatch w listenerze po commicie, gdy spójność ma znaczenie.
Jeden handler, jedna odpowiedzialność. Wiadomość SendOrderConfirmation powinna wysłać mail potwierdzający. Faktury, stock i CRM mogą być osobnymi wiadomościami z tego samego zdarzenia domenowego.
Kiedy Messenger nie jest dobrym narzędziem
Nie każda wolna operacja należy do kolejki. Jeśli użytkownik musi zobaczyć wynik od razu (ekran potwierdzenia płatności, walidacja na żywo), zostaw pracę synchroniczną i ją zoptymalizuj. Messenger sprawdza się, gdy biznes akceptuje krótkie opóźnienie w zamian za niezawodność.
Podsumowanie
Symfony Messenger to jedno z narzędzi o najwyższej dźwigni w nowoczesnym stacku Symfony. Chroni czas odpowiedzi, amortyzuje kapryśne integracje i zamienia efekty uboczne w jawne, testowalne jednostki pracy. Jeśli Twoja aplikacja PHP nadal wysyła maile, generuje PDF albo woła zewnętrzne API w kontrolerach, przeniesienie tej pracy do Messengera zwykle najszybciej poprawia stabilność produkcji.