Job Class: Bungkus Tugas Background
Buat job dedicated untuk tugas berat yang reusable.
Kapan Butuh Job Class
Tidak semua pekerjaan background butuh class tersendiri. Mail::queue() dan Notification yang ShouldQueue sudah cukup untuk kasus sederhana. Job class dibutuhkan saat tugasnya punya logika sendiri yang reusable, butuh retry/backoff khusus, atau melibatkan beberapa langkah (ambil file, proses, upload hasil, catat ke database). Contoh klasik: resize gambar, generate laporan, sinkronisasi ke API pihak ketiga, dan render video.
Anatomi Job yang Matang
php artisan make:job ResizeImage// app/Jobs/ResizeImage.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class ResizeImage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// berapa kali boleh dicoba sebelum menyerah
public int $tries = 3;
// jeda antar percobaan: 30 dtk, 2 mnt, 10 mnt
public function backoff(): array
{
return [30, 120, 600];
}
public function __construct(public string $path) {}
public function middleware(): array
{
// jangan proses file yang sama 2x bersamaan
return [new WithoutOverlapping($this->path)];
}
public function handle(): void
{
$gambar = Storage::disk('public')->get($this->path);
// ... resize dengan Intervention Image ...
Storage::disk('public')->put('thumbs/'.$this->path, $hasil);
}
public function failed(\Throwable $e): void
{
// dipanggil setelah semua percobaan habis
logger()->error('Resize gagal total', [
'path' => $this->path,
'error' => $e->getMessage(),
]);
}
}Setiap properti di sini punya alasan: $tries mencegah retry selamanya, backoff() memberi napas ke API eksternal yang sedang down (retry langsung 3x dalam 5 detik ke API yang down itu sia-sia), dan WithoutOverlapping mencegah dua worker mengerjakan file yang sama saat job ter-dispatch ganda.
Dispatch: Lebih dari Sekadar dispatch()
use App\Jobs\ResizeImage;
ResizeImage::dispatch($path); // sekarang
ResizeImage::dispatch($path)->delay(now()->addHour()); // tunda 1 jam
ResizeImage::dispatch($path)->onQueue('gambar'); // antrean khusus
ResizeImage::dispatch($path)->onConnection('redis'); // koneksi khusus
// rantai: jalan berurutan, berhenti kalau satu gagal
use Illuminate\Support\Facades\Bus;
Bus::chain([
new DownloadVideo($url),
new RenderVideo($id),
new UploadKeYoutube($id),
])->dispatch();
// batch: banyak job paralel + callback selesai
$batch = Bus::batch([
new ResizeImage('a.jpg'),
new ResizeImage('b.jpg'),
])->then(function ($batch) {
logger()->info('Semua resize selesai!');
})->dispatch();Chain cocok untuk alur yang dependen (tidak ada gunanya render video kalau download gagal). Batch cocok untuk tugas independen yang ingin dipantau sebagai satu kesatuan.
Idempotency: Aturan Paling Penting
Worker bisa mengeksekusi job yang sama dua kali: retry setelah timeout, atau retry_after yang terlalu pendek membuat job dianggap gagal padahal masih jalan. Maka handle() harus idempotent: dijalankan 2x hasilnya sama seperti 1x.
Contoh buruk: job kirim email yang selalu Mail::send() tanpa cek. Kalau di-retry, user dapat email ganda. Contoh benar:
public function handle(): void
{
$pesanan = Pesanan::find($this->pesananId);
// guard: jangan kirim ulang
if ($pesanan->email_terkirim) {
return;
}
Mail::to($pesanan->user)->send(new InvoiceMail(...));
$pesanan->update(['email_terkirim' => true]);
}Pola "cek dulu, kerjakan, tandai" ini berlaku untuk semua job yang berefek samping.
Jebakan Umum
Melewatkan model besar tanpa SerializesModels. Job di-serialize ke JSON di tabel jobs. Model dengan relasi yang sudah di-load ikut terserialize semua, payload membengkak. Trait SerializesModels hanya menyimpan class + ID, lalu me-load ulang model fresh saat job dieksekusi.
Properti yang tidak bisa di-serialize. Closure, resource file, atau koneksi DB di constructor akan meledak dengan SerializationException. Constructor hanya boleh menerima tipe sederhana: string, int, array, atau model.
$this->delete() / $this->release() sembarangan. InteractsWithQueue memberi kontrol manual, tapi 99% kasus cukup andalkan $tries dan exception. Kontrol manual hanya untuk logika retry yang sangat spesifik.
Catatan teknis: Untuk job yang benar-benar tidak boleh gagal diam-diam (misal sinkronisasi pembayaran), kombinasikan
failed()dengan notifikasi ke dirimu sendiri, dan pertimbangkan antreanfailed_jobssebagai inbox yang kamu cek rutin, bukan tempat sampah yang tidak pernah dibuka.
Tantangan
Job Laporan
Buat job GenerateLaporan yang menulis file CSV berisi 1000 baris dummy ke storage. Dispatch dengan delay 1 menit, pantau di tabel jobs sampai dieksekusi worker, verifikasi file-nya ada.