Blog

  • Concurrency Adalah? Cara Kerja Concurrency di Laravel 12

    Concurrency adalah kemampuan menjalankan beberapa proses secara bersamaan (paralel), bukan satu per satu secara berurutan. Di dunia web modern, concurrency artinya bisa memanggil tiga API eksternal sekaligus dan menunggu semuanya selesai — dibanding menunggu satu selesai, baru mulai berikutnya. Laravel 12 memperkenalkan Concurrency facade yang membuat pola ini jauh lebih mudah diimplementasikan di aplikasi PHP.

    Artikel ini menjelaskan apa itu Concurrency di Laravel 12, bagaimana cara kerjanya secara teknis, dan kapan sebaiknya dipakai — beserta kasus di mana lebih baik tidak menggunakannya.

    Apa Itu Concurrency di Laravel 12?

    Concurrency adalah teknik eksekusi paralel di mana beberapa operasi berjalan secara bersamaan, bukan berurutan. Dalam konteks Laravel, concurrency artinya Anda bisa menjalankan beberapa closure PHP secara paralel dan mengumpulkan semua hasilnya sekaligus.

    Concurrency facade di Laravel 12 memungkinkan Anda menjalankan beberapa closure secara bersamaan (paralel) dan menunggu semua hasilnya. Ini berguna saat Anda punya beberapa operasi yang tidak bergantung satu sama lain dan masing-masing membutuhkan waktu, seperti memanggil beberapa API eksternal atau query ke database berbeda.

    Tanpa concurrency (sequential):

    // Total waktu: 1 + 2 + 1.5 = ~4.5 detik
    $weather  = fetchWeather($city);     // 1 detik
    $stock    = fetchStockPrice($code);  // 2 detik
    $exchange = fetchExchangeRate();     // 1.5 detik

    Dengan concurrency (paralel):

    use Illuminate\Support\Facades\Concurrency;
    
    // Total waktu: ~2 detik (terlama)
    [$weather, $stock, $exchange] = Concurrency::run([
        fn() => fetchWeather($city),
        fn() => fetchStockPrice($code),
        fn() => fetchExchangeRate(),
    ]);

    Bagaimana Cara Kerjanya?

    Laravel Concurrency bekerja dengan membuka beberapa proses PHP secara paralel (bukan thread). Setiap closure dijalankan di child process terpisah, hasilnya di-serialize dan dikembalikan ke parent process.

    Ini berbeda dengan async programming di Node.js atau Go. Laravel Concurrency tidak non-blocking event loop, melainkan multi-process. Artinya ada overhead untuk setiap proses baru, jadi tidak ideal untuk tugas yang sangat pendek (di bawah 100ms).

    Contoh Penggunaan Nyata

    Dashboard analytics yang perlu data dari beberapa sumber:

    use Illuminate\Support\Facades\Concurrency;
    
    [$orders, $revenue, $topProducts, $visitors] = Concurrency::run([
        fn() => Order::thisMonth()->count(),
        fn() => Order::thisMonth()->sum('total'),
        fn() => Product::topSelling(5)->get(),
        fn() => Analytics::uniqueVisitors(today()),
    ]);
    
    return view('dashboard', compact('orders', 'revenue', 'topProducts', 'visitors'));

    Tanpa concurrency, kalau tiap query butuh 200ms, total jadi 800ms. Dengan concurrency, total sekitar 200ms (sesuai yang terlama).

    Defer: Jalankan Setelah Response Dikirim

    Laravel 12 juga punya Concurrency::defer() — mirip dengan concurrency, tapi closure dijalankan setelah response HTTP sudah dikirim ke browser.

    use Illuminate\Support\Facades\Concurrency;
    
    // Response langsung dikirim ke user
    // Tiga closure di bawah jalan di background
    Concurrency::defer([
        fn() => $this->updateLastLogin($user),
        fn() => $this->logActivity($user, 'login'),
        fn() => $this->syncWithCRM($user),
    ]);
    
    return redirect()->intended('dashboard');

    Berguna untuk aksi “fire and forget” yang tidak perlu hasilnya sebelum response dikirim.

    Kapan Pakai, Kapan Tidak?

    Cocok untuk:

    • Beberapa panggilan API eksternal yang independent
    • Query ke database berbeda (multi-connection) yang tidak saling bergantung
    • Proses komputasi berat yang bisa diparalelkan

    Tidak cocok untuk:

    • Tugas sangat cepat (overhead proses lebih mahal daripada keuntungannya)
    • Operasi yang saling bergantung (output A dibutuhkan input B)
    • Operasi yang butuh akses ke state yang sama (tidak ada shared memory antar proses)

    Concurrency vs Queue vs Async: Apa Bedanya?

    Banyak developer bingung membedakan tiga konsep ini. Berikut perbedaannya dalam konteks Laravel:

    • Concurrency (Laravel 12) — Proses berjalan paralel, hasilnya ditunggu langsung di request yang sama. Ideal untuk operasi I/O yang independent dalam satu request.
    • Queue (Laravel Queue) — Tugas dikirim ke antrian dan dikerjakan di background oleh worker terpisah. Tidak menunggu hasilnya. Ideal untuk email, notifikasi, proses besar yang tidak perlu hasilnya segera.
    • Async (JavaScript/Node.js) — Non-blocking event loop. PHP secara native bukan bahasa async, jadi Laravel Concurrency mensimulasikannya via multi-process.

    Aturan sederhana: Butuh hasilnya sekarang di dalam request? → Gunakan Concurrency. Tidak butuh hasilnya segera? → Gunakan Queue.

    Pitfalls dan Hal yang Perlu Diperhatikan

    Tidak Ada Shared State Antar Proses

    Karena setiap closure berjalan di child process terpisah, tidak ada shared memory. Variabel, singleton, atau cache in-memory yang diubah di satu closure tidak akan terlihat di closure lain atau di parent process.

    Exception Handling

    Jika salah satu closure throw exception, Concurrency::run() akan menangkapnya dan throw ulang di parent process. Pastikan setiap closure ditangani dengan baik:

    [$result1, $result2] = Concurrency::run([
        fn() => safeApiCall1(),
        fn() => safeApiCall2(),
    ]);

    Database Connection per Process

    Setiap child process butuh koneksi database sendiri. Pastikan database connection pool Anda cukup untuk menampung jumlah proses konkuren yang Anda jalankan.

    Baca Juga

    FAQ: Pertanyaan Umum tentang Concurrency di Laravel

    Apa itu concurrency dalam pemrograman?

    Concurrency adalah kemampuan sistem menjalankan beberapa proses atau operasi secara bersamaan. Concurrency artinya tidak ada satu proses yang harus menunggu proses lain selesai sebelum bisa mulai — semua bisa berjalan paralel.

    Apa perbedaan concurrency dan parallelism?

    Concurrency adalah konsep logis (beberapa pekerjaan dalam proses yang tumpang-tindih), sementara parallelism adalah eksekusi fisik secara bersamaan di beberapa CPU core. Laravel Concurrency di balik layar menggunakan multi-process, yang merupakan bentuk true parallelism.

    Apakah Laravel Concurrency tersedia di Laravel 11?

    Concurrency facade diperkenalkan di Laravel 11 (sebagai fitur eksperimental) dan disempurnakan di Laravel 12. Jika Anda menggunakan Laravel 11.x, pastikan versi Laravel Anda sudah mendukung Illuminate\Support\Facades\Concurrency.

    Apakah laravel concurrency aman untuk production?

    Ya, aman untuk production selama Anda memahami limitasinya: tidak ada shared state antar proses, exception harus ditangani dengan baik, dan jumlah concurrent process harus sesuai dengan kapasitas server (RAM dan database connections).

    Concurrency artinya apa dalam bahasa Indonesia?

    Concurrency artinya “kebersamaan” atau “pemrosesan bersamaan” — kemampuan menjalankan beberapa tugas secara paralel. Dalam konteks Laravel, concurrency artinya beberapa closure PHP dieksekusi secara bersamaan di proses terpisah.

    Tertarik Membangun Aplikasi Laravel yang Performant?

    Memahami fitur seperti Concurrency adalah bagian dari membangun aplikasi Laravel yang benar-benar scalable. Jika tim Anda butuh bantuan merancang arsitektur aplikasi PHP yang efisien — mulai dari pemilihan teknik pemrosesan yang tepat hingga deployment di server production — tim Arrazy Inovasi siap membantu.

    Ada proyek yang ingin didiskusikan? Konsultasi gratis dengan tim kami.

  • Tutorial Laravel 12: Menggunakan Context Facade untuk Propagasi Data

    Tutorial Laravel 12: Menggunakan Context Facade untuk Propagasi Data

    Laravel 12 nambah fitur baru namanya Context. Kalau kamu pernah bingung gimana caranya nyebarin data kayak tenant_id, request_id, atau user_id ke semua log, job, dan event tanpa harus ngoper variabel ke mana-mana — ini solusinya.

    Tutorial ini bahas Context dari dasar sampai implementasi nyata. Cocok buat kamu yang udah familiar sama Laravel tapi belum pernah pakai Context facade.

    Apa Itu Context di Laravel 12?

    Context adalah cara Laravel nyimpen data yang terikat sama satu request atau satu eksekusi. Data ini bisa diakses dari mana aja tanpa harus ngoper variabel ke tiap fungsi atau class.

    Bayangin kamu punya aplikasi multi-tenant. Setiap request butuh tahu tenant_id-nya siapa. Biasanya kamu harus:

    • Ngirim $tenantId ke controller
    • Terus ke service
    • Terus ke repository
    • Terus ke job
    • Terus ke log

    Ribet. Context bikin ini jadi simpel:

    use Illuminate\Support\Facades\Context;
    
    // Set sekali di middleware
    Context::add('tenant_id', 123);
    
    // Ambil di mana aja
    $tenantId = Context::get('tenant_id'); // 123

    Data ini otomatis ikut ke log, job, event, dan exception yang terjadi dalam request yang sama.

    Cara Pakai Context: Dasar

    Context tersedia lewat facade Illuminate\Support\Facades\Context atau helper context().

    Menambah Data ke Context

    Context::add('request_id', 'REQ-20260525-0001');
    Context::add('user_id', 42);
    
    // Atau sekaligus
    Context::add([
        'request_id' => 'REQ-20260525-0001',
        'user_id' => 42,
    ]);

    Mengambil Data dari Context

    $requestId = Context::get('request_id');
    
    // Dengan default value kalau tidak ada
    $userId = Context::get('user_id', 0);

    Menghapus Data dari Context

    // Hapus satu key
    Context::forget('request_id');
    
    // Hapus semua
    Context::flush();

    Studi Kasus 1: Request ID di Semua Log

    Masalah klasik: saat ada error, susah tahu log mana yang terkait satu request. Apalagi kalau traffic tinggi, log campur aduk.

    Solusinya: bikin middleware yang nambah request ID ke Context.

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Context;
    use Illuminate\Support\Str;
    
    class AddRequestContext
    {
        public function handle(Request $request, Closure $next): mixed
        {
            Context::add([
                'request_id' => Str::uuid()->toString(),
                'url'        => $request->fullUrl(),
                'method'     => $request->method(),
                'ip'         => $request->ip(),
                'user_id'    => $request->user()?->id,
            ]);
    
            return $next($request);
        }
    }

    Daftarkan di bootstrap/app.php:

    ->withMiddleware(function (Middleware $middleware) {
        $middleware->prepend(AddRequestContext::class);
    })

    Sekarang semua log otomatis punya context:

    Log::info('Memproses pembayaran');
    
    // Output di log:
    // [2026-05-25 10:23:45] local.INFO: Memproses pembayaran 
    // {"request_id":"abc-123","url":"/checkout","user_id":42}

    Kalau ada error, tinggal cari semua log dengan request_id yang sama. Debugging jadi jauh lebih gampang.

    Studi Kasus 2: Multi-Tenancy dengan Context

    Aplikasi multi-tenant butuh tenant_id di hampir semua query. Biasanya kamu harus ngirim $tenantId ke mana-mana. Dengan Context, cukup set sekali di middleware:

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Context;
    
    class ResolveTenant
    {
        public function handle(Request $request, Closure $next): mixed
        {
            // Resolve tenant dari subdomain atau header
            $tenant = $this->getTenantFromRequest($request);
    
            Context::add('tenant_id', $tenant->id);
    
            return $next($request);
        }
    
        private function getTenantFromRequest(Request $request)
        {
            // Contoh: dari subdomain
            $subdomain = explode('.', $request->getHost())[0];
            return Tenant::where('subdomain', $subdomain)->firstOrFail();
        }
    }

    Sekarang di service atau repository, tinggal ambil dari Context:

    class OrderService
    {
        public function getCurrentTenantOrders()
        {
            $tenantId = Context::get('tenant_id');
    
            return Order::where('tenant_id', $tenantId)->get();
        }
    }

    Catatan penting: Jangan bergantung 100% pada Context untuk data bisnis kritis. Kalau kamu jalanin Artisan command atau test, Context mungkin kosong. Selalu kasih default value atau cek null.

    Studi Kasus 3: Propagasi Context ke Queue Jobs

    Masalah: ketika request trigger job di queue, context (request ID, user ID) hilang karena job jalan di worker terpisah.

    Kabar baiknya: Context otomatis dipropagasi ke jobs kalau job implement ShouldQueue.

    <?php
    
    namespace App\Jobs;
    
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Support\Facades\Context;
    use Illuminate\Support\Facades\Log;
    
    class ProcessPayment implements ShouldQueue
    {
        use Dispatchable, InteractsWithQueue, Queueable;
    
        public function __construct(
            private int $orderId
        ) {}
    
        public function handle(): void
        {
            // Context dari request yang dispatch job ini otomatis tersedia
            $requestId = Context::get('request_id');
    
            Log::info("Memproses pembayaran order {$this->orderId}", [
                'request_id' => $requestId,
            ]);
    
            // proses pembayaran...
        }
    }

    Best practice: Untuk data penting kayak tenant_id, simpan juga sebagai property Job. Jangan cuma andalin Context, karena lifecycle worker bisa beda-beda.

    Studi Kasus 4: Hidden Context untuk Data Sensitif

    Kadang kamu butuh data di Context tapi nggak mau muncul di log (misalnya token atau API key).

    // Tambah context tersembunyi — tidak masuk ke log
    Context::addHidden([
        'payment_token' => $request->payment_token,
        'api_key'       => config('services.payment.key'),
    ]);
    
    // Ambil di bagian lain
    $token = Context::getHidden('payment_token');

    Data ini tetap bisa diakses di kode, tapi nggak akan muncul di log output.

    Kapan Pakai Context vs Dependency Injection?

    Context bukan pengganti dependency injection. Ini panduan kapan pakai mana:

    Pakai Context kalau:

    • Data perlu tersebar ke banyak layer (log, job, event) tanpa ubah signature
    • Multi-tenant: tenant_id butuh ada di mana-mana
    • Request tracing: request_id untuk debugging
    • Feature flags: tracking fitur apa yang aktif per request

    Pakai Dependency Injection kalau:

    • Data bisnis yang jelas dependensinya (misal OrderService butuh PaymentGateway)
    • Konfigurasi statis (cukup lewat config atau env)
    • Kode yang perlu di-test dengan mock/stub

    Intinya: Context untuk data “ambient” yang perlu ada di mana-mana. DI untuk dependensi eksplisit yang jelas.

    Testing Kode yang Pakai Context

    Di test, kamu harus set Context secara manual:

    use Illuminate\Support\Facades\Context;
    
    public function test_service_menggunakan_tenant_dari_context()
    {
        Context::flush(); // Pastikan kosong dulu
        Context::add('tenant_id', 10);
    
        $service = app(OrderService::class);
        $orders = $service->getCurrentTenantOrders();
    
        $this->assertTrue(
            $orders->every(fn ($order) => $order->tenant_id === 10)
        );
    }

    Jangan lupa Context::flush() di setUp() atau tearDown() biar nggak ada kebocoran state antar test.

    Pitfall yang Harus Dihindari

    1. Overuse Context = Global State

    Kalau semua data masuk Context, kode jadi susah di-trace. Pakai Context cuma untuk data yang emang perlu ada di mana-mana.

    2. Lupa Guard di CLI/Artisan

    Artisan command jalan tanpa request, jadi Context bisa kosong. Selalu kasih default:

    $tenantId = Context::get('tenant_id');
    
    if (is_null($tenantId)) {
        throw new \RuntimeException('Tenant ID tidak tersedia di context');
    }

    3. Ubah Context di Layer Bawah

    Kalau middleware A set tenant_id, jangan ubah lagi di service atau repository. Bikin aturan: Context cuma di-set di middleware, layer lain cuma baca.

    Ringkasan

    Context facade di Laravel 12 bikin kamu bisa nyebarin data ke seluruh aplikasi tanpa harus ngoper variabel ke mana-mana. Cocok buat:

    • Request tracing dengan request_id
    • Multi-tenancy dengan tenant_id
    • Feature flags dan debugging
    • Propagasi data ke log, job, dan event

    Tapi ingat: jangan overuse. Pakai Context untuk data ambient, pakai DI untuk dependensi eksplisit.

    Baca Juga

    Butuh tim yang bantu setup observability dan logging yang proper di aplikasi Laravel? Lihat layanan pengembangan aplikasi kami.

  • Apa Itu Contract di Laravel 12? Penjelasan Simpel + Contoh Kode

    Bayangkan Anda membangun fitur notifikasi. Hari ini kirim via email, bulan depan ditambah WhatsApp, tahun depan mungkin push notification. Kalau kode Anda langsung bergantung ke implementasi email spesifik, setiap perubahan akan memaksa Anda menyentuh banyak file sekaligus.

    Di sinilah Contract di Laravel 12 berguna.

    Apa Itu Contract di Laravel 12?

    Contract di Laravel adalah sekumpulan interface yang mendefinisikan “kontrak perilaku” sebuah layanan. Mereka berada di namespace Illuminate\Contracts dan tidak berisi implementasi — hanya daftar method yang harus dipenuhi oleh siapapun yang mengimplementasikannya.

    Singkatnya: Contract mendefinisikan apa yang bisa dilakukan, bukan bagaimana melakukannya.

    Perbedaan Contract dan Interface Biasa

    Secara teknis, Contract Laravel adalah interface PHP biasa. Yang membedakannya adalah tujuan dan skala: Contract Laravel menstandardisasi layanan inti framework (cache, queue, auth, storage, dll.) sehingga Anda bisa menukar implementasinya tanpa mengubah kode yang memakainya.

    Contoh: Illuminate\Contracts\Cache\Store mendefinisikan method get, put, forget, dll. Baik driver Redis maupun Memcached sama-sama mengimplementasikan contract ini — kode Anda tidak perlu tahu mana yang aktif.

    Membuat Contract Sendiri

    Selain contract bawaan Laravel, Anda bisa membuat contract untuk layanan buatan sendiri. Contoh sederhana untuk layanan notifikasi:

    // app/Contracts/NotificationServiceContract.php
    namespace App\Contracts;
    
    interface NotificationServiceContract
    {
        public function send(string $recipient, string $message): bool;
    }
    

    Lalu buat implementasinya:

    // app/Services/EmailNotificationService.php
    namespace App\Services;
    
    use App\Contracts\NotificationServiceContract;
    
    class EmailNotificationService implements NotificationServiceContract
    {
        public function send(string $recipient, string $message): bool
        {
            // logika kirim email
            return true;
        }
    }
    

    Binding Contract ke Service Container

    Daftarkan binding di AppServiceProvider atau service provider khusus:

    use App\Contracts\NotificationServiceContract;
    use App\Services\EmailNotificationService;
    
    public function register(): void
    {
        $this->app->bind(
            NotificationServiceContract::class,
            EmailNotificationService::class
        );
    }
    

    Setelah itu, gunakan contract via dependency injection di controller atau class lain:

    use App\Contracts\NotificationServiceContract;
    
    class OrderController extends Controller
    {
        public function __construct(
            private NotificationServiceContract $notifier
        ) {}
    
        public function store(Request $request)
        {
            // ... proses order
            $this->notifier->send($request->email, 'Pesanan diterima!');
        }
    }
    

    Besok ketika Anda ingin ganti ke WhatsApp, cukup buat WhatsAppNotificationService, ubah satu baris binding di service provider — tidak ada yang berubah di controller.

    Kapan Sebaiknya Menggunakan Contract?

    Contract paling berguna ketika:

    • Implementasi layanan bisa berubah di masa depan (storage, payment gateway, notifikasi)
    • Anda ingin membuat unit test yang tidak bergantung ke implementasi nyata (mock mudah dibuat dari interface)
    • Tim besar — contract menjadi “dokumen hidup” yang mendefinisikan API internal antar modul

    Untuk fungsi sederhana yang tidak akan berubah, Contract justru menambah kompleksitas yang tidak perlu. Gunakan dengan pertimbangan, bukan sebagai aturan baku.

    Contract vs Facade di Laravel

    Laravel punya Facade yang memberikan akses statis ke layanan container. Contract dan Facade sering digunakan untuk tujuan serupa, tapi berbeda pendekatan. Facade lebih ringkas untuk kode cepat; Contract lebih eksplisit dan mudah di-mock saat testing. Untuk aplikasi skala besar dengan banyak unit test, Contract umumnya lebih direkomendasikan.

    Kalau Anda sedang membangun aplikasi Laravel dan butuh konsultasi arsitektur atau tim pengembang, kami di Arrazy Inovasi siap membantu.

    Lihat layanan pengembangan aplikasi kami →

  • Tutorial Laravel 12 Job Batching: Implementasi, Progress, dan Error Handling

    Bayangkan Anda perlu kirim email ke 5.000 pengguna sekaligus, atau proses 1.000 gambar setelah upload. Kalau dijalankan satu per satu lewat queue biasa, Anda tidak tahu kapan semuanya selesai, dan tidak bisa jalankan aksi “setelah semua beres”.

    Job Batching di Laravel menyelesaikan masalah ini. Artikel ini membahas cara kerjanya, implementasi lengkap dengan contoh kode, dan cara handle error dalam batch.

    Apa Itu Job Batching?

    Job Batching memungkinkan Anda mengelompokkan beberapa job ke dalam satu batch, lalu mendefinisikan callback yang dijalankan:

    • Saat semua job berhasil (then)
    • Saat ada job yang gagal (catch)
    • Saat semua job selesai (berhasil atau gagal) (finally)

    Setup: Membuat Tabel Batch

    php artisan make:batches-table
    php artisan migrate

    Ini membuat tabel job_batches yang menyimpan status setiap batch.

    Membuat Job yang Batchable

    Job harus implement ShouldQueue dan gunakan trait Batchable:

    <?php
    
    namespace AppJobs;
    
    use AppModelsUser;
    use AppMailPromotionMail;
    use IlluminateBusBatchable;
    use IlluminateBusQueueable;
    use IlluminateContractsQueueShouldQueue;
    use IlluminateFoundationBusDispatchable;
    use IlluminateQueueInteractsWithQueue;
    use IlluminateSupportFacadesMail;
    
    class SendPromotionEmail implements ShouldQueue
    {
        use Batchable, Dispatchable, InteractsWithQueue, Queueable;
    
        public function __construct(
            private User $user
        ) {}
    
        public function handle(): void
        {
            // Cek apakah batch sudah di-cancel
            if ($this->batch()->cancelled()) {
                return;
            }
    
            Mail::to($this->user)->send(new PromotionMail());
        }
    }

    Mengirim Batch

    use AppJobsSendPromotionEmail;
    use IlluminateSupportFacadesBus;
    
    $users = User::where('subscribed', true)->get();
    
    $batch = Bus::batch(
        $users->map(fn ($user) => new SendPromotionEmail($user))->toArray()
    )->then(function (Batch $batch) {
        // Dijalankan saat semua job berhasil
        Log::info("Batch selesai: {$batch->totalJobs} email terkirim.");
    })->catch(function (Batch $batch, Throwable $e) {
        // Dijalankan saat ada job yang gagal
        Log::error("Batch error: {$e->getMessage()}");
    })->finally(function (Batch $batch) {
        // Selalu dijalankan saat batch selesai (berhasil atau tidak)
        Cache::forget('promotion-batch-running');
    })->name('Kirim Email Promosi')
      ->allowFailures()  // batch tetap lanjut meski ada job gagal
      ->dispatch();
    
    // Simpan ID batch untuk monitor progress
    return $batch->id;

    Monitor Progress Batch

    Ambil info batch berdasarkan ID:

    use IlluminateSupportFacadesBus;
    
    $batch = Bus::findBatch($batchId);
    
    // Info yang tersedia
    $batch->id;
    $batch->name;
    $batch->totalJobs;
    $batch->pendingJobs;
    $batch->failedJobs;
    $batch->processedJobs();  // totalJobs - pendingJobs
    $batch->progress();       // 0-100 persen
    $batch->finished();       // apakah sudah selesai
    $batch->cancelled();      // apakah di-cancel
    

    Contoh endpoint API untuk polling progress:

    Route::get('/batches/{batchId}/progress', function (string $batchId) {
        $batch = Bus::findBatch($batchId);
    
        if (!$batch) {
            return response()->json(['error' => 'Batch tidak ditemukan'], 404);
        }
    
        return response()->json([
            'progress'     => $batch->progress(),
            'total'        => $batch->totalJobs,
            'processed'    => $batch->processedJobs(),
            'failed'       => $batch->failedJobs,
            'finished'     => $batch->finished(),
        ]);
    })->middleware('auth');

    Cancel Batch

    $batch = Bus::findBatch($batchId);
    $batch->cancel();

    Job yang belum diproses akan skip sendiri karena pengecekan $this->batch()->cancelled() di awal method handle().

    Batch dengan Job Berantai

    Anda bisa chain batch: jalankan batch kedua setelah batch pertama selesai:

    Bus::batch([
        new ProcessImages($product),
        new GenerateThumbnails($product),
    ])->then(function (Batch $batch) use ($product) {
        // Setelah gambar dan thumbnail selesai, generate sitemap
        GenerateSitemap::dispatch();
    })->dispatch();

    Praktis: Batch untuk Impor CSV

    Skenario umum: impor data dari file CSV besar.

    <?php
    
    namespace AppJobs;
    
    use IlluminateBusBatchable;
    use IlluminateBusQueueable;
    use IlluminateContractsQueueShouldQueue;
    
    class ImportCsvRow implements ShouldQueue
    {
        use Batchable, Queueable;
    
        public function __construct(private array $row) {}
    
        public function handle(): void
        {
            if ($this->batch()->cancelled()) return;
    
            Product::updateOrCreate(
                ['sku'  => $this->row['sku']],
                ['name' => $this->row['name'], 'price' => $this->row['price']]
            );
        }
    }
    
    // Di controller
    $rows  = CsvParser::parse($file);
    $jobs  = collect($rows)->map(fn ($row) => new ImportCsvRow($row));
    
    $batch = Bus::batch($jobs->toArray())
                ->name('Import Produk')
                ->allowFailures()
                ->dispatch();
    
    return redirect()->route('imports.progress', $batch->id);

    Baca Juga

    Butuh tim untuk implementasi background processing di aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

  • Notification di Laravel 12: Email, Database, dan Channel Kustom

    Hampir semua aplikasi web butuh notifikasi: konfirmasi order, reset password, atau pemberitahuan saat ada komentar baru. Laravel punya sistem notifikasi yang cukup fleksibel: satu kelas bisa kirim ke email, database, Slack, dan channel lain sekaligus.

    Artikel ini membahas cara kerja notification di Laravel 12, mulai dari membuat kelas notifikasi, kirim via email dan database, sampai membuat channel kustom.

    Membuat Kelas Notifikasi

    php artisan make:notification OrderConfirmed

    File dibuat di app/Notifications/OrderConfirmed.php:

    <?php
    
    namespace AppNotifications;
    
    use AppModelsOrder;
    use IlluminateBusQueueable;
    use IlluminateNotificationsNotification;
    use IlluminateNotificationsMessagesMailMessage;
    use IlluminateContractsQueueShouldQueue;
    
    class OrderConfirmed extends Notification implements ShouldQueue
    {
        use Queueable;
    
        public function __construct(
            private Order $order
        ) {}
    
        public function via(object $notifiable): array
        {
            return ['mail', 'database'];
        }
    
        public function toMail(object $notifiable): MailMessage
        {
            return (new MailMessage)
                ->subject("Order #{$this->order->id} Dikonfirmasi")
                ->greeting("Halo, {$notifiable->name}!")
                ->line("Order Anda telah kami terima dan sedang diproses.")
                ->action('Lihat Detail Order', route('orders.show', $this->order))
                ->line("Terima kasih sudah berbelanja.");
        }
    
        public function toDatabase(object $notifiable): array
        {
            return [
                'order_id' => $this->order->id,
                'message'  => "Order #{$this->order->id} dikonfirmasi",
                'url'      => route('orders.show', $this->order),
            ];
        }
    }

    Notifiable Trait

    Model yang ingin menerima notifikasi harus menggunakan trait Notifiable. Model User bawaan Laravel sudah menyertakan ini:

    use IlluminateNotificationsNotifiable;
    
    class User extends Authenticatable
    {
        use Notifiable;
    }

    Untuk model lain — misalnya Admin atau Customer — tambahkan trait yang sama.

    Mengirim Notifikasi

    // Lewat instance model
    $user->notify(new OrderConfirmed($order));
    
    // Lewat Notification facade (tanpa model spesifik)
    Notification::send($users, new OrderConfirmed($order));
    
    // On-demand — kirim ke email tanpa model User
    Notification::route('mail', 'admin@example.com')
                ->notify(new OrderConfirmed($order));

    Karena kelas ini implement ShouldQueue, notifikasi akan diproses di background queue, tidak memperlambat response HTTP.

    Notifikasi Email: Kustomisasi Template

    Untuk kontrol penuh atas tampilan email, publish template bawaan Laravel:

    php artisan vendor:publish --tag=laravel-mail

    Template disimpan di resources/views/vendor/mail/. Edit sesuai kebutuhan brand Anda.

    Kalau mau pakai Markdown template yang lebih kaya:

    php artisan make:notification OrderShipped --markdown=mail.order.shipped

    Template dibuat di resources/views/mail/order/shipped.blade.php:

    @component('mail::message')
    # Order Anda Dalam Perjalanan
    
    Pesanan **#{{ $order->id }}** sedang dikirim ke alamat Anda.
    
    @component('mail::button', ['url' => $trackingUrl])
    Lacak Pengiriman
    @endcomponent
    
    Estimasi tiba: **{{ $order->estimated_arrival }}**
    
    Salam,
    {{ config('app.name') }}
    @endcomponent

    Notifikasi Database

    Untuk simpan notifikasi di database (biasanya untuk fitur notifikasi in-app), buat tabel dulu:

    php artisan make:notifications-table
    php artisan migrate

    Ambil notifikasi yang belum dibaca:

    // Semua notifikasi
    $notifications = auth()->user()->notifications;
    
    // Hanya yang belum dibaca
    $unread = auth()->user()->unreadNotifications;
    
    // Tandai sudah dibaca
    auth()->user()->unreadNotifications->markAsRead();
    
    // Tandai satu notifikasi sudah dibaca
    $notification->markAsRead();

    Channel Kustom

    Kalau perlu kirim via WhatsApp, Telegram, atau sistem internal, buat channel kustom:

    <?php
    
    namespace AppNotificationsChannels;
    
    use IlluminateNotificationsNotification;
    
    class WhatsAppChannel
    {
        public function send(object $notifiable, Notification $notification): void
        {
            $message = $notification->toWhatsApp($notifiable);
    
            // Kirim via WhatsApp API
            app(WhatsAppService::class)->send(
                $notifiable->phone,
                $message
            );
        }
    }

    Daftarkan di method via():

    public function via(object $notifiable): array
    {
        return [WhatsAppChannel::class, 'database'];
    }
    
    public function toWhatsApp(object $notifiable): string
    {
        return "Order #{$this->order->id} Anda sudah dikonfirmasi.";
    }

    Baca Juga

    Butuh tim yang bantu implementasi sistem notifikasi di aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

  • Keunggulan Laravel Volt: Single-File Component yang Efisien

    Setelah cukup banyak pakai Livewire biasa, saya coba beralih ke Volt di salah satu proyek internal. Hasilnya memang ada perbedaan yang terasa, terutama saat nambah fitur baru atau onboarding developer junior ke proyek.

    Artikel ini membahas keunggulan nyata Laravel Volt dibanding cara penulisan Livewire sebelumnya, lengkap dengan contoh kode perbandingan.

    1. Single-File Component: Logika dan Template Dalam Satu Tempat

    Livewire biasa butuh dua file per komponen. Volt hanya butuh satu.

    Livewire biasa:

    // app/Livewire/SearchBox.php
    class SearchBox extends Component
    {
        public string $query = '';
    
        public function updatedQuery(): void
        {
            // filter results
        }
    
        public function render()
        {
            return view('livewire.search-box');
        }
    }
    
    {{-- resources/views/livewire/search-box.blade.php --}}
    <div>
        <input wire:model.live="query" />
    </div>

    Volt (satu file):

    <?php
    
    use function LivewireVolt{state};
    
    state(['query' => '']);
    
    $updatedQuery = fn() => /* filter results */;
    
    ?>
    
    <div>
        <input wire:model.live="query" />
    </div>

    Lebih sedikit file = lebih mudah navigasi, lebih mudah review PR.

    2. Computed Properties yang Lebih Eksplisit

    Livewire v2 punya computed properties tapi agak tersembunyi. Volt mengekspos ini dengan cara yang lebih jelas:

    <?php
    
    use AppModelsProduct;
    use function LivewireVolt{state, computed};
    
    state(['category' => 'all']);
    
    $products = computed(function () {
        return $this->category === 'all'
            ? Product::paginate(12)
            : Product::where('category', $this->category)->paginate(12);
    });
    
    ?>
    
    <div>
        <select wire:model.live="category">
            <option value="all">Semua</option>
            <option value="elektronik">Elektronik</option>
        </select>
    
        @foreach ($this->products as $product)
            <p>{{ $product->name }}</p>
        @endforeach
    
        {{ $this->products->links() }}
    </div>

    Computed property otomatis di-cache selama satu request, tidak dihitung ulang setiap setiap kali template render.

    3. Lifecycle Hooks yang Lebih Bersih

    Volt mendukung lifecycle hooks Livewire dengan cara yang lebih ringkas:

    <?php
    
    use AppModelsCart;
    use function LivewireVolt{state, mount, updated};
    
    state(['quantity' => 1, 'productId' => null]);
    
    mount(function (int $productId) {
        $this->productId = $productId;
    });
    
    updated('quantity', function () {
        if ($this->quantity < 1) $this->quantity = 1;
        if ($this->quantity > 99) $this->quantity = 99;
    });
    
    $addToCart = function () {
        Cart::addItem($this->productId, $this->quantity);
        $this->dispatch('cart-updated');
    };
    
    ?>
    
    <div>
        <input wire:model="quantity" type="number" />
        <button wire:click="addToCart">Tambah ke Keranjang</button>
    </div>

    4. Form Handling yang Lebih Bersih

    Volt mendukung Form Objects dari Livewire v3:

    <?php
    
    use AppLivewireFormsContactForm;
    use function LivewireVolt{form};
    
    form(ContactForm::class, 'contact');
    
    $submit = function () {
        $this->contact->submit();
        $this->reset('contact');
    };
    
    ?>
    
    <form wire:submit="submit">
        <input wire:model="contact.name" />
        <input wire:model="contact.email" />
        <textarea wire:model="contact.message"></textarea>
        <button type="submit">Kirim</button>
    </form>

    5. Lebih Mudah Dipahami oleh Developer Baru

    Ini mungkin yang paling terasa di tim. Dengan Volt, developer yang baru bergabung cukup buka satu file untuk memahami apa yang dilakukan sebuah komponen. Tidak perlu loncat antara dua file untuk trace logika.

    Baca Juga

    Tertarik membangun aplikasi dengan Laravel + Livewire + Volt? Lihat layanan pengembangan aplikasi kami.

  • Cara Instalasi Laravel Volt di Laravel 12: Panduan Lengkap

    Laravel Volt sudah termasuk dalam Livewire v3, jadi cara installnya lebih mudah dari yang Anda bayangkan. Artikel ini membahas dua jalur instalasi: lewat Laravel Breeze, dan langsung lewat Livewire.

    Prasyarat

    • PHP 8.1 atau lebih baru
    • Laravel 10 ke atas (disarankan Laravel 12)
    • Composer

    Jalur 1: Install via Laravel Breeze

    Ini jalur paling umum. Breeze menyediakan scaffolding autentikasi lengkap sekaligus menginstall Volt.

    # Buat project Laravel baru
    composer create-project laravel/laravel nama-project
    cd nama-project
    
    # Install Breeze
    composer require laravel/breeze --dev
    
    # Install dengan opsi Livewire (Volt sudah termasuk)
    php artisan breeze:install livewire
    
    # Install dependencies Node.js dan build assets
    npm install && npm run dev
    
    # Jalankan migration
    php artisan migrate

    Setelah ini, Anda langsung punya halaman login, register, dan dashboard yang sudah jalan dengan Livewire + Volt.

    Jalur 2: Install Livewire + Volt Langsung

    Kalau tidak butuh scaffolding autentikasi dari Breeze:

    # Di project Laravel yang sudah ada
    composer require livewire/livewire
    
    # Volt sudah termasuk di dalam package livewire/livewire v3
    # Aktifkan Volt dengan publish config
    php artisan livewire:publish --config

    Tambahkan direktif Livewire di layout utama Anda (resources/views/layouts/app.blade.php):

    <html>
    <head>
        ...
        @livewireStyles
    </head>
    <body>
        {{ $slot }}
        @livewireScripts
    </body>
    </html>

    Konfigurasi Path Komponen Volt

    Secara default, komponen Volt disimpan di resources/views/livewire/. Anda bisa konfigurasi lewat app/Providers/AppServiceProvider.php:

    <?php
    
    namespace App\Providers;
    
    use Illuminate\Support\ServiceProvider;
    use Livewire\Volt\Volt;
    
    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            // Tambah folder komponen Volt
            Volt::mount([
                resource_path('views/livewire'),
                resource_path('views/pages'),  // tambahan
            ]);
        }
    }

    Membuat Komponen Volt Pertama

    php artisan make:volt search-box

    File dibuat di resources/views/livewire/search-box.blade.php:

    <?php
    
    use function Livewire\Volt\{state};
    
    state(['query' => '']);
    
    ?>
    
    <div>
        <input
            wire:model.live.debounce.300ms="query"
            placeholder="Cari..."
            class="border rounded px-3 py-2"
        />
    
        @if($query)
            <p>Mencari: {{ $query }}</p>
        @endif
    </div>

    Gunakan di halaman Blade mana saja:

    <livewire:search-box />

    Verifikasi Instalasi

    # Pastikan Livewire terinstall
    composer show livewire/livewire
    
    # Jalankan server dan cek halaman yang ada komponen Volt
    php artisan serve

    Kalau komponen muncul dan reaktif (bisa klik/input tanpa reload halaman), instalasi berhasil.

    Baca Juga

    Mau membangun aplikasi web interaktif dengan Laravel + Livewire? Lihat layanan pengembangan aplikasi kami.

  • Perbedaan Laravel Volt dan Laravel Breeze: Kapan Pakai Yang Mana?

    Saat mulai proyek Laravel baru, salah satu pertanyaan yang sering muncul adalah: pakai Breeze atau Volt? Keduanya dari ekosistem Laravel, tapi perannya sangat berbeda.

    Artikel ini menjelaskan perbedaan Laravel Volt dan Laravel Breeze, bukan untuk memilih yang “lebih baik”, tapi untuk memahami kapan masing-masing dipakai.

    Laravel Breeze: Starter Kit Autentikasi

    Laravel Breeze adalah starter kit yang menyediakan scaffolding autentikasi: login, register, reset password, verifikasi email, dan konfirmasi password.

    Breeze bisa diinstall dengan beberapa pilihan frontend:

    • Blade (default) — tanpa JavaScript framework
    • Livewire — menggunakan Livewire untuk interaktivitas
    • Inertia + Vue
    • Inertia + React
    • API (untuk SPA atau mobile)

    Ketika Anda pilih opsi Livewire saat install Breeze, Volt sudah termasuk di dalamnya.

    composer require laravel/breeze --dev
    php artisan breeze:install livewire

    Laravel Volt: Cara Tulis Komponen Livewire

    Volt bukan starter kit. Volt adalah API untuk menulis komponen Livewire dalam format single-file.

    Perbedaannya dengan Livewire biasa: tidak perlu file class PHP terpisah. Logika dan template ada dalam satu file Blade:

    <?php
    use function LivewireVolt{state};
    
    state(['search' => '']);
    
    $results = computed(function () {
        return Article::where('title', 'like', "%{$this->search}%")->get();
    });
    ?>
    
    <div>
        <input wire:model.live="search" placeholder="Cari artikel..." />
        @foreach ($this->results as $article)
            <p>{{ $article->title }}</p>
        @endforeach
    </div>

    Perbandingan Langsung

    Aspek Breeze Volt
    Fungsi utama Scaffolding autentikasi API tulis komponen Livewire
    Diinstall terpisah? Ya, via Composer Sudah termasuk di Livewire v3
    Menghasilkan file? Ya (routes, views, controllers) Tidak — hanya mengubah cara penulisan
    Bisa dipakai tanpa yang lain? Ya Ya (tapi butuh Livewire v3)
    Wajib? Tidak Tidak

    Hubungan Keduanya

    Breeze dan Volt bukan saingan. Keduanya bisa dipakai bersamaan. Skenario yang paling umum:

    1. Install Breeze dengan opsi Livewire → Volt otomatis terinstall
    2. Gunakan Volt untuk menulis komponen-komponen interaktif di aplikasi Anda
    3. Halaman autentikasi (login, register) sudah di-generate Breeze

    Kalau Anda tidak butuh autentikasi siap pakai, bisa install Livewire dan Volt langsung tanpa Breeze.

    Kapan Tidak Perlu Breeze?

    • Proyek API-only (tidak ada view)
    • Autentikasi sudah diimplementasi custom
    • Menggunakan Jetstream (alternatif Breeze yang lebih lengkap)

    Baca Juga

    Butuh tim yang bantu setup Laravel + Livewire untuk proyek Anda? Lihat layanan pengembangan aplikasi kami.

  • Apa Itu Laravel Volt dan Bagaimana Cara Kerjanya

    Kalau Anda pernah pakai Livewire di Laravel, mungkin sudah tahu betapa nyamannya bikin komponen reaktif tanpa harus tulis JavaScript. Laravel Volt membawa pengalaman itu selangkah lebih jauh dengan sintaks single-file component yang lebih bersih.

    Artikel ini menjelaskan apa itu Laravel Volt, bagaimana cara kerjanya, dan di mana Volt cocok dipakai.

    Apa Itu Laravel Volt?

    Laravel Volt adalah API baru untuk Livewire v3 yang memungkinkan Anda menulis komponen Livewire dalam satu file Blade, tanpa class PHP terpisah.

    Sebelum Volt, sebuah komponen Livewire butuh dua file:

    1. app/Livewire/Counter.php — class dengan logika
    2. resources/views/livewire/counter.blade.php — template

    Dengan Volt, keduanya bisa dalam satu file counter.blade.php:

    <?php
    
    use function LivewireVolt{state, computed};
    
    state(['count' => 0]);
    
    $increment = fn() => $this->count++;
    $decrement = fn() => $this->count--;
    
    ?>
    
    <div>
        <h1>{{ $count }}</h1>
        <button wire:click="increment">+</button>
        <button wire:click="decrement">-</button>
    </div>

    Lebih ringkas, dan semua yang perlu dibaca ada dalam satu file.

    Perbedaan Volt Class API vs Functional API

    Volt mendukung dua cara penulisan. Functional API (contoh di atas) lebih ringkas untuk komponen sederhana. Class API cocok untuk komponen yang lebih kompleks karena lebih terstruktur:

    <?php
    
    use LivewireVoltComponent;
    
    new class extends Component {
        public int $count = 0;
    
        public function increment(): void
        {
            $this->count++;
        }
    
        public function decrement(): void
        {
            $this->count--;
        }
    } ?>
    
    <div>
        <h1>{{ $count }}</h1>
        <button wire:click="increment">+</button>
        <button wire:click="decrement">-</button>
    </div>

    Apa Yang Volt Sederhanakan?

    Volt bukan framework baru, melainkan layer di atas Livewire v3. Yang berubah:

    • Satu file per komponen: tidak perlu buka dua file sekaligus saat edit
    • Co-location: logika dan template ada di satu tempat, lebih mudah dipahami konteksnya
    • Lebih sedikit boilerplate: tidak perlu bikin class, register namespace, dll.

    Yang tetap sama: cara Livewire bekerja (wire:click, wire:model, $refresh, dll.) tidak berubah sama sekali.

    Kapan Pakai Volt vs Livewire Biasa?

    Volt cocok untuk komponen yang:

    • Logikanya tidak terlalu besar (kurang dari ~100 baris)
    • Tidak perlu di-extend atau di-inherit komponen lain
    • Berdiri sendiri dan tidak terlalu banyak dependency

    Tetap pakai Livewire class biasa kalau:

    • Komponen butuh extend dari base class kustom
    • Logikanya sudah terlalu panjang dan butuh dipisah ke service
    • Tim lebih familiar dengan pola class-based

    Baca Juga

    Tertarik membangun aplikasi dengan stack Laravel + Livewire? Lihat layanan pengembangan aplikasi kami.