Once order 1042 is paid, its invoice needs a PDF and an email. They can run as two jobs, and you only want to mark the invoice sent when both have succeeded. A batch is that group, and then() is the callback for “all of them worked”.
Queues in production · Lesson 2
Batches and then()
Video
01 Run the follow-up when the batch succeeds
Each job in the batch uses the Batchable trait. then() runs when every job has succeeded. A failure cancels the jobs still waiting, catch() runs, and then() does not. finally() runs either way.
Bus::batch([
new GenerateInvoicePdf($invoice->id),
new EmailInvoice($invoice->id),
])
->name('invoice-'.$invoice->id)
->then(function (Batch $batch) use ($invoice): void {
$invoice->forceFill(['sent_at' => now()])->save();
})
->catch(function (Batch $batch, Throwable $exception): void {
report($exception);
})
->dispatch();allowFailures() lets the remaining jobs continue after one fails. then() still waits for a batch with no failures. Do not mark the invoice sent in finally(): that callback also runs when the PDF failed and the email never went out.
A test that only cares the batch was dispatched uses Bus::fake() and Bus::assertBatched(). A single job, dispatched on its own, is the case the testing series fakes with Queue::fake().
02 Stop a job when the batch has already been cancelled
A failure cancels the jobs that have not started. A job that has started should notice. The Batchable trait gives handle() the batch, and cancelled() is the check. Returning leaves that job successful. It does not count as another failure.
class GenerateInvoicePdf implements ShouldQueue
{
use Batchable, Queueable;
public function __construct(public int $invoiceId)
{
}
public function handle(): void
{
if ($this->batch()?->cancelled()) {
return;
}
Invoice::query()->findOrFail($this->invoiceId)->writePdf();
}
}then() still does not run after a cancellation. The PDF method is skipped, the email job that was waiting is not run, and sent_at stays null. finally() can record that the batch ended. It must not be the place that marks the invoice sent.
Name the batch with the invoice id, as the dispatch already does. A failed batch is then findable in Horizon without opening every payload. The name is a label. It is not the lock, and it does not replace the cancelled() check inside the job. Two batches can share a name. The id on the job is what handle() loads.
Recap
You can mark an invoice sent only when the PDF job and the email job have both succeeded.
then()runs when the batch has no failures.finally()also runs when it failed.allowFailures()does not makethen()ignore a failure.- A job that has started should return when the batch is already cancelled.
Check yourself · 3 questions
Question 1 of 3The PDF job throws. Where do you set sent_at?