LaravelQueueJobMahir4 mnt baca

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

bash
php artisan make:job ResizeImage
php
// 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()

php
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:

php
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 antrean failed_jobs sebagai 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.