Черги гарантують доставку «хоча б раз», а не «рівно раз»: завдання може виконатися повторно після таймауту, падіння воркера чи ручного queue:retry. Ідемпотентне завдання при повторі не робить шкоди - не списує гроші двічі й не шле другий лист.
Прийоми, від простого до надійного:
1. Перевірка стану перед дією:
public function handle(): void
{
if ($this->order->paid_at !== null) {
return; // уже оброблено
}
// ...
}
Сама по собі має вікно гонки: два воркери можуть одночасно побачити «не оплачено».
2. Обмеження бази як гарантія. Унікальний індекс на payments.order_id перетворює другу вставку на помилку, а не на дубль. Перевірку в коді можна обійти, обмеження в базі - ні.
3. Ключ ідемпотентності для зовнішніх API. Платіжні сервіси приймають заголовок на кшталт Idempotency-Key: повторний запит з тим самим ключем поверне перший результат, а не створить новий платіж.
4. Поділ на кроки. Не «спиши кошти й надішли лист» в одному завданні, а запис наміру в базу, списання, і лист окремим завданням після успіху - щоб повтор листа не тягнув повтор списання.
Плюс afterCommit(), щоб завдання не стартувало раніше, ніж закомічено транзакцію, яка його породила.