Memindahkan kerja berat ke latar belakang dengan Messenger: konsep message & handler, dispatch ke bus, transport sync vs async (Doctrine, Redis, AMQP), menjalankan worker, serta retry dan failure handling dengan messenger:failed:retry.

Di episode 9 kita belajar command untuk pekerjaan terjadwal. Episode 10 menjawab masalah yang berbeda: pekerjaan yang harus dimulai dari dalam aplikasi tetapi tidak boleh memperlambat response — kirim email, buat thumbnail, sinkronisasi data ke pihak ketiga. Jawabannya adalah Messenger, queue system bawaan Symfony.
Mengapa penting? Bayangkan POST /articles yang harus mengirim 500 email konfirmasi di dalam request yang sama — pengguna menunggu puluhan detik. Dengan Messenger, kita hanya memasukkan pesan ke antrian (milidetik), lalu worker memproses email di latar belakang. Response cepat, beban tidak menumpuk, dan jika gagal bisa dicoba ulang.
Messenger memakai pola message/handler:
SendWelcomeEmail.SendWelcomeEmailHandler.MessageBusInterface::dispatch($message) meneruskan message ke handler yang cocok.<?php
namespace App\Message;
final class SendWelcomeEmail
{
public function __construct(
public readonly string $userId,
) {
}
}use App\Message\SendWelcomeEmail;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class SendWelcomeEmailHandler
{
public function __construct(
private readonly MailerInterface $mailer,
) {
}
public function __invoke(SendWelcomeEmail $message): void
{
$this->mailer->send(
(new Email())
->to("user-{$message->userId}@example.com")
->subject('Selamat datang!')
->html('<p>Terima kasih telah mendaftar.</p>')
);
}
}Attribute #[AsMessageHandler] mendaftarkan handler otomatis (autoconfigure). Dispatch dari mana pun — controller, command, service:
use Symfony\Component\Messenger\MessageBusInterface;
public function register(User $user, MessageBusInterface $bus): void
{
// ... simpan user ...
$bus->dispatch(new SendWelcomeEmail($user->getId()));
}Secara default message diproses sync — tepat di titik dispatch, tidak ada kecepatan yang didapat. Perubahan terjadi saat kita menetapkan transport async:
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
App\Message\SendWelcomeEmail: asyncrouting menentukan message mana yang harus lewat antrian. DSN menentukan backend antrian:
| DSN | Backend | Cocok untuk |
|---|---|---|
sync:// | Proses langsung | Dev, testing |
doctrine://default | Tabel di database | Prod sederhana, tidak butuh infra baru |
redis://localhost:6379 | Redis list | Prod throughput sedang |
amqp://guest:guest@localhost:5672 | RabbitMQ | Prod enterprise, routing kompleks |
Untuk mulai praktik, pakai transport database:
MESSENGER_TRANSPORT_DSN=doctrine://defaultLalu siapkan tabel antrian dan mulai worker:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
php bin/console messenger:consume asyncsymfony console messenger:consume async (lewat Symfony CLI ada bonus: restart otomatis saat kode berubah) akan memblokir terminal dan memproses antrian terus-menerus — ini yang berjalan di production sebagai service.
Ada godaan: "kecilkan beban, jadikan async tapi jalankan di dalam request juga." Jangan. Worker harus berupa proses terpisah agar:
Pekerjaan yang gagal tidak boleh hilang diam-diam. Messenger menyediakan retry otomatis dengan backoff, dan memindahkan pesan yang gagal permanen ke transport failed:
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay_ms: 1000
multiplier: 2
failure_transport: failedLalu tambahkan transport failed (juga doctrine://default atau sync:// di dev). Setelah gagal beberapa kali, message pindah ke sana:
php bin/console messenger:failed:show # daftar pesan gagal
php bin/console messenger:failed:retry # coba lagi semua
php bin/console messenger:failed:remove --force # buangAlur yang sehat di produksi: cek messenger:failed:show secara rutin (bisa via cron), retry setelah bug diperbaiki, dan amati log worker. Detail observability-nya kita bahas di episode 15.
Warning
Message yang masuk antrian tidak selalu memproses data terbaru. Jangan menaruh entity dalam message — kirim id dan minta handler memuat ulang dari database (find()). Objek yang di-serialize di tengah perubahan bisa basi dan menyimpan state yang tidak konsisten.
Messenger bukan hanya untuk email — pola umum lain:
| Use case | Message |
|---|---|
| Email/notification | SendWelcomeEmail (di atas) |
| Processing file | ProcessImage(fileId) |
| Third-party sync | SyncProductToCatalog(productId) |
| Batch report | GenerateDailyReport(date) |
Yang penting: message tetap kecil dan idempotent-friendly. Karena retry bisa mengirim dua kali, handler harus aman dijalankan berulang (misal mengecek apakah sudah diproses).
Pada episode 10 ini, kalian telah memindahkan kerja berat ke latar belakang.
Inti yang harus dibawa pulang:
routing + transport DSN.doctrine:// untuk mulai, redis:///amqp:// untuk skala.messenger:consume) wajib berjalan sebagai proses terpisah.failure_transport untuk pesan yang gagal permanen.Di episode 11 selanjutnya, aplikasi kita mulai bicara ke dunia luar: HTTP Client & HttpClient — memanggil API pihak ketiga, mengatur timeout & auth, retry otomatis, streaming response, dan mocking untuk testing. Sampai jumpa di episode 11!