<? phpukraine СПІВБЕСІДИ
Пошук по платформі
LARAVEL · MIDDLE

Чим відрізняються queue jobs, events і listeners?

Event описує факт, що щось сталося; listener реагує на нього; job — це одиниця відкладеної роботи в черзі, яку хтось явно поставив.

Коли робити job, а коли event із listener?
Що станеться, якщо queued listener впаде?
Як правильно відправити лист після реєстрації користувача?
черги події ідемпотентність

Різниця перш за все семантична. Event описує факт, який уже стався: OrderPaid, UserRegistered. Він не знає, хто на нього відреагує, і скільки буде слухачів. Listener підписується на подію й виконує реакцію. Job — це команда: конкретна одиниця роботи, яку хтось явно поставив у чергу, наприклад GenerateInvoicePdf.

Технічно межа розмита. Listener, який реалізує ShouldQueue, стає окремим job у черзі, зі своїми tries, backoff і failed(). Тому питання «job чи listener» зводиться до того, чи потрібна розвʼязка через подію. Якщо на факт реагує кілька незалежних дій, подія дає їм незалежні спроби й незалежні падіння. Якщо потрібна одна важка операція з ланцюжком або батчем, це job.

Все, що йде в чергу, виконується щонайменше один раз. Retry гарантований, воркер може впасти після виконання дії, але до підтвердження. Тому обробник мусить бути ідемпотентним: перевіряти стан агрегату перед дією, а не вірити, що його викликали вперше.

Друга типова пастка — транзакції. Подія, відправлена всередині DB::transaction, може дістатись воркера раніше, ніж транзакція закомітилась, і слухач не знайде запис. Рішення: ShouldDispatchAfterCommit на події, afterCommit() на job або глобальна опція after_commit у конфігурації черги.

// Подія: факт у минулому часі, без знання про споживачів
final class OrderPaid implements ShouldDispatchAfterCommit
{
    public function __construct(public readonly int $orderId) {}
}

// Слухач у черзі: окремий job на кожен listener, незалежні падіння
final class SendReceipt implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function handle(OrderPaid $event): void
    {
        $order = Order::findOrFail($event->orderId);

        if ($order->receipt_sent_at !== null) {
            return; // ідемпотентність: повтор не шле другий лист
        }

        Mail::to($order->email)->send(new ReceiptMail($order));
        $order->update(['receipt_sent_at' => now()]);
    }

    public function failed(OrderPaid $event, Throwable $e): void
    {
        Log::error('Receipt failed', ['order' => $event->orderId, 'error' => $e->getMessage()]);
    }
}

// Job: явна команда зробити конкретну роботу
GenerateInvoicePdf::dispatch($order->id)->onQueue('pdf')->afterCommit();

DB::transaction(function () use ($order) {
    $order->markPaid();
    OrderPaid::dispatch($order->id); // піде після коміту
});
Семантику: event названий у минулому часі й нічого не знає про споживачів, job це команда зробити конкретну роботу.
Що listener може реалізувати ShouldQueue і тоді виконується у черзі як окремий job на кожен listener.
Що все, що йде в чергу, мусить бути ідемпотентним, бо retry гарантований, а доставка щонайменше один раз.
Що job серіалізується: моделі передаються через SerializesModels як id і перечитуються при виконанні, тому стан може змінитись.
Розуміння, що event з кількома queued listeners дає незалежні спроби й незалежні падіння, а один job із трьома діями впаде цілком.
Казати, що event це відкладена робота, а job синхронна: listener може бути в черзі, а job може виконатись синхронно через dispatchSync.
Диспатчити event всередині транзакції без afterCommit: queued listener стартує до коміту й не знайде запис.
Робити job, який не переживає повтор: другий retry створює другий платіж або відправляє другий лист.
Передавати в job великі масиви або обʼєкти замість id: payload роздувається, а дані застарівають.
Не задавати tries, backoff і failed(): job тихо зникає після першої помилки або повторюється нескінченно.
ПОРАДА

Скажіть, що ключове питання — ідемпотентність: усе, що йде в чергу, мусить безпечно виконуватись повторно після retry. І згадайте afterCommit для подій усередині транзакцій.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Listener може реалізувати ShouldQueue, і тоді різниця з job зводиться до семантики.