Category: Tutorial

  • Apa Itu Event dan Listener di Laravel 12: Arsitektur Event-Driven

    Salah satu ciri arsitektur Laravel yang baik: ketika sesuatu terjadi di aplikasi, komponen lain bisa “mendengarkan” dan merespons, tanpa si pengirim perlu tahu siapa yang merespons.

    Itulah fungsi Event dan Listener di Laravel. Artikel ini menjelaskan cara kerjanya dan bagaimana implementasinya dengan contoh nyata.

    Konsep Event dan Listener

    Event adalah kejadian yang terjadi di aplikasi: user mendaftar, order diproses, pembayaran berhasil. Listener adalah kode yang merespons kejadian tersebut.

    Satu event bisa punya beberapa listener. Misalnya saat UserRegistered:

    • Listener 1: kirim email selamat datang
    • Listener 2: create profil default
    • Listener 3: sync ke CRM

    Controller yang trigger event tidak perlu tahu ada 3 listener di belakang. Ini yang membuat kode lebih modular.

    Membuat Event dan Listener

    php artisan make:event UserRegistered
    php artisan make:listener SendWelcomeEmail --event=UserRegistered
    php artisan make:listener CreateDefaultProfile --event=UserRegistered

    Kelas event (app/Events/UserRegistered.php):

    <?php
    
    namespace AppEvents;
    
    use AppModelsUser;
    use IlluminateFoundationEventsDispatchable;
    use IlluminateQueueSerializesModels;
    
    class UserRegistered
    {
        use Dispatchable, SerializesModels;
    
        public function __construct(
            public readonly User $user
        ) {}
    }

    Listener (app/Listeners/SendWelcomeEmail.php):

    <?php
    
    namespace AppListeners;
    
    use AppEventsUserRegistered;
    use AppMailWelcomeMail;
    use IlluminateContractsQueueShouldQueue;
    use IlluminateSupportFacadesMail;
    
    class SendWelcomeEmail implements ShouldQueue
    {
        public function handle(UserRegistered $event): void
        {
            Mail::to($event->user)->send(new WelcomeMail($event->user));
        }
    }

    Implement ShouldQueue agar listener dijalankan di background queue, tidak memperlambat proses registrasi.

    Registrasi Event dan Listener

    Di Laravel 12 (dan sejak Laravel 11), cukup tambahkan di app/Providers/AppServiceProvider.php:

    <?php
    
    namespace AppProviders;
    
    use AppEventsUserRegistered;
    use AppListenersSendWelcomeEmail;
    use AppListenersCreateDefaultProfile;
    use IlluminateSupportFacadesEvent;
    use IlluminateSupportServiceProvider;
    
    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Event::listen(UserRegistered::class, SendWelcomeEmail::class);
            Event::listen(UserRegistered::class, CreateDefaultProfile::class);
        }
    }

    Laravel juga mendukung auto-discovery. Kalau nama listener mengikuti konvensi, registrasi manual tidak diperlukan. Tapi registrasi manual lebih eksplisit dan lebih mudah di-trace.

    Dispatch Event

    // Di controller atau service
    use AppEventsUserRegistered;
    
    $user = User::create($validated);
    
    UserRegistered::dispatch($user);
    
    // Atau lewat helper
    event(new UserRegistered($user));

    Listener Synchronous vs Queued

    Kalau listener implement ShouldQueue, dijalankan di background. Kalau tidak, dijalankan langsung (synchronous) sebelum response dikirim.

    Kapan pakai synchronous:

    • Listener yang hasilnya dibutuhkan sebelum response dikirim
    • Operasi yang sangat cepat (kurang dari 10ms)

    Kapan pakai queued:

    • Kirim email, notifikasi, sync ke API eksternal
    • Semua operasi yang tidak perlu hasilnya sekarang

    Wildcard Listener

    Kalau perlu listen ke banyak event sekaligus (misalnya untuk logging):

    Event::listen('AppEvents*', function (string $eventName, array $data) {
        Log::info("Event: {$eventName}", $data);
    });

    Baca Juga

    Butuh tim untuk merancang arsitektur event-driven di aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

  • Apa Itu Policy dan Gate di Laravel 12: Sistem Otorisasi yang Tepat

    Bayangkan ada dua pertanyaan berbeda soal keamanan di aplikasi Anda: “Apakah user ini boleh edit artikel?” dan “Apakah user yang login adalah editor?”

    Pertanyaan pertama terkait Policy: otorisasi berdasarkan resource. Pertanyaan kedua terkait Gate: otorisasi berdasarkan kemampuan/role. Keduanya bagian dari sistem Authorization di Laravel.

    Apa Itu Gate?

    Gate adalah cara mendefinisikan otorisasi berbasis kemampuan (ability) secara sederhana, biasanya untuk aksi yang tidak terkait langsung dengan model tertentu.

    Definisikan Gate di app/Providers/AppServiceProvider.php:

    <?php
    
    namespace AppProviders;
    
    use AppModelsUser;
    use IlluminateSupportFacadesGate;
    use IlluminateSupportServiceProvider;
    
    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Gate::define('access-admin-panel', function (User $user) {
                return $user->role === 'admin';
            });
    
            Gate::define('manage-settings', function (User $user) {
                return in_array($user->role, ['admin', 'superadmin']);
            });
        }
    }

    Gunakan di controller atau view:

    // Di controller
    if (Gate::denies('access-admin-panel')) {
        abort(403);
    }
    
    // Dengan authorize (auto-throw 403 jika gagal)
    Gate::authorize('access-admin-panel');
    
    // Di Blade
    @can('access-admin-panel')
        <a href="/admin">Admin Panel</a>
    @endcan

    Apa Itu Policy?

    Policy adalah kelas yang mengelompokkan logika otorisasi untuk satu model. Cocok untuk otorisasi seperti “siapa yang boleh edit, hapus, atau lihat resource ini?”

    php artisan make:policy ArticlePolicy --model=Article

    File dibuat di app/Policies/ArticlePolicy.php:

    <?php
    
    namespace AppPolicies;
    
    use AppModelsArticle;
    use AppModelsUser;
    
    class ArticlePolicy
    {
        public function viewAny(User $user): bool
        {
            return true; // Semua user bisa lihat daftar artikel
        }
    
        public function view(User $user, Article $article): bool
        {
            // Bisa lihat kalau published, atau kalau milik sendiri
            return $article->status === 'published' || $article->user_id === $user->id;
        }
    
        public function create(User $user): bool
        {
            return $user->hasVerifiedEmail();
        }
    
        public function update(User $user, Article $article): bool
        {
            // Hanya pemilik yang bisa edit
            return $article->user_id === $user->id;
        }
    
        public function delete(User $user, Article $article): bool
        {
            // Pemilik dan admin bisa hapus
            return $article->user_id === $user->id || $user->role === 'admin';
        }
    }

    Registrasi Policy

    Di Laravel 12, policy otomatis ditemukan (auto-discovery) kalau struktur folder sesuai konvensi. Tapi Anda juga bisa register manual di AppServiceProvider:

    use AppModelsArticle;
    use AppPoliciesArticlePolicy;
    use IlluminateSupportFacadesGate;
    
    Gate::policy(Article::class, ArticlePolicy::class);

    Menggunakan Policy

    // Di controller
    public function edit(Article $article)
    {
        $this->authorize('update', $article); // Throw 403 kalau tidak bisa
    
        return view('articles.edit', compact('article'));
    }
    
    // Cek tanpa throw exception
    if ($request->user()->can('update', $article)) {
        // boleh lanjut
    }
    
    // Di Blade
    @can('update', $article)
        <a href="{{ route('articles.edit', $article) }}">Edit</a>
    @endcan
    
    @cannot('delete', $article)
        <p>Anda tidak bisa hapus artikel ini.</p>
    @endcannot

    Gate vs Policy: Kapan Pakai Yang Mana?

    Kondisi Gunakan
    Otorisasi terkait satu model (Article, Order, dll.) Policy
    Otorisasi berdasarkan role/kemampuan global Gate
    Aturan sederhana yang tidak butuh kelas terpisah Gate
    Banyak aturan berbeda untuk satu model Policy

    Baca Juga

    Butuh tim yang bantu implementasi sistem otorisasi yang aman di aplikasi Laravel? Lihat layanan pengembangan aplikasi kami.

  • Cara Membuat Custom Artisan Command di Laravel 12

    Artisan bawaan Laravel sudah mencakup banyak kebutuhan. Tapi kadang Anda butuh command yang spesifik untuk proyek Anda, misalnya generate laporan harian, kirim email batch, atau sync data dari API eksternal.

    Di sinilah custom Artisan command berguna. Artikel ini menunjukkan cara membuat, mengatur parameter, dan menjalankan scheduled command di Laravel 12.

    Membuat Custom Command

    Gunakan Artisan untuk generate skeleton command:

    php artisan make:command SendDailyReport

    File dibuat di app/Console/Commands/SendDailyReport.php:

    <?php
    
    namespace AppConsoleCommands;
    
    use IlluminateConsoleCommand;
    
    class SendDailyReport extends Command
    {
        protected $signature = 'report:daily';
        protected $description = 'Send daily sales report to managers';
    
        public function handle(): int
        {
            // logika command di sini
    
            return Command::SUCCESS;
        }
    }

    Jalankan:

    php artisan report:daily

    Menambahkan Argumen dan Opsi

    Command yang berguna biasanya butuh input. Ada dua jenis: argumen (wajib) dan opsi (opsional dengan --).

    protected $signature = 'report:send
                            {type : Tipe laporan: sales atau inventory}
                            {--date= : Tanggal laporan (default: hari ini)}
                            {--dry-run : Simulasi tanpa kirim email}';
    

    Cara mengakses di dalam handle():

    public function handle(): int
    {
        $type    = $this->argument('type');
        $date    = $this->option('date') ?? now()->toDateString();
        $dryRun  = $this->option('dry-run');
    
        $this->info("Membuat laporan {$type} untuk tanggal {$date}...");
    
        if ($dryRun) {
            $this->warn('Mode dry-run: email tidak akan dikirim.');
        }
    
        // proses laporan...
    
        $this->info('Selesai.');
        return Command::SUCCESS;
    }

    Contoh penggunaan:

    php artisan report:send sales
    php artisan report:send sales --date=2025-04-01
    php artisan report:send inventory --dry-run

    Output yang Informatif

    Artisan menyediakan beberapa helper untuk output yang bersih:

    $this->info('Pesan biasa');      // hijau
    $this->error('Ada error!');     // merah
    $this->warn('Perhatian');       // kuning
    $this->line('Teks biasa');      // putih
    $this->comment('// komentar'); // abu-abu
    
    // Progress bar untuk proses batch
    $users = User::all();
    $bar   = $this->output->createProgressBar(count($users));
    $bar->start();
    
    foreach ($users as $user) {
        // proses
        $bar->advance();
    }
    
    $bar->finish();

    Dependency Injection di Custom Command

    Inject service lewat constructor, sama seperti controller:

    <?php
    
    namespace AppConsoleCommands;
    
    use AppServicesReportService;
    use AppServicesMailService;
    use IlluminateConsoleCommand;
    
    class SendDailyReport extends Command
    {
        protected $signature = 'report:daily {--date=}';
        protected $description = 'Send daily report via email';
    
        public function __construct(
            private ReportService $reportService,
            private MailService   $mailService,
        ) {
            parent::__construct();
        }
    
        public function handle(): int
        {
            $date   = $this->option('date') ?? now()->toDateString();
            $report = $this->reportService->generate($date);
    
            if ($report->isEmpty()) {
                $this->warn("Tidak ada data untuk tanggal {$date}.");
                return Command::SUCCESS;
            }
    
            $this->mailService->sendReport($report);
            $this->info("Laporan {$date} berhasil dikirim ke " . $report->recipientCount() . " penerima.");
    
            return Command::SUCCESS;
        }
    }

    Menjadwalkan Command Otomatis

    Di Laravel 12, jadwal command diatur di routes/console.php — bukan lagi di app/Console/Kernel.php:

    <?php
    
    use IlluminateSupportFacadesSchedule;
    
    // Kirim laporan setiap hari jam 7 pagi
    Schedule::command('report:daily')->dailyAt('07:00');
    
    // Bersihkan log setiap minggu
    Schedule::command('log:cleanup')->weekly();
    
    // Sync produk setiap 30 menit
    Schedule::command('products:sync')->everyThirtyMinutes();
    
    // Jalankan khusus hari kerja
    Schedule::command('report:daily')->weekdays()->at('08:00');

    Tambahkan satu baris ke crontab server untuk menjalankan scheduler setiap menit:

    * * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1

    Testing Custom Command

    Laravel menyediakan helper untuk test command tanpa harus jalankan manual:

    public function test_report_command_runs_successfully(): void
    {
        $this->artisan('report:daily')
             ->expectsOutput('Laporan berhasil dikirim')
             ->assertExitCode(0);
    }
    
    // Test dengan argumen
    public function test_report_with_date(): void
    {
        $this->artisan('report:send', [
            'type'   => 'sales',
            '--date' => '2025-04-01',
        ])->assertExitCode(0);
    }

    Baca Juga

    Kalau Anda butuh tim untuk membangun dan maintain aplikasi Laravel Anda, lihat layanan pengembangan aplikasi kami.

  • 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.

    Concurrency Adalah: Pengertian Singkat

    Secara umum, concurrency adalah kemampuan sebuah program menangani beberapa tugas dalam rentang waktu yang sama, tanpa harus menunggu satu tugas benar-benar selesai sebelum memulai tugas berikutnya. Contoh sehari-hari: koki yang merebus air, sambil menunggu airnya mendidih ia memotong sayur. Dua pekerjaan berjalan tumpang tindih, walau dikerjakan satu orang.

    Istilah ini sering disamakan dengan paralel, padahal sedikit berbeda. Paralel berarti tugas-tugas benar-benar dijalankan pada detik yang sama oleh beberapa prosesor atau proses. Concurrency lebih luas: yang penting tugas-tugasnya bisa berjalan tumpang tindih. Di Laravel 12, concurrency diwujudkan dengan menjalankan beberapa proses PHP secara bersamaan, jadi dalam praktiknya hasilnya memang paralel.

    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.

  • 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.

  • Laravel Volt: Pengertian, Cara Install di Laravel 12, dan Statusnya di Livewire 4

    Laravel Volt adalah API fungsional untuk Livewire yang memungkinkan logika PHP dan template Blade sebuah komponen ditulis dalam satu file. Volt dipasang sebagai paket terpisah, livewire/volt, dengan dua perintah: composer require livewire/volt lalu php artisan volt:install. Panduan ini menjelaskan cara kerja Volt, cara install di Laravel 12, contoh komponen yang sudah kami uji, dan satu hal yang jarang dibahas: sejak Livewire 4, komponen satu file sudah bawaan Livewire, sehingga Volt kini bersifat opsional.

    Apa Itu Laravel Volt dan Cara Kerjanya

    Livewire adalah library untuk membuat antarmuka interaktif di Laravel cukup dengan PHP dan Blade, tanpa menulis JavaScript sendiri. Di Livewire 3, satu komponen biasanya terdiri dari dua file: class PHP di app/Livewire dan view Blade di resources/views/livewire.

    Volt menyatukan keduanya. Kamu menulis state dan aksi di blok <?php ?> di bagian atas file Blade, lalu template di bawahnya. Di balik layar, Volt mengompilasi kode itu menjadi class komponen Livewire biasa. Jadi semua fitur Livewire tetap berlaku: wire:click, wire:model, validasi, pagination, dan lainnya.

    Contoh komponen counter dengan Volt:

    <?php
    
    use function LivewireVolt{state};
    
    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>

    Tombol diklik, Livewire mengirim request ke server, method increment dijalankan, dan angka di halaman berubah tanpa reload.

    Status Volt di 2026: Livewire 4 dan Starter Kit Laravel

    Bagian ini penting supaya kamu tidak salah memilih. Berikut kondisi per Oktober 2026, berdasarkan dokumentasi resmi Livewire dan Laravel:

    • Livewire 4 sudah punya komponen satu file bawaan. Perintah php artisan make:livewire sekarang membuat file Blade dengan blok PHP di atasnya, memakai sintaks yang sama dengan Volt versi class. Dokumentasi Livewire menulis bahwa Volt opsional di Livewire 4 dan sebagian besar aplikasi tidak membutuhkannya.
    • Volt tetap dirawat. Versi terbaru (1.11) mendukung Livewire 3 dan 4, serta Laravel 10 sampai 13. Volt masih berguna kalau kamu suka gaya fungsional berbasis closure seperti contoh di atas.
    • Starter kit Livewire di Laravel 12 saat pertama rilis (Februari 2025) memakai Volt dan Flux UI untuk semua halamannya, termasuk login dan pengaturan profil. Starter kit versi terbaru sudah beralih ke Livewire 4 tanpa Volt.

    Kesimpulannya: kalau kamu memulai project baru dan suka gaya class, komponen satu file Livewire 4 sudah cukup. Kalau kamu mewarisi project yang memakai Volt (misalnya dari starter kit Laravel 12 versi awal), atau lebih suka gaya fungsional, Volt masih aman dipakai.

    Cara Install Laravel Volt di Laravel 12

    Kami menguji langkah ini di project Laravel 12 yang baru dibuat (Laravel 12.70, PHP 8.5). Composer otomatis memasang Livewire 4.4 bersama Volt 1.11.

    composer require livewire/volt
    php artisan volt:install

    Perintah volt:install membuat file app/Providers/VoltServiceProvider.php dan mendaftarkannya di bootstrap/providers.php. Service provider ini menentukan folder tempat Volt mencari komponen:

    public function boot(): void
    {
        Volt::mount([
            config('livewire.view_path', resource_path('views/livewire')),
            resource_path('views/pages'),
        ]);
    }

    Secara bawaan, komponen Volt dicari di resources/views/livewire dan resources/views/pages. Tambahkan folder lain ke array itu kalau perlu.

    Satu koreksi untuk tutorial lama: Volt tidak otomatis ikut terpasang bersama paket livewire/livewire. Kamu tetap harus menjalankan composer require livewire/volt. Jalur lama lewat php artisan breeze:install livewire juga sudah tidak disarankan, karena Laravel Breeze tidak lagi mendapat pembaruan sejak Laravel 12. Perbandingan lengkapnya ada di artikel perbedaan Laravel Volt dan Breeze.

    Cek apakah paketnya terpasang:

    composer show livewire/volt

    Membuat Komponen Volt Pertama

    php artisan make:volt counter

    File dibuat di resources/views/livewire/counter.blade.php. Isi dengan kode counter di atas, lalu tampilkan di view Blade mana saja:

    <livewire:counter />

    Layout halaman yang memuat komponen Livewire perlu directive @livewireStyles di dalam <head> dan @livewireScripts sebelum </body>. Kalau kamu belum punya layout, jalankan:

    php artisan livewire:layout

    Perintah itu membuat resources/views/layouts/app.blade.php yang sudah berisi kedua directive tersebut. Di Livewire 4, file inilah yang dipakai sebagai layout bawaan untuk komponen halaman penuh.

    Functional API vs Class API

    Volt punya dua gaya penulisan. Contoh counter di atas memakai functional API. Untuk komponen yang lebih besar, class API biasanya lebih mudah dibaca. Buat dengan opsi --class:

    php artisan make:volt todo --class
    <?php
    
    use LivewireVoltComponent;
    
    new class extends Component {
        public string $task = '';
        public array $tasks = [];
    
        public function add(): void
        {
            $this->validate(['task' => 'required|min:3']);
    
            $this->tasks[] = $this->task;
            $this->reset('task');
        }
    }; ?>
    
    <div>
        <form wire:submit="add">
            <input type="text" wire:model="task" placeholder="Tugas baru">
            <button type="submit">Tambah</button>
            @error('task') <span>{{ $message }}</span> @enderror
        </form>
    
        <ul>
            @foreach ($tasks as $item)
                <li>{{ $item }}</li>
            @endforeach
        </ul>
    </div>
    Functional API Class API
    Cara menulis state state(['count' => 0]) Properti publik public int $count = 0;
    Cara menulis aksi Closure: $increment = fn () => ... Method biasa
    Cocok untuk Komponen kecil dan sederhana Komponen dengan banyak aksi, validasi, atau trait
    Pindah ke Livewire 4 tanpa Volt Perlu ditulis ulang ke gaya class Cukup ganti satu baris import

    Komponen Volt sebagai Halaman Penuh

    Komponen Volt bisa langsung dipasang ke URL tanpa controller:

    use LivewireVoltVolt;
    
    Volt::route('/todo', 'todo');

    Buka /todo dan komponen akan tampil di dalam layout layouts/app. Kalau muncul error No hint path defined for [layouts], berarti file layout belum ada. Jalankan php artisan livewire:layout seperti di atas.

    Menguji Komponen Volt

    Volt menyediakan helper test yang bisa dipakai di PHPUnit maupun Pest:

    use LivewireVoltVolt;
    
    public function test_counter_bertambah(): void
    {
        Volt::test('counter')
            ->call('increment')
            ->call('increment')
            ->assertSet('count', 2);
    }

    Volt atau Komponen Satu File Livewire 4?

    Ini komponen yang sama dengan sintaks bawaan Livewire 4, tanpa Volt. Kami membuatnya dengan php artisan make:livewire search-box:

    <?php
    
    use LivewireComponent;
    
    new class extends Component
    {
        public string $query = '';
    };
    ?>
    
    <div>
        <input type="text" wire:model.live="query" placeholder="Cari...">
        <p>Kata kunci: {{ $query }}</p>
    </div>

    Bedanya dengan Volt class API hanya pada baris import (LivewireComponent, bukan LivewireVoltComponent). Livewire 4 menyimpan file ini di resources/views/components dengan awalan emoji ⚡ di nama file (bisa dimatikan lewat konfigurasi). Untuk route, pakai Route::livewire('/cari', 'search-box').

    Kalau suatu saat ingin melepas Volt, panduan upgrade Livewire 4 menyebut langkahnya:

    1. Ganti use LivewireVoltComponent; menjadi use LivewireComponent;.
    2. Ganti Volt::route() menjadi Route::livewire().
    3. Di test, ganti Volt::test() menjadi Livewire::test().
    4. Hapus app/Providers/VoltServiceProvider.php dan entrinya di bootstrap/providers.php.
    5. Jalankan composer remove livewire/volt.

    Komponen dengan functional API (state(), closure) perlu ditulis ulang ke gaya class sebelum langkah di atas.

    Kapan Volt Cocok, Kapan Tidak

    Volt dan komponen satu file cocok untuk komponen yang berdiri sendiri: form pencarian, counter, toggle, form kontak, atau halaman pengaturan. Logika dan tampilan ada di satu tempat, jadi mudah dirawat.

    Pertimbangkan komponen class terpisah (atau multi-file component di Livewire 4) kalau logikanya sudah panjang, perlu dipakai ulang lewat inheritance, atau tim kamu lebih nyaman memisahkan PHP dan Blade. Logika bisnis yang berat, misalnya perhitungan stok atau integrasi pembayaran, sebaiknya tetap di class service atau controller, lalu dipanggil dari komponen.

    Laravel Volt Bukan Laravolt

    Nama yang mirip sering membuat bingung. Laravel Volt (livewire/volt) adalah API komponen untuk Livewire yang dibahas di artikel ini. Laravolt adalah proyek berbeda: platform untuk membangun aplikasi Laravel (paket Composer laravolt/laravolt), dengan dokumentasi sendiri di laravolt.dev. Keduanya tidak berhubungan.

    Pertanyaan yang Sering Diajukan

    Apakah Laravel Volt sudah termasuk di Livewire?

    Tidak. Volt adalah paket terpisah, livewire/volt, yang harus dipasang lewat Composer. Yang sudah bawaan di Livewire 4 adalah komponen satu file dengan gaya class, yang sintaksnya sama dengan Volt class API.

    Apakah Volt masih layak dipelajari di 2026?

    Layak kalau kamu merawat project yang sudah memakai Volt, termasuk project dari starter kit Livewire versi awal Laravel 12. Untuk project baru, komponen satu file Livewire 4 lebih sederhana karena tidak perlu paket tambahan.

    Livewire itu dipakai untuk apa?

    Livewire dipakai untuk membuat halaman interaktif, seperti form dengan validasi langsung, pencarian tanpa reload, tabel dengan filter, dan dashboard, cukup dengan PHP dan Blade. Cocok untuk tim yang lebih kuat di backend Laravel dibanding framework JavaScript.

    Apakah Volt bisa dipakai di Laravel 11 atau 13?

    Bisa. Volt 1.11 mendukung Laravel 10, 11, 12, dan 13, dengan Livewire 3.6 ke atas atau Livewire 4.

    Ingin membangun dashboard atau aplikasi internal dengan Laravel dan Livewire tetapi tidak punya waktu mengerjakannya sendiri? Tim Arrazy Inovasi di Purwokerto mengerjakan sistem aplikasi custom berbasis Laravel. Materi Laravel lainnya ada di halaman tutorial Laravel.

    Diskusikan kebutuhan aplikasi kamu →

  • Migration Laravel: Cara Membuat, Menjalankan, dan Rollback di Laravel 12

    Migration Laravel adalah file PHP yang berisi perubahan struktur database, seperti membuat tabel, menambah kolom, atau memasang foreign key, sehingga perubahan itu tercatat di kode dan bisa dijalankan ulang di komputer mana pun. Buat migration dengan php artisan make:migration create_categories_table, lalu jalankan dengan php artisan migrate. Panduan ini membahas cara membuat migration di Laravel 12, tipe kolom yang sering dipakai, foreign key, mengubah kolom, dan semua perintah migrate (rollback, refresh, fresh, dan lainnya) beserta bedanya. Semua contoh sudah kami jalankan di Laravel 12.70.

    Belum punya project? Mulai dari cara install Laravel 12. Urutan materi lengkap ada di halaman tutorial Laravel.

    Kenapa Memakai Migration

    Tanpa migration, struktur tabel biasanya dibuat lewat phpMyAdmin atau file SQL yang dikirim lewat chat. Masalahnya, sulit tahu tabel mana yang sudah diubah di laptop siapa, dan server production sering tertinggal. Dengan migration:

    • Setiap perubahan struktur tersimpan di folder database/migrations dan ikut masuk Git.
    • Rekan satu tim cukup menjalankan php artisan migrate untuk menyamakan struktur database.
    • Laravel mencatat migration yang sudah dijalankan di tabel migrations, jadi perintah yang sama tidak menjalankan file dua kali.
    • Perubahan bisa dibatalkan dengan migrate:rollback karena setiap file punya method down().

    Ini juga jawaban untuk pertanyaan “cara memindahkan project Laravel ke laptop lain”: clone repository, jalankan composer install, siapkan .env, lalu php artisan migrate. Struktur tabel akan terbentuk sama persis.

    Cara Membuat Migration di Laravel 12

    php artisan make:migration create_categories_table

    File baru muncul di database/migrations dengan nama berawalan waktu, misalnya 2026_10_19_052752_create_categories_table.php. Awalan waktu itu menentukan urutan eksekusi. Laravel juga menebak nama tabel dari nama migration: pola create_xxx_table menghasilkan kerangka Schema::create('xxx'), sedangkan pola seperti add_category_id_to_products_table menghasilkan kerangka Schema::table('products') untuk mengubah tabel yang sudah ada.

    Migration juga bisa dibuat bersamaan dengan model:

    php artisan make:model Category -mfs

    Opsi -m membuat migration, -f factory, dan -s seeder. Daftar opsi lainnya ada di artikel perintah PHP Artisan.

    Struktur file migration

    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        public function up(): void
        {
            Schema::create('categories', function (Blueprint $table) {
                $table->id();
                $table->string('name', 100);
                $table->string('slug')->unique();
                $table->text('description')->nullable();
                $table->boolean('is_active')->default(true);
                $table->timestamps();
            });
        }
    
        public function down(): void
        {
            Schema::dropIfExists('categories');
        }
    };

    Method up() dijalankan saat migrate, sedangkan down() saat rollback. Isi down() harus membatalkan apa yang dilakukan up(). Kalau up() membuat tabel, down() menghapusnya.

    Tipe Kolom dan Modifier yang Sering Dipakai

    Method Hasil
    $table->id() Primary key id, big integer auto increment
    $table->string('name', 100) VARCHAR, panjang bawaan 255
    $table->text('description') Teks panjang
    $table->integer('qty') / unsignedInteger / unsignedBigInteger Bilangan bulat
    $table->decimal('weight', 8, 2) Desimal presisi tetap
    $table->boolean('is_active') Benar/salah
    $table->date('birth_date') / dateTime / timestamp Tanggal dan waktu
    $table->json('meta') Data JSON
    $table->enum('status', ['draft', 'published']) Pilihan terbatas
    $table->foreignId('category_id') Kolom unsigned big integer untuk relasi
    $table->timestamps() Kolom created_at dan updated_at
    $table->softDeletes() Kolom deleted_at untuk soft delete

    Modifier ditulis berantai setelah tipe kolom: ->nullable() (boleh kosong), ->default(0), ->unique(), ->index(), ->comment('...'), dan ->after('id') (posisi kolom, hanya berlaku di MySQL/MariaDB).

    Untuk uang dalam rupiah, simpan sebagai bilangan bulat (unsignedBigInteger) daripada float. Float bisa menghasilkan selisih pembulatan.

    Foreign Key dan Relasi Antar Tabel

    Misalnya tabel products sudah ada dan ingin dihubungkan ke categories:

    php artisan make:migration add_category_id_to_products_table
    public function up(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->foreignId('category_id')
                ->nullable()
                ->after('id')
                ->constrained()
                ->nullOnDelete();
        });
    }
    
    public function down(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->dropConstrainedForeignId('category_id');
        });
    }

    constrained() otomatis menebak tabel tujuan dari nama kolom (category_id ke tabel categories, kolom id). Pilih aksi saat data induk dihapus:

    • ->cascadeOnDelete(): produk ikut terhapus saat kategorinya dihapus.
    • ->nullOnDelete(): category_id produk diisi NULL. Kolomnya harus nullable(). Kami menguji ini: setelah kategori dihapus, category_id produk berubah menjadi null.
    • ->restrictOnDelete(): kategori tidak bisa dihapus selama masih dipakai produk.

    Urutan file penting. Migration tabel categories harus berjalan sebelum migration yang memasang foreign key ke tabel itu.

    Menjalankan Migration: Semua Perintah dan Bedanya

    Perintah Yang terjadi Data hilang?
    php artisan migrate Menjalankan migration yang belum pernah dijalankan Tidak
    php artisan migrate:status Menampilkan migration yang sudah (Ran) dan belum (Pending), beserta nomor batch Tidak
    php artisan migrate --pretend Menampilkan query SQL tanpa menjalankannya Tidak
    php artisan migrate:rollback Membatalkan batch terakhir (semua file yang dijalankan di migrate terakhir) Ya, untuk tabel/kolom yang di-rollback
    php artisan migrate:rollback --step=1 Membatalkan satu file migration terakhir Ya, sebagian
    php artisan migrate:reset Menjalankan down() semua migration Ya, semua
    php artisan migrate:refresh Rollback semua lewat down(), lalu migrate lagi Ya, semua
    php artisan migrate:fresh Menghapus semua tabel langsung (tanpa down()), lalu migrate lagi Ya, semua
    php artisan migrate:fresh --seed Seperti di atas, lalu menjalankan seeder Ya, diganti data contoh

    Beda migrate:refresh dan migrate:fresh: refresh memanggil down() setiap file satu per satu, sehingga ikut menguji apakah method down() kamu benar. fresh langsung menghapus semua tabel, jadi lebih cepat dan tetap jalan walaupun ada down() yang rusak. Keduanya menghapus semua data. Jangan pernah dijalankan di database production.

    Menjalankan satu file migration saja

    Pakai opsi --path dengan path file relatif dari root project:

    php artisan migrate --path=database/migrations/2026_10_19_052753_add_category_id_to_products_table.php

    Hanya file itu yang dijalankan, migration lain yang masih pending dibiarkan. Kalau file tersebut sudah pernah dijalankan, hasilnya Nothing to migrate.

    Di server production

    php artisan migrate --force

    Di environment production, Laravel meminta konfirmasi sebelum menjalankan migration. --force melewati konfirmasi itu, biasanya dipakai di skrip deploy. Kalau aplikasi di-deploy ke beberapa server sekaligus, tambahkan --isolated agar migration tidak dijalankan dua server bersamaan.

    Menjalankan migration dari kode

    use Illuminate\Support\Facades\Artisan;
    
    Artisan::call('migrate', ['--force' => true]);

    Nilai kembalian 0 berarti berhasil. Cara ini kadang dipakai di installer aplikasi atau job deploy.

    Mengubah, Mengganti Nama, dan Menghapus Kolom

    Jangan mengedit file migration yang sudah dijalankan di server atau sudah dipakai rekan satu tim. Buat migration baru untuk setiap perubahan.

    php artisan make:migration change_price_on_products_table
    public function up(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->unsignedBigInteger('price')->default(0)->change();
        });
    }
    
    public function down(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->unsignedInteger('price')->change();
        });
    }

    Aturan penting dari dokumentasi Laravel: saat memakai change(), tulis ulang semua modifier yang ingin dipertahankan (nullable, default, unsigned, comment). Modifier yang tidak ditulis akan hilang. Sejak Laravel 11, change() tidak lagi butuh paket doctrine/dbal.

    Mengganti nama dan menghapus kolom:

    Schema::table('products', function (Blueprint $table) {
        $table->renameColumn('stock', 'quantity');
        $table->dropColumn(['old_code', 'notes']);
    });

    Error Migration yang Sering Muncul

    • Base table or view already exists. Tabelnya sudah dibuat manual atau dari migration lain, tetapi tidak tercatat di tabel migrations. Di lokal, cara tercepat adalah migrate:fresh. Di production, hapus atau ubah migration yang bentrok, jangan menghapus tabel berisi data.
    • Error foreign key saat migrate. Tabel tujuan belum ada karena urutan file salah, atau tipe kolomnya berbeda. Pakai foreignId() agar tipenya cocok dengan id().
    • SQLSTATE[HY000] [1045] Access denied atau could not find driver. Masalah koneksi, bukan migration. Cek DB_* di .env dan ekstensi pdo_mysql/pdo_sqlite. Panduan lengkapnya ada di artikel instalasi Laravel 12.
    • Rollback gagal. Method down() kosong atau tidak membalik up() dengan benar. Uji dengan migrate:refresh di lokal sebelum dipakai di server.
    • Perintah meminta konfirmasi lalu berhenti di skrip deploy. Environment-nya production. Tambahkan --force.

    Pertanyaan yang Sering Diajukan

    Bagaimana cara menjalankan satu migration tertentu?

    Pakai php artisan migrate --path=database/migrations/nama_file.php. Untuk membatalkan satu migration terakhir, pakai php artisan migrate:rollback --step=1.

    Apa beda migrate:fresh dan migrate:refresh?

    migrate:refresh menjalankan down() setiap migration lalu migrate ulang. migrate:fresh langsung menghapus semua tabel lalu migrate ulang. Keduanya menghapus semua data.

    Apakah boleh mengedit file migration lama?

    Boleh hanya kalau migration itu belum pernah dijalankan di tempat lain selain laptop kamu. Kalau sudah ada di server atau dipakai tim, buat migration baru.

    Apa itu database migration secara umum?

    Istilah ini punya dua arti. Di Laravel, migration berarti versi struktur database dalam bentuk kode. Di luar konteks framework, “migrasi data” juga bisa berarti memindahkan data dari satu sistem ke sistem lain, yang merupakan pekerjaan berbeda.

    Setelah tabel siap, hubungkan ke URL lewat route Laravel dan logika CRUD di controller Laravel 12. Kalau database kamu PostgreSQL, lihat juga tutorial PostgreSQL di Laravel.

    Perlu memindahkan data dari sistem lama atau Excel ke aplikasi Laravel baru? Tim Arrazy Inovasi di Purwokerto mengerjakan pembuatan sistem aplikasi custom, termasuk perancangan database dan migrasi datanya.

    Diskusikan project kamu →

  • Tutorial PostgreSQL di Laravel: Setup, JSONB, dan Full-Text Search

    Laravel secara default menggunakan MySQL. Tapi kalau proyek Anda butuh fitur seperti JSON columns yang lebih canggih, full-text search bawaan, atau JSONB, PostgreSQL adalah pilihan yang solid.

    Artikel ini membahas cara setup PostgreSQL di Laravel, termasuk konfigurasi, perbedaan dengan MySQL, dan fitur-fitur PostgreSQL yang bisa dimanfaatkan langsung dari Eloquent.

    Instalasi dan Konfigurasi

    Pastikan extension PHP untuk PostgreSQL sudah aktif:

    # Ubuntu/Debian
    sudo apt install php-pgsql
    
    # Cek apakah sudah aktif
    php -m | grep pdo_pgsql

    Edit file .env:

    DB_CONNECTION=pgsql
    DB_HOST=127.0.0.1
    DB_PORT=5432
    DB_DATABASE=nama_database
    DB_USERNAME=postgres
    DB_PASSWORD=password_anda

    Tidak perlu ubah konfigurasi lain. Laravel sudah punya driver PostgreSQL bawaan.

    Perbedaan Sintaks Migration

    Sebagian besar sintaks migration sama antara MySQL dan PostgreSQL. Perbedaan utama ada di beberapa tipe data:

    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 10, 2);
    
        // PostgreSQL: pakai jsonb, bukan json biasa
        $table->jsonb('metadata')->nullable();
    
        // PostgreSQL: UUID native
        $table->uuid('external_id')->unique();
    
        // Enum di PostgreSQL butuh cast berbeda
        $table->string('status')->default('active');
    
        $table->timestamps();
    });

    Tips: Di PostgreSQL, pakai jsonb daripada json. JSONB disimpan dalam format binary yang lebih efisien untuk query dan indexing.

    Query JSON dengan PostgreSQL

    PostgreSQL punya operator JSON yang sangat powerful. Di Laravel, Anda bisa pakai operator -> dan ->> langsung di Eloquent:

    // Cari produk berdasarkan field di dalam JSON
    $products = Product::where('metadata->category', 'elektronik')->get();
    
    // Atau dengan whereJsonContains
    $products = Product::whereJsonContains('metadata->tags', 'promo')->get();
    
    // Query nested JSON
    $products = Product::where('metadata->specs->weight', '>', 500)->get();
    

    Contoh migration dengan index JSONB:

    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->jsonb('metadata')->nullable();
        $table->timestamps();
    });
    
    // Tambah index GIN untuk performa query JSONB
    DB::statement('CREATE INDEX products_metadata_gin ON products USING GIN (metadata jsonb_path_ops)');
    

    Full-Text Search

    PostgreSQL punya full-text search bawaan yang bisa langsung dipakai tanpa plugin tambahan:

    // Migration: tambah kolom tsvector
    $table->tsvector('search_vector')->nullable();
    
    // Isi kolom tsvector saat insert/update
    DB::statement("
        UPDATE articles
        SET search_vector = to_tsvector('indonesian', coalesce(title, '') || ' ' || coalesce(content, ''))
    ");
    
    // Query full-text search
    $results = Article::whereRaw(
        "search_vector @@ plainto_tsquery('indonesian', ?)",
        [$keyword]
    )->get();

    Untuk proyek besar, buat trigger PostgreSQL agar search_vector otomatis diperbarui saat data berubah.

    UUID sebagai Primary Key

    PostgreSQL mendukung UUID natively. Cara pakai di Laravel 12:

    Schema::create('orders', function (Blueprint $table) {
        $table->uuid('id')->primary()->default(DB::raw('gen_random_uuid()'));
        $table->string('status');
        $table->timestamps();
    });

    Di model:

    <?php
    
    namespace AppModels;
    
    use IlluminateDatabaseEloquentModel;
    use IlluminateDatabaseEloquentConcernsHasUuids;
    
    class Order extends Model
    {
        use HasUuids;
    }

    Transaksi dan Savepoint

    PostgreSQL mendukung nested transaction lewat savepoint. Di Laravel ini otomatis dipakai saat Anda nested DB::transaction():

    DB::transaction(function () {
        $order = Order::create([...]);
    
        DB::transaction(function () use ($order) {
            // Ini pakai savepoint di PostgreSQL
            // Kalau gagal, hanya bagian ini yang di-rollback
            foreach ($order->items as $item) {
                Inventory::decrement($item->product_id, $item->qty);
            }
        });
    
        $order->update(['status' => 'confirmed']);
    });

    Koneksi Multiple Database

    Kalau Anda pakai MySQL dan PostgreSQL bersamaan, tambahkan konfigurasi di config/database.php:

    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            // ...
        ],
        'pgsql' => [
            'driver'   => 'pgsql',
            'host'     => env('PGSQL_HOST', '127.0.0.1'),
            'port'     => env('PGSQL_PORT', '5432'),
            'database' => env('PGSQL_DATABASE'),
            'username' => env('PGSQL_USERNAME'),
            'password' => env('PGSQL_PASSWORD'),
        ],
    ],

    Di model, tentukan koneksi yang dipakai:

    class AnalyticsEvent extends Model
    {
        protected $connection = 'pgsql';
    }

    Baca Juga

    Butuh tim yang berpengalaman setup aplikasi Laravel dengan PostgreSQL di production? Lihat layanan pengembangan aplikasi kami.