Job yang sudah jadi belum berarti apa-apa sampai dikirim ke antrean — dan act pengiriman itu, dispatch, punya lebih banyak variasi daripada sekadar “masukkan dan lupakan”. Kadang kita butuh job berjalan lima menit dari sekarang, kadang hanya boleh terkirim bila kondisi tertentu terpenuhi, dan kadang kita justru ingin eksekusinya terjadi sekarang juga tanpa menyentuh antrean. Laravel menyediakan method untuk semua skenario itu. Pada chapter ini kita pegang satu job SendWelcomeEmail dari chapter sebelumnya dan menjalankannya melewati setiap variasi dispatch, ditutup pengujian nyata lewat controller dan feature test.
Dua Bentuk dispatch #
Ada dua cara memanggil mekanisme dispatch. Bentuk pertama adalah method statis pada class job:
SendWelcomeEmail::dispatch($user);Bentuk kedua adalah helper global dispatch() yang menerima instance job:
dispatch(new SendWelcomeEmail($user));Keduanya menghasilkan hal yang sama — job masuk antrean default connection. Helper dispatch() unggul saat job dibangun bertahap dalam variabel, misalnya dipilih lewat percabangan, sementara bentuk statis lebih ringkas untuk pemanggilan langsung. Yang penting diingat: keduanya mengembalikan object PendingDispatch, dan method lanjutan seperti delay() serta onQueue() bisa dirantai pada hasilnya.
Menunda Eksekusi dengan delay #
Job tidak harus dikerjakan segera. Method delay() menerima DateTimeInterface untuk waktu pasti, atau integer untuk jumlah detik:
use App\Models\User;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Support\Facades\DB;
DB::table('jobs')->delete();
$user = User::where('email', '[email protected]')->firstOrFail();
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
$job = DB::table('jobs')->first();
echo 'job_1_available_dalam: ' . ($job->available_at - time()) . " detik\n";
dispatch(new SendWelcomeEmail($user))->delay(30);
$job = DB::table('jobs')->orderByDesc('id')->first();
echo 'job_2_available_dalam: ' . ($job->available_at - time()) . " detik\n";Output yang Dihasilkan:
job_1_available_dalam: 300 detik
job_2_available_dalam: 30 detikJob berdelay tetap masuk tabel jobs seketika, hanya saja kolom available_at-nya diisi masa depan — lima menit dan 30 detik dari sekarang pada contoh di atas. Worker yang mem-pop antrean melewati job yang available_at-nya belum tiba, lalu kembali menjemputnya begitu waktunya lewat. Ini juga cara kerja percobaan ulang di chapter Job Retries
: job yang gagal dilepas kembali ke antrean dengan available_at baru.
Method onQueue() menyelesaikan trik terakhir penempatan: mengarahkan job ke antrean bernama yang sudah kita lihat di chapter Queue Configuration
:
SendWelcomeEmail::dispatch($user)->onQueue('emails');Baris job tersebut menyimpan emails di kolom queue, menunggu worker yang mengawasi antrean bernama itu.
dispatchSync dan dispatchAfterResponse #
Dua method ini terdengar seperti antitesis queue, tetapi justru memperjelas batasnya. dispatchSync() memaksa eksekusi langsung di proses saat ini, melewati antrean:
use App\Models\User;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Support\Facades\DB;
DB::table('jobs')->delete();
@unlink($dir.'/storage/logs/laravel.log');
$user = User::where('email', '[email protected]')->firstOrFail();
$sebelum = DB::table('jobs')->count();
SendWelcomeEmail::dispatchSync($user);
$sesudah = DB::table('jobs')->count();
echo "jobs_baru_untuk_dispatchSync: " . ($sesudah - $sebelum) . "\n";
echo 'email_langsung_masuk_log: ' . var_export(
str_contains(file_get_contents($dir.'/storage/logs/laravel.log'), 'Subject: Selamat Datang!'), true) . "\n";Output yang Dihasilkan:
jobs_baru_untuk_dispatchSync: 0
email_langsung_masuk_log: trueTidak ada baris baru di tabel jobs karena dispatchSync() mengalihkan job ke connection sync untuk sekali eksekusi ini — berguna saat pekerjaan penting lebih cepat diselesaikan daripada diantrekan, misalnya menghapus cache tepat setelah data berubah.
dispatchAfterResponse() menempati posisi tengah: pekerjaan tetap sinkron di proses yang sama, tetapi ditunda sampai response terkirim ke browser. Kita buktikan lewat request HTTP sungguhan yang melewati kernel Laravel:
use App\Models\User;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@unlink($dir.'/storage/logs/laravel.log');
DB::table('jobs')->delete();
$user = User::where('email', '[email protected]')->firstOrFail();
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$request = Request::create('/kirim-laporan/'.$user->id, 'GET');
$response = $kernel->handle($request);
echo 'isi_response: ' . $response->getContent() . "\n";
echo 'jobs_saat_response: ' . DB::table('jobs')->count() . "\n";
// di aplikasi nyata, response dikirim ke browser di titik ini,
// lalu kernel->terminate() dieksekusi:
$kernel->terminate($request, $response);
echo 'email_di_log_setelah_terminate: ' . var_export(
str_contains(file_get_contents($dir.'/storage/logs/laravel.log'), 'Subject: Selamat Datang!'), true) . "\n";Output yang Dihasilkan:
isi_response: Laporan sedang diproses
jobs_saat_response: 0
email_di_log_setelah_terminate: truePerhatikan: tabel jobs tidak pernah tersentuh — bukan dispatchAfterResponse mengantrekan pekerjaan, melainkan kernel->terminate() yang mengeksekusi handle() setelah response selesai dikirim. Route /kirim-laporan/{user} di runner di atas berisi satu baris SendWelcomeEmail::dispatchAfterResponse($user) lalu mengembalikan teks. Cocok untuk pekerjaan singkat yang ingin terasa “gratis” bagi user tanpa biaya infrastruktur worker — namun ingat, proses PHP tetap sibuk sampai pekerjaan itu tuntas, jadi pekerjaan berat tetap pantas masuk antrean sungguhan.
Dispatch Bersyarat #
Seringkali job hanya layak terkirim bila kondisi tertentu berlaku — user sudah verifikasi email, pesanan berstatus lunas, dan sebagainya. Method dispatchIf() dan dispatchUnless() menerima kondisi sebagai argumen pertama:
use App\Models\User;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Support\Facades\DB;
DB::table('jobs')->delete();
$user = User::where('email', '[email protected]')->firstOrFail();
$emailTerverifikasi = $user->email_verified_at !== null;
SendWelcomeEmail::dispatchIf($emailTerverifikasi, $user);
echo 'jobs_setelah_dispatchIf_false: ' . DB::table('jobs')->count() . "\n";
SendWelcomeEmail::dispatchUnless($emailTerverifikasi, $user);
echo 'jobs_setelah_dispatchUnless_false: ' . DB::table('jobs')->count() . "\n";Output yang Dihasilkan:
jobs_setelah_dispatchIf_false: 0
jobs_setelah_dispatchUnless_false: 1Pada contoh ini [email protected] belum verifikasi email sehingga $emailTerverifikasi bernilai false. Sesuai namanya, dispatchIf(false, ...) tidak mengirim apa pun — antrean tetap kosong. Kebalikannya, dispatchUnless(false, ...) justru mengirim job karena kondisi “kecuali” tidak terpenuhi. Logika terbalik dua method ini sering menjebak; pilih yang membuat kalimat kode terbaca paling alami.
Dispatch dari Controller #
Semua variasi di atas biasanya dipanggil dari controller. Kita lengkapi route pendaftaran sederhana:
use App\Http\Controllers\RegistrationController;
use Illuminate\Support\Facades\Route;
Route::post('/daftar', [RegistrationController::class, 'store'])
->name('daftar.store');<?php
namespace App\Http\Controllers;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RegistrationController extends Controller
{
public function store(Request $request): JsonResponse
{
$user = User::create([
'name' => $request->string('name'),
'email' => $request->string('email'),
'password' => 'rahasia-123',
]);
SendWelcomeEmail::dispatch($user);
return response()->json([
'message' => 'Pendaftaran diterima.',
'user_id' => $user->id,
]);
}
}Controller membuat user, mendispatch job, dan langsung mengembalikan JSON — tidak ada yang menunggu email. Feature test berikut membuktikan dua hal sekaligus: response cepat dan job benar-benar masuk antrean:
use App\Jobs\SendWelcomeEmail;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class JobDispatchTest extends TestCase
{
use RefreshDatabase;
public function test_daftar_mengirim_job_tanpa_memblokir_response(): void
{
// phpunit.xml memakai QUEUE_CONNECTION=sync; kembalikan ke database
config(['queue.default' => 'database']);
$mulai = microtime(true);
$response = $this->postJson('/daftar', [
'name' => 'Siti',
'email' => '[email protected]',
]);
$durasi = round(microtime(true) - $mulai, 3);
$response->assertOk();
fwrite(STDERR, 'status: ' . $response->status() . "\n");
fwrite(STDERR, 'durasi_response: ' . $durasi . " detik\n");
fwrite(STDERR, 'jobs_di_antrean: ' . DB::table('jobs')->count() . "\n");
$job = DB::table('jobs')->first();
fwrite(STDERR, 'nama_queue: ' . $job->queue . "\n");
fwrite(STDERR, 'displayName: ' . json_decode($job->payload)->displayName . "\n");
}
}Output yang Dihasilkan:
status: 200
durasi_response: 0.026 detik
jobs_di_antrean: 1
nama_queue: default
displayName: App\Jobs\SendWelcomeEmail
PASS Tests\Feature\JobDispatchTest
✓ daftar mengirim job tanpa memblokir response 0.15s
Tests: 1 passed (1 assertions)Satu baris config(['queue.default' => 'database']) di awal test menarik perhatian: karena phpunit.xml menyetel driver sync, test ini secara eksplisit memilih connection database agar dispatch benar-benar menulis ke tabel jobs. Hasilnya: response 200 tuntas dalam 26 milidetik dengan satu job menunggu di antrean — persis kontrak yang dijanjikan queue di chapter pertama. Seluruh katalog method dispatch bisa dirujuk di dokumentasi Queues
.
Antrean kini berisi job sungguhan. Pertanyaan berikutnya: siapa yang mengerjakannya, bagaimana ia bekerja, dan bagaimana mengelolanya tanpa mematikan aplikasi? Jawabannya ada di chapter Queue Workers .