Cara Membuat Scheduler dan Job Queues di Laravel

| | | Laravel
Cara Membuat Scheduler dan Job Queues di Laravel

Laravel menyediakan fasilitas untuk beberapa arsitektur aplikasi yang kurang umum, salah satunya adalah Jobs dan Queue. di tulisan ini kita bahas cara mengimplementasikan data antrian, proses pekerjaan yang mengantri, plus Laravel scheduler yang membuat jadwal cron. Studi kasusnya, kita bikin task todo yang otomatis jalan di jam tertentu, lalu diantrekan ke queue.

Apa itu Queue dan Scheduler

Queue itu konsepnya seperti antrian di bank. satu orang dilayani pada satu waktu dari setiap antrian, dan setiap orang pada akhirnya sampai ke depan loket untuk dilayani. di pemrograman sama saja, aplikasi menambahkan “pekerjaan” ke antrian, lalu job worker mengambilnya satu per satu dan mengerjakan sesuai perintah. worker bisa menghapus pekerjaan, mengembalikannya ke antrian dengan penundaan, atau menandainya sukses.

Laravel memudahkan fitur queue ini dengan banyak driver, bisa Redis, beanstalkd, Amazon SQS, atau tabel database. ada juga driver sinkron agar pekerjaan jalan langsung tanpa benar-benar diantrekan, cocok untuk testing.

Sementara scheduler itu penjadwal. dia yang nentuin kapan sebuah task dijalankan, misalnya tiap menit atau tiap hari. jadi gambaran besarnya: scheduler memicu, queue yang mengeksekusi di belakang layar.

di studi kasus ini saya lanjut dari project Membuat REST API CRUD dengan Laravel serta JWT yang sudah kita buat sebelumnya.

Menyiapkan Database

Membuat Migration

kita butuh dua tabel, task untuk data todo dan task_scheduler untuk jadwalnya. jalankan generator migrate:

php artisan make:migration task
php artisan make:migration task_scheduler

isi kode migrate untuk task:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('task', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description');
            $table->integer('user_id');
            $table->enum('status', ['WAIT', 'DONE'])->default('WAIT');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('task');
    }
};

isi kode migrate untuk task_scheduler:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('task_scheduler', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('days');
            $table->time('time');
            $table->string('description');
            $table->integer('user_id');
            $table->text('log_executed');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('task_scheduler');
    }
};

Penjelasan :

  • Schema::create('task_scheduler', ...) : membuat tabel baru ketika command migrate dijalankan
  • $table->id(); : kolom id dengan auto increment
  • $table->string('days'); : kolom hari, disimpan sebagai string berisi daftar hari dipisah koma
  • $table->time('time'); : kolom jam dengan tipe data TIME
  • $table->text('log_executed'); : menyimpan catatan kapan task terakhir dieksekusi
  • $table->timestamps(); : membuat kolom created_at dan updated_at otomatis

setelah file migrate sesuai, jalankan:

php artisan migrate

Membuat Model

lanjut buat model untuk mapping data:

php artisan make:model Task
php artisan make:model TaskScheduler

isi app/Models/Task.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;
    protected $table = 'task';

    protected $fillable = [
        'name', 'description', 'user_id', 'status'
    ];
}

isi app/Models/TaskScheduler.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class TaskScheduler extends Model
{
    use HasFactory;
    protected $table = 'task_scheduler';

    protected $fillable = [
        'name', 'days', 'time', 'description', 'user_id', 'log_executed',
    ];
}

Membuat Job

buat job dengan artisan:

php artisan make:job TaskSchedulerJob

Penjelasan :

  • perintah di atas membuat file app/Jobs/TaskSchedulerJob.php. isinya nanti yang mengerjakan task ketika antrian jalan

lalu edit app/Jobs/TaskSchedulerJob.php:

<?php

namespace App\Jobs;

use App\Models\Task;
use App\Models\TaskScheduler;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class TaskSchedulerJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $schedulerId;
    public $userId;
    public $taskData;

    public function __construct($schedulerId, $userId, $taskData)
    {
        $this->schedulerId = $schedulerId;
        $this->userId      = $userId;
        $this->taskData    = $taskData;
    }

    public function handle(): void
    {
        // buat task dari data yang dikirim
        $task = Task::create([
            'name'        => $this->taskData['name'],
            'description' => $this->taskData['description'],
            'user_id'     => $this->userId,
            'status'      => 'DONE',
        ]);

        // catat log eksekusi di scheduler
        TaskScheduler::where('id', $this->schedulerId)
            ->update(['log_executed' => now()->toDateTimeString()]);
    }
}

Penjelasan :

  • implements ShouldQueue : memberitahu Laravel bahwa job ini harus diantrekan
  • handle() : method yang dieksekusi worker ketika job diambil dari antrian
  • di dalam handle() kita bikin task baru dan memperbarui log_executed supaya ada jejak eksekusinya

Menyiapkan Repository

saya pakai Repository Pattern, jadi akses database dipisah dari controller. buat file app/Repositories/TaskScheduleRepository.php:

<?php

namespace App\Repositories;

use App\Models\TaskScheduler;
use Illuminate\Support\Facades\Auth;

class TaskScheduleRepository
{
    private $user;

    public function __construct()
    {
        $this->user = Auth::user();
    }

    public function getAll()
    {
        return TaskScheduler::where('user_id', $this->user->id)->get();
    }

    public function getById($id)
    {
        return TaskScheduler::where('user_id', $this->user->id)
            ->where('id', $id)
            ->firstOrFail();
    }

    public function create($input)
    {
        $input['days']    = implode(',', $input['days']);
        $input['user_id'] = $this->user->id;

        return TaskScheduler::create($input);
    }

    public function update($input, $id)
    {
        $task = TaskScheduler::where('user_id', $this->user->id)->findOrFail($id);
        $input['days'] = implode(',', $input['days']);

        $task->update($input);
        return $task;
    }

    public function delete($id)
    {
        $task = TaskScheduler::where('user_id', $this->user->id)->findOrFail($id);
        $task->delete();
        return $task;
    }
}

Penjelasan :

  • implode(',', $input['days']) : mengubah array hari (Monday, Tuesday) jadi string “Monday,Tuesday” untuk disimpan di kolom days
  • firstOrFail / findOrFail : otomatis melempar 404 kalau data tidak ditemukan, jadi tidak perlu blok try-catch berulang

Membuat Controller

buat controller:

php artisan make:controller TaskScheduleController

lalu isi app/Http/Controllers/TaskScheduleController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Repositories\TaskScheduleRepository;
use App\Models\TaskScheduler;
use App\Jobs\TaskSchedulerJob;
use Illuminate\Support\Facades\Log;

class TaskScheduleController extends Controller
{
    public function index()
    {
        $repoTask   = new TaskScheduleRepository;
        $response   = $repoTask->getAll();

        return response()->json([
            'status' => true,
            'task'   => $response,
        ], 200);
    }

    public function create(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name'        => 'required|string',
            'days'        => 'required|array|in:Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday',
            'time'        => 'required|string|date_format:H:i',
            'description' => 'required|string',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status'  => false,
                'message' => $validator->messages()->first(),
            ], 400);
        }

        $repoTask   = new TaskScheduleRepository;
        $response   = $repoTask->create($request->all());

        return response()->json([
            'status' => true,
            'message' => 'Berhasil Membuat Task Scheduler baru',
            'task'   => $response,
        ], 201);
    }

    public function show($id)
    {
        $repoTask   = new TaskScheduleRepository;
        $response   = $repoTask->getById($id);

        return response()->json([
            'status' => true,
            'task'   => $response,
        ], 200);
    }

    public function update(Request $request, $id)
    {
        $validator = Validator::make($request->all(), [
            'name'        => 'required|string',
            'days'        => 'required|array|in:Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday',
            'time'        => 'required|string|date_format:H:i',
            'description' => 'required|string',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'status'  => false,
                'message' => $validator->messages()->first(),
            ], 400);
        }

        $repoTask   = new TaskScheduleRepository;
        $response   = $repoTask->update($request->all(), $id);

        return response()->json([
            'status' => true,
            'message' => 'Berhasil Update task',
            'task'   => $response,
        ], 200);
    }

    public function destroy($id)
    {
        $repoTask   = new TaskScheduleRepository;
        $repoTask->delete($id);

        return response()->json([
            'status'  => true,
            'message' => 'Berhasil Delete task',
        ], 200);
    }

    public function handleJob()
    {
        $day  = now()->format('l');
        $time = now()->setTimezone('+07:00')->format('H:i');

        $schedulers = TaskScheduler::where('days', 'like', '%' . $day . '%')
            ->where('time', '=', $time)
            ->get();

        $executedIds = [];

        foreach ($schedulers as $scheduler) {
            $taskData = [
                'name'        => $scheduler->name,
                'description' => $scheduler->description,
            ];

            // kirim ke antrian, bukan eksekusi langsung
            TaskSchedulerJob::dispatch($scheduler->id, $scheduler->user_id, $taskData)
                ->onQueue('task_scheduler');

            $executedIds[] = $scheduler->id;
        }

        Log::info('TaskScheduler handleJob dijalankan', ['ids' => $executedIds]);

        return [
            'status'                    => count($executedIds) > 0 ? 'success' : 'empty',
            'total_executed_scheduler'  => count($executedIds),
            'executed_idSchedulers'     => $executedIds,
        ];
    }
}

Penjelasan :

  • method handleJob berfungsi sebagai trigger dari cron. dia mencari scheduler yang hari dan jamnya sesuai sekarang, lalu men-dispatch job ke queue
  • TaskSchedulerJob::dispatch(...)->onQueue('task_scheduler') : mengirim job ke antrian bernama task_scheduler, bukan mengeksekusi langsung

Membuat Route Endpoint

tambahkan di routes/api.php:

use App\Http\Controllers\TaskScheduleController;

Route::middleware('auth:sanctum')->group(function () {
    Route::get('task-schedule', [TaskScheduleController::class, 'index']);
    Route::post('task-schedule', [TaskScheduleController::class, 'create']);
    Route::get('task-schedule/{id}', [TaskScheduleController::class, 'show']);
    Route::put('task-schedule/{id}', [TaskScheduleController::class, 'update']);
    Route::patch('task-schedule/{id}', [TaskScheduleController::class, 'update']);
    Route::delete('task-schedule/{id}', [TaskScheduleController::class, 'destroy']);
});

// panggil manual tanpa menunggu cron, untuk testing di localhost
Route::get('task-schedule/handleJob', [TaskScheduleController::class, 'handleJob']);

Penjelasan :

  • route CRUD dibungkus auth:sanctum karena butuh login
  • route task-schedule/handleJob sengaja di luar middleware supaya gampang di-test dari browser, tinggal akses URL-nya langsung

Menjalankan Scheduler dan Queue

setelah semua siap, ada dua proses yang perlu jalan.

1. Menjalankan Queue Worker

queue worker yang mengambil job dari antrian. sintaks yang benar:

php artisan queue:work redis --queue=task_scheduler --tries=3 --timeout=0

Penjelasan :

  • queue:work redis : driver queue-nya Redis (kalau pakai database, ganti jadi queue:work database)
  • --queue=task_scheduler : nama antrian yang diproses, di sini task_scheduler
  • --tries=3 : mencoba ulang maksimal 3 kali kalau job gagal
  • --timeout=0 : tanpa batas waktu eksekusi (hati-hati untuk job yang bisa infinite loop)

2. Menjalankan Scheduler

di Laravel 11, cukup jalankan perintah ini dan dia akan terus jalan memeriksa jadwal tiap menit:

php artisan schedule:work

untuk production, tambahkan entry cron di server supaya scheduler jalan terus:

* * * * * cd /path/to/rest-api-laravel && php artisan schedule:run >> /dev/null 2>&1

3. Mendaftarkan Jadwal di Kernel

untuk Laravel 10 ke bawah, daftarkan jadwal di app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->call('\App\Http\Controllers\TaskScheduleController@handleJob')->everyMinute();
}

di Laravel 11 ke atas, buat command artisan atau gunakan route handleJob yang sudah kita buat, lalu panggil dari cron.

Alur lengkapnya

  1. user membuat jadwal lewat API POST /api/task-schedule dengan hari dan jam
  2. scheduler (via cron / schedule:work) memanggil handleJob tiap menit
  3. handleJob mencari jadwal yang cocok dengan hari dan jam sekarang
  4. setiap jadwal yang cocok di-dispatch ke queue task_scheduler
  5. queue worker mengambil job dan menjalankan TaskSchedulerJob::handle(), task baru dibuat di tabel task

Pertanyaan yang Sering Diajukan

Apa beda scheduler dan queue di Laravel?

Scheduler mengatur kapan task dijalankan (jadwal cron, misal tiap menit atau tiap hari jam 9). Queue mengatur bagaimana task berat dijalankan di belakang layar lewat worker, supaya request utama tidak diblokir.

Bagaimana cara menjalankan scheduler di Laravel localhost?

Jalankan php artisan schedule:work (Laravel 11+) atau schedule:run. Untuk production, tambahkan entry cron * * * * * cd /path/project && php artisan schedule:run >> /dev/null 2>&1.

Kenapa queue worker harus selalu berjalan?

Karena job yang di-dispatch hanya dieksekusi oleh worker. Kalau worker mati, job mengantri selamanya. Di production biasanya dipantau dengan supervisor supaya worker restart otomatis kalau mati.

Sintaks queue:work yang benar seperti apa?

php artisan queue:work redis --queue=task_scheduler --tries=3 --timeout=0. Perhatikan: driver (redis) ditulis sebagai opsi queue:work redis, bukan digabung setelah nama queue.

Sigit Nurhanafi avatar
Full-stack developer & technical writer. Berpengalaman di PHP, Laravel, NodeJS, MySQL, dan Python. Aktif menulis tutorial pemrograman dan maintaining open-source projects di PemburuKode sejak 2021.