Blog

  • Kenapa Setiap Bisnis di Purwokerto Butuh Website?

    Kenapa Setiap Bisnis di Purwokerto Butuh Website?

    Kenapa bisnis di Purwokerto butuh website? Di zaman serba digital seperti sekarang, perilaku konsumen berubah sangat cepat. Sebelum membeli produk atau menggunakan jasa, mereka selalu mencari informasi terlebih dahulu di internet. Bahkan untuk kebutuhan lokal seperti kuliner, jasa desain, hingga pendidikan di Purwokerto, calon pelanggan cenderung mengetikkan kata kunci seperti “toko kue Purwokerto” atau “jasa pembuatan website Purwokerto” di Google.

    Bagi pelaku bisnis, kondisi ini merupakan peluang besar sekaligus tantangan. Jika usaha Anda belum memiliki website profesional, maka Anda sedang kehilangan potensi pelanggan setiap hari. Artikel ini akan membahas secara lengkap kenapa bisnis butuh website dan apa manfaat website untuk bisnis di Purwokerto, terutama bagi UMKM dan pelaku usaha jasa.


    1. Website adalah Identitas Digital Bisnis Anda

    Memiliki website profesional bukan hanya soal tampil keren di dunia maya. Website adalah wajah digital dari bisnis Anda. Sama seperti papan nama di depan toko, keberadaan website membantu calon pelanggan mengenali, mengingat, dan mempercayai brand Anda.

    Ketika seseorang mencari “jasa percetakan Purwokerto” dan menemukan situs Anda dengan tampilan profesional, lengkap dengan profil, testimoni, dan kontak yang jelas, maka tingkat kepercayaan akan meningkat drastis.
    Sebaliknya, jika bisnis Anda tidak memiliki website sama sekali, calon pelanggan bisa ragu dan beralih ke kompetitor.

    Website juga membantu menciptakan kesan profesional. Bahkan usaha rumahan sekalipun bisa tampak kredibel di mata konsumen jika memiliki website yang tertata rapi, cepat diakses, dan berisi informasi lengkap.


    2. Meningkatkan Kepercayaan Pelanggan Lokal

    Salah satu alasan utama kenapa bisnis butuh website adalah untuk meningkatkan kepercayaan pelanggan. Di Purwokerto, banyak calon pelanggan lebih memilih bisnis yang bisa mereka temukan di Google. Mereka akan mengecek apakah bisnis tersebut punya situs resmi, alamat jelas, ulasan pelanggan, dan portofolio.

    Menurut survei kecil di kalangan pelaku UMKM, 78% pelanggan merasa lebih yakin membeli dari bisnis yang memiliki website profesional. Itu karena website menjadi bukti nyata bahwa bisnis Anda sungguh ada, bukan abal-abal.

    Selain itu, website juga bisa menampilkan portofolio dan testimoni pelanggan, dua hal penting dalam membangun reputasi.
    Misalnya Anda seorang fotografer atau desainer grafis lokal — dengan menampilkan hasil karya di website, calon klien bisa langsung menilai kualitas layanan Anda tanpa harus bertanya panjang lebar.


    3. Meningkatkan Daya Jangkau dan Potensi Penjualan

    Jika toko fisik Anda hanya bisa dijangkau oleh pelanggan di sekitar, website justru bisa menjangkau siapa saja, kapan saja. Dengan optimasi SEO yang tepat, website Anda dapat muncul di hasil pencarian bagi pengguna di Purwokerto, Banyumas, Purbalingga, hingga Yogyakarta.

    Kuncinya ada di SEO lokal, yaitu optimasi agar situs Anda mudah ditemukan berdasarkan lokasi. Contohnya, Anda bisa menargetkan kata kunci seperti “jasa pembuatan website Purwokerto” atau “toko herbal Purwokerto”.
    Ketika calon pelanggan mengetik kata kunci itu, situs Anda akan muncul dan memberikan peluang baru untuk penjualan.

    Bahkan tanpa mengeluarkan biaya iklan besar, website bisa bekerja sebagai tenaga marketing otomatis. Ia mempromosikan produk, mengedukasi calon pelanggan, dan mengarahkan mereka untuk membeli — selama 24 jam sehari, 7 hari seminggu.


    4. Media Promosi yang Efisien dan Hemat Biaya

    Bandingkan biaya promosi konvensional seperti brosur atau spanduk yang hanya bertahan beberapa minggu. Dengan website, Anda bisa menampilkan informasi produk, promo, dan testimoni selama bertahun-tahun dengan biaya perawatan yang jauh lebih murah.

    Website juga bisa dikembangkan menjadi platform promosi digital yang terukur. Anda bisa melihat dari mana pengunjung datang, berapa lama mereka membaca konten, dan halaman mana yang paling sering dibuka.
    Dari data tersebut, Anda dapat menyusun strategi pemasaran yang lebih efektif dan menghemat biaya iklan.


    5. Meningkatkan Kredibilitas di Mata Mitra dan Investor

    Selain pelanggan, website juga meningkatkan kepercayaan dari mitra bisnis dan calon investor.
    Bagi perusahaan yang ingin tumbuh, memiliki situs resmi menunjukkan keseriusan dan profesionalitas. Misalnya, ketika Anda ingin menjalin kerja sama dengan instansi pemerintah atau sekolah di Purwokerto, salah satu hal pertama yang mereka cek adalah: apakah bisnis Anda punya website resmi?

    Tanpa website, peluang kerja sama besar bisa hilang begitu saja.


    6. Website Sekolah dan Lembaga Pendidikan di Purwokerto Juga Penting

    Tak hanya untuk bisnis, sekolah dan lembaga pendidikan di Purwokerto juga memerlukan website resmi. Website sekolah berfungsi sebagai pusat informasi akademik, pendaftaran siswa baru, hingga publikasi kegiatan sekolah.

    Jika Anda pengelola sekolah, Anda bisa mempertimbangkan layanan Jasa Website Sekolah Purwokerto dari Arrazy Inovasi.
    Kami membantu sekolah menampilkan profil, kurikulum, galeri kegiatan, serta integrasi sistem informasi akademik dengan desain modern dan mudah diakses.


    7. Mau Punya Website Profesional untuk Bisnis Anda?

    Bagi Anda pelaku UMKM, startup, atau profesional di Purwokerto, kini saatnya membangun website bisnis profesional.
    Tim Arrazy Inovasi siap membantu Anda membuat website yang menarik, responsif, dan dioptimasi untuk hasil pencarian Google.

    Kami tidak hanya membuat tampilan website, tapi juga membantu dalam strategi SEO lokal, sehingga bisnis Anda mudah ditemukan oleh calon pelanggan di sekitar Purwokerto.
    Dengan pendekatan soft selling dan pengalaman melayani berbagai sektor usaha, kami siap menjadi mitra digital Anda dalam jangka panjang.


    🔗 Artikel Lain yang Bisa Anda Baca:

    1. Perbandingan Website vs Media Sosial untuk Bisnis Lokal
    2. Jasa Website Purwokerto dari Arrazy Inovasi
    3. Cara Meningkatkan Branding Bisnis Lewat Website

    Kesimpulan

    Website bukan lagi pilihan, tetapi kebutuhan mutlak bagi bisnis modern, termasuk di Purwokerto.
    Dengan memiliki website profesional, bisnis Anda akan lebih mudah ditemukan, dipercaya, dan diingat oleh pelanggan.
    Selain itu, website menjadi aset digital yang terus bekerja — meskipun Anda sedang tidur.

    Jadi, daripada kehilangan peluang setiap hari karena belum punya website, lebih baik mulai sekarang bangun fondasi digital bisnis Anda bersama Arrazy Inovasi.
    Karena di era internet, pelanggan pertama yang menemukan Anda adalah yang paling berpeluang menjadi pelanggan setia.


    Sebelum memilih, ada baiknya pelajari dulu 7 tips memilih jasa website di Purwokerto — checklist praktis agar tidak salah pilih vendor.


    Siap Buat Website untuk Bisnis Anda di Purwokerto?

    Jika Anda sudah yakin bisnis di Purwokerto butuh website, langkah selanjutnya adalah memilih partner yang tepat. Jasa website Purwokerto dari Arrazy Inovasi hadir dengan pendekatan yang berbeda. Mulai dari Rp 6.900/hari, sudah termasuk domain, hosting, dan optimasi SEO lokal Purwokerto.

  • Contoh Implementasi Observer di Laravel 12: Auto-Slug, Audit Trail, dan Cache

    Artikel ini melanjutkan penjelasan konsep Observer di Laravel 12 dengan studi kasus implementasi lengkap: sistem audit trail dan auto-slug generation.

    Studi Kasus 1: Auto-Slug Generation

    Masalah umum: setiap kali artikel dibuat atau diupdate, slug harus di-generate dari title. Tanpa Observer, logika ini tersebar di berbagai controller.

    Dengan Observer, cukup satu tempat:

    <?php
    
    namespace AppObservers;
    
    use AppModelsArticle;
    use IlluminateSupportStr;
    
    class ArticleObserver
    {
        public function creating(Article $article): void
        {
            $article->slug = $this->generateUniqueSlug($article->title);
        }
    
        public function updating(Article $article): void
        {
            if ($article->isDirty('title')) {
                $article->slug = $this->generateUniqueSlug($article->title, $article->id);
            }
        }
    
        private function generateUniqueSlug(string $title, ?int $excludeId = null): string
        {
            $slug  = Str::slug($title);
            $query = Article::where('slug', $slug);
    
            if ($excludeId) {
                $query->where('id', '!=', $excludeId);
            }
    
            if (!$query->exists()) {
                return $slug;
            }
    
            // Tambah angka kalau slug sudah ada
            $counter = 1;
            while (Article::where('slug', "{$slug}-{$counter}")
                          ->when($excludeId, fn ($q) => $q->where('id', '!=', $excludeId))
                          ->exists()) {
                $counter++;
            }
    
            return "{$slug}-{$counter}";
        }
    }

    Studi Kasus 2: Audit Trail Otomatis

    Rekam semua perubahan pada model penting, berguna untuk compliance, debugging, atau fitur “lihat riwayat perubahan”:

    <?php
    
    namespace AppObservers;
    
    use AppModelsArticle;
    use AppModelsAuditLog;
    
    class ArticleObserver
    {
        public function created(Article $article): void
        {
            $this->log('created', $article, [], $article->getAttributes());
        }
    
        public function updated(Article $article): void
        {
            $this->log('updated', $article, $article->getOriginal(), $article->getChanges());
        }
    
        public function deleted(Article $article): void
        {
            $this->log('deleted', $article, $article->getAttributes(), []);
        }
    
        private function log(string $action, Article $article, array $old, array $new): void
        {
            AuditLog::create([
                'user_id'     => auth()->id(),
                'model_type'  => Article::class,
                'model_id'    => $article->id,
                'action'      => $action,
                'old_values'  => $old,
                'new_values'  => $new,
                'ip_address'  => request()->ip(),
            ]);
        }
    }

    Model AuditLog:

    Schema::create('audit_logs', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
        $table->string('model_type');
        $table->unsignedBigInteger('model_id');
        $table->string('action'); // created, updated, deleted
        $table->json('old_values')->nullable();
        $table->json('new_values')->nullable();
        $table->string('ip_address')->nullable();
        $table->timestamp('created_at');
    
        $table->index(['model_type', 'model_id']);
    });

    Studi Kasus 3: Cache Invalidation

    Cache artikel halaman statis dan harus di-clear saat artikel berubah:

    <?php
    
    namespace AppObservers;
    
    use AppModelsArticle;
    use IlluminateSupportFacadesCache;
    
    class ArticleObserver
    {
        public function saved(Article $article): void
        {
            // Clear cache artikel individual
            Cache::forget("article:{$article->id}");
            Cache::forget("article:{$article->slug}");
    
            // Clear cache daftar artikel
            Cache::forget('articles:latest');
            Cache::forget("articles:category:{$article->category_id}");
        }
    
        public function deleted(Article $article): void
        {
            Cache::forget("article:{$article->id}");
            Cache::forget("article:{$article->slug}");
            Cache::forget('articles:latest');
        }
    }

    Studi Kasus 4: Observer dengan Multiple Models

    Kalau beberapa model butuh audit trail yang sama, buat Observer yang reusable:

    <?php
    
    namespace AppObservers;
    
    use AppModelsAuditLog;
    
    class AuditableObserver
    {
        public function created($model): void
        {
            AuditLog::create([
                'user_id'    => auth()->id(),
                'model_type' => get_class($model),
                'model_id'   => $model->id,
                'action'     => 'created',
                'new_values' => $model->getAttributes(),
            ]);
        }
    
        public function updated($model): void
        {
            AuditLog::create([
                'user_id'    => auth()->id(),
                'model_type' => get_class($model),
                'model_id'   => $model->id,
                'action'     => 'updated',
                'old_values' => $model->getOriginal(),
                'new_values' => $model->getChanges(),
            ]);
        }
    }

    Register ke beberapa model sekaligus:

    // Di AppServiceProvider
    Article::observe(AuditableObserver::class);
    Product::observe(AuditableObserver::class);
    Order::observe(AuditableObserver::class);

    Bypass Observer saat Seeding

    <?php
    
    namespace DatabaseSeeders;
    
    use AppModelsArticle;
    
    class ArticleSeeder extends Seeder
    {
        public function run(): void
        {
            // Bypass observer agar tidak trigger audit trail saat seeding
            Article::withoutObservers(function () {
                Article::factory(100)->create();
            });
        }
    }

    Baca Juga

    Butuh tim yang bantu implementasi arsitektur yang clean di aplikasi Laravel? Lihat layanan pengembangan aplikasi kami.

  • Contoh Implementasi Policy dan Gate di Laravel 12: Studi Kasus CMS Multi-Role

    Artikel ini melanjutkan penjelasan konsep Policy dan Gate di Laravel 12 dengan studi kasus implementasi lengkap: sistem manajemen konten dengan beberapa level akses.

    Studi Kasus: Sistem CMS dengan Multi-Role

    Skenario: aplikasi CMS dengan role admin, editor, dan author. Aturannya:

    • Admin bisa lakukan semua aksi di artikel mana saja
    • Editor bisa buat, edit, dan publish artikel mana saja
    • Author hanya bisa buat dan edit artikel miliknya sendiri

    Setup Model User dengan Role

    <?php
    
    namespace App\Models;
    
    use Illuminate\Foundation\Auth\User as Authenticatable;
    
    class User extends Authenticatable
    {
        protected $fillable = ['name', 'email', 'password', 'role'];
    
        public function isAdmin(): bool
        {
            return $this->role === 'admin';
        }
    
        public function isEditor(): bool
        {
            return in_array($this->role, ['admin', 'editor']);
        }
    }

    ArticlePolicy Lengkap

    <?php
    
    namespace App\Policies;
    
    use App\Models\Article;
    use App\Models\User;
    
    class ArticlePolicy
    {
        // Ini dijalankan sebelum semua method lain
        // Return true = admin bypass semua check
        public function before(User $user, string $ability): ?bool
        {
            if ($user->isAdmin()) {
                return true;
            }
    
            return null; // null = lanjut ke check berikutnya
        }
    
        public function viewAny(?User $user): bool
        {
            // Semua orang bisa lihat daftar artikel yang published
            return true;
        }
    
        public function view(?User $user, Article $article): bool
        {
            if ($article->status === 'published') {
                return true;
            }
    
            // Draft hanya bisa dilihat pemilik atau editor
            return $user && ($article->user_id === $user->id || $user->isEditor());
        }
    
        public function create(User $user): bool
        {
            return $user->hasVerifiedEmail();
        }
    
        public function update(User $user, Article $article): bool
        {
            // Editor bisa edit semua, author hanya punya sendiri
            return $user->isEditor() || $article->user_id === $user->id;
        }
    
        public function delete(User $user, Article $article): bool
        {
            // Hanya admin (via before()) atau pemilik artikel
            return $article->user_id === $user->id;
        }
    
        public function publish(User $user, Article $article): bool
        {
            // Hanya editor ke atas yang bisa publish
            return $user->isEditor();
        }
    
        public function restore(User $user, Article $article): bool
        {
            return $user->isEditor() || $article->user_id === $user->id;
        }
    }

    Menggunakan Policy di Controller

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Article;
    use App\Http\Requests\StoreArticleRequest;
    
    class ArticleController extends Controller
    {
        public function index()
        {
            $this->authorize('viewAny', Article::class);
    
            $articles = Article::with('author')
                               ->when(!auth()->user()?->isEditor(), fn ($q) =>
                                   $q->where('status', 'published')
                                     ->orWhere('user_id', auth()->id())
                               )
                               ->paginate(15);
    
            return view('articles.index', compact('articles'));
        }
    
        public function edit(Article $article)
        {
            $this->authorize('update', $article);
            return view('articles.edit', compact('article'));
        }
    
        public function destroy(Article $article)
        {
            $this->authorize('delete', $article);
            $article->delete();
            return redirect()->route('articles.index')
                             ->with('success', 'Artikel dihapus.');
        }
    
        public function publish(Article $article)
        {
            $this->authorize('publish', $article);
            $article->update(['status' => 'published', 'published_at' => now()]);
            return back()->with('success', 'Artikel dipublish.');
        }
    }

    Policy di Blade Template

    @foreach ($articles as $article)
        <div>
            <h2>{{ $article->title }}</h2>
    
            @can('update', $article)
                <a href="{{ route('articles.edit', $article) }}">Edit</a>
            @endcan
    
            @can('publish', $article)
                @if($article->status === 'draft')
                    <form action="{{ route('articles.publish', $article) }}" method="POST">
                        @csrf @method('PATCH')
                        <button>Publish</button>
                    </form>
                @endif
            @endcan
    
            @can('delete', $article)
                <form action="{{ route('articles.destroy', $article) }}" method="POST">
                    @csrf @method('DELETE')
                    <button>Hapus</button>
                </form>
            @endcan
        </div>
    @endforeach

    Gate untuk Aksi Global

    Untuk akses fitur yang tidak terkait model tertentu, pakai Gate:

    // Di AppServiceProvider
    Gate::define('access-analytics', fn (User $user) => $user->isEditor());
    Gate::define('export-all-data',  fn (User $user) => $user->isAdmin());
    
    // Di controller
    Gate::authorize('access-analytics');
    return view('analytics.dashboard');
    
    // Di Blade
    @can('access-analytics')
        <a href="/analytics">Analytics</a>
    @endcan

    Baca Juga

    Butuh tim yang bantu implementasi sistem otorisasi yang tepat untuk aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

  • Contoh Penggunaan Concurrency di Laravel 12: Dashboard, API Paralel, dan Defer

    Artikel sebelumnya membahas konsep Concurrency di Laravel 12. Artikel ini fokus pada implementasi: studi kasus nyata bagaimana Concurrency bisa mempercepat aplikasi secara signifikan.

    Studi Kasus 1: Dashboard dengan Banyak Data Source

    Dashboard admin yang butuh data dari beberapa tabel berbeda. Ini biasanya jadi bottleneck karena diquery satu per satu.

    Sebelum (sequential — sekitar 800ms):

    public function dashboard()
    {
        $totalOrders    = Order::thisMonth()->count();          // ~200ms
        $totalRevenue   = Order::thisMonth()->sum('total');     // ~200ms
        $pendingOrders  = Order::where('status', 'pending')->count(); // ~150ms
        $newCustomers   = User::thisMonth()->count();           // ~150ms
        $topProducts    = Product::topSelling(5)->get();       // ~100ms
    
        return view('dashboard', compact(...));
    }

    Sesudah (paralel — sekitar 200ms):

    use IlluminateSupportFacadesConcurrency;
    
    public function dashboard()
    {
        [$totalOrders, $totalRevenue, $pendingOrders, $newCustomers, $topProducts] =
            Concurrency::run([
                fn() => Order::thisMonth()->count(),
                fn() => Order::thisMonth()->sum('total'),
                fn() => Order::where('status', 'pending')->count(),
                fn() => User::thisMonth()->count(),
                fn() => Product::topSelling(5)->get(),
            ]);
    
        return view('dashboard', compact(
            'totalOrders', 'totalRevenue', 'pendingOrders', 'newCustomers', 'topProducts'
        ));
    }

    Studi Kasus 2: Multiple API Calls

    Halaman product detail yang butuh data dari beberapa API eksternal:

    use IlluminateSupportFacadesConcurrency;
    
    public function productDetail(Product $product): View
    {
        [$reviews, $stock, $shippingOptions] = Concurrency::run([
            fn() => $this->reviewApi->getProductReviews($product->id),    // API 1
            fn() => $this->inventoryApi->getStock($product->sku),         // API 2
            fn() => $this->shippingApi->getOptions($product->weight),     // API 3
        ]);
    
        return view('products.detail', compact('product', 'reviews', 'stock', 'shippingOptions'));
    }

    Kalau masing-masing API butuh 500ms, tanpa concurrency total 1.5 detik. Dengan concurrency, cukup ~500ms.

    Studi Kasus 3: Defer untuk Aksi Non-Blocking

    User logout — beberapa aksi perlu terjadi tapi tidak perlu selesai sebelum response dikirim:

    use IlluminateSupportFacadesConcurrency;
    
    public function logout(Request $request): RedirectResponse
    {
        $user = $request->user();
    
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
    
        // Jalankan setelah response dikirim
        Concurrency::defer([
            fn() => $this->activityLog->record($user, 'logout'),
            fn() => $this->deviceTokenService->revokeAll($user),
            fn() => $this->sessionCleanup->cleanup($user),
        ]);
    
        return redirect('/');
    }

    Studi Kasus 4: Generate Laporan Parallel

    Generate beberapa bagian laporan sekaligus lalu gabungkan:

    public function generateAnnualReport(int $year): array
    {
        [$salesData, $customerData, $productData, $regionData] = Concurrency::run([
            fn() => $this->salesReport->compile($year),
            fn() => $this->customerReport->compile($year),
            fn() => $this->productReport->compile($year),
            fn() => $this->regionReport->compile($year),
        ]);
    
        return [
            'year'     => $year,
            'sales'    => $salesData,
            'customers' => $customerData,
            'products' => $productData,
            'regions'  => $regionData,
            'generated_at' => now()->toIso8601String(),
        ];
    }

    Error Handling di Concurrency

    Kalau salah satu closure melempar exception, Concurrency akan meneruskan exception tersebut:

    use IlluminateSupportFacadesConcurrency;
    
    try {
        [$data1, $data2] = Concurrency::run([
            fn() => riskyApiCall(),
            fn() => anotherApiCall(),
        ]);
    } catch (Exception $e) {
        // Handle error — biasanya fallback ke data cached atau default
        Log::warning("Concurrency error: {$e->getMessage()}");
        [$data1, $data2] = [getFromCache('data1'), getFromCache('data2')];
    }

    Kapan Tidak Pakai Concurrency

    Jangan pakai untuk operasi yang sangat cepat. Overhead membuka child process (~50-100ms) lebih mahal dari manfaatnya kalau setiap closure selesai dalam 5ms.

    Gunakan untuk operasi yang masing-masing butuh 100ms ke atas, terutama I/O seperti HTTP request, query database, atau baca file besar.

    Baca Juga

    Butuh tim yang bantu optimasi performa aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

  • Contoh Penggunaan Context di Laravel 12: Request Tracing dan Queue Propagation

    Artikel sebelumnya menjelaskan apa itu Context facade di Laravel 12. Artikel ini fokus pada implementasi nyata: bagaimana Context dipakai untuk trace request ID, propagasi ke jobs, dan debugging yang lebih mudah.

    Studi Kasus 1: Request ID di Semua Log

    Masalah klasik: saat ada error, sulit tahu log mana yang terkait satu request. Solusi dengan 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 di seluruh request otomatis punya context:

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

    Studi Kasus 2: Propagasi Context ke Queue Jobs

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

    Solusi: Context secara otomatis dipropagasi ke jobs jika menggunakan Illuminate\Queue\SerializesModels atau kalau job implement ShouldQueue — tapi kita harus pastikan data context yang ingin dipropagasi ada di “hydrated context”.

    <?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...
        }
    }

    Studi Kasus 3: Hidden Context untuk Data Sensitif

    Beberapa context tidak ingin muncul di log tapi perlu tersedia di kode:

    // 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');
    

    Studi Kasus 4: Debugging Feature Flag

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Context;
    
    class TrackFeatureFlags
    {
        public function handle(Request $request, Closure $next): mixed
        {
            $user = $request->user();
    
            if ($user) {
                Context::add([
                    'features' => [
                        'new_checkout'     => $user->hasFeature('new-checkout'),
                        'ai_recommendations' => $user->hasFeature('ai-rec'),
                    ],
                ]);
            }
    
            return $next($request);
        }
    }
    

    Kalau ada bug laporan dari user tertentu, cek log dan langsung tahu fitur apa yang aktif saat itu.

    Dehydrate dan Hydrate untuk Custom Serialization

    Untuk objek yang tidak bisa di-serialize otomatis:

    Context::dehydrating(function (Context $context) {
        // Ubah Carbon ke string saat queue job dikirim
        if ($context->has('started_at')) {
            $context->add('started_at', $context->get('started_at')->toIso8601String());
        }
    });
    
    Context::hydrated(function (Context $context) {
        // Kembalikan ke Carbon saat job diproses
        if ($context->has('started_at')) {
            $context->add('started_at', Carbon::parse($context->get('started_at')));
        }
    });

    Baca Juga

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

  • Contoh Penggunaan Contract di Laravel 12: Implementasi dan Binding

    Kalau Anda sudah membaca artikel tentang apa itu Contract di Laravel 12, artikel ini melanjutkannya dengan contoh penggunaan nyata: bagaimana membuat implementasi Contract sendiri dan kapan ini berguna dalam proyek.

    Menggunakan Contract Bawaan Laravel

    Contract bawaan Laravel ada di namespace Illuminate\Contracts\*. Contoh yang paling sering dipakai adalah type-hinting di constructor untuk decoupling:

    <?php
    
    namespace App\Services;
    
    use Illuminate\Contracts\Cache\Repository as CacheContract;
    use Illuminate\Contracts\Mail\Mailer as MailerContract;
    
    class NotificationService
    {
        public function __construct(
            private CacheContract  $cache,
            private MailerContract $mailer,
        ) {}
    
        public function sendIfNotSent(string $userId, string $template): void
        {
            $cacheKey = "notification:{$userId}:{$template}";
    
            if ($this->cache->has($cacheKey)) {
                return; // Sudah dikirim
            }
    
            $this->mailer->send($template, [], fn ($m) => $m->to($userId));
            $this->cache->put($cacheKey, true, now()->addDay());
        }
    }

    Service ini bisa ditest dengan mudah — tinggal inject mock CacheContract dan MailerContract.

    Membuat Contract Sendiri

    Studi kasus: aplikasi yang butuh fitur kirim notifikasi ke berbagai channel (email, SMS, WhatsApp). Dengan Contract, kita bisa ganti implementasi tanpa mengubah kode yang memakainya.

    Buat Contract interface di app/Contracts/NotificationChannel.php:

    <?php
    
    namespace App\Contracts;
    
    interface NotificationChannel
    {
        public function send(string $recipient, string $message): bool;
        public function isAvailable(): bool;
    }

    Buat beberapa implementasi:

    <?php
    
    namespace App\Services\Notifications;
    
    use App\Contracts\NotificationChannel;
    use Illuminate\Support\Facades\Mail;
    
    class EmailChannel implements NotificationChannel
    {
        public function send(string $recipient, string $message): bool
        {
            try {
                Mail::raw($message, fn ($m) => $m->to($recipient));
                return true;
            } catch (\Exception $e) {
                return false;
            }
        }
    
        public function isAvailable(): bool
        {
            return config('mail.default') !== null;
        }
    }
    
    <?php
    
    namespace App\Services\Notifications;
    
    use App\Contracts\NotificationChannel;
    
    class WhatsAppChannel implements NotificationChannel
    {
        public function __construct(
            private WhatsAppApiClient $client
        ) {}
    
        public function send(string $recipient, string $message): bool
        {
            return $this->client->sendMessage($recipient, $message);
        }
    
        public function isAvailable(): bool
        {
            return !empty(config('services.whatsapp.token'));
        }
    }
    

    Bind Contract ke Implementasi di Service Provider

    <?php
    
    namespace App\Providers;
    
    use App\Contracts\NotificationChannel;
    use App\Services\Notifications\WhatsAppChannel;
    use Illuminate\Support\ServiceProvider;
    
    class AppServiceProvider extends ServiceProvider
    {
        public function register(): void
        {
            // Bind default implementation
            $this->app->bind(NotificationChannel::class, WhatsAppChannel::class);
        }
    }

    Sekarang di mana saja Anda type-hint NotificationChannel, Laravel otomatis inject WhatsAppChannel:

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Contracts\NotificationChannel;
    
    class AlertController extends Controller
    {
        public function __construct(
            private NotificationChannel $channel
        ) {}
    
        public function send(Request $request): Response
        {
            $sent = $this->channel->send(
                $request->recipient,
                $request->message
            );
    
            return response()->json(['success' => $sent]);
        }
    }

    Manfaat: Mudah Ganti Implementasi

    Kalau besok ingin switch dari WhatsApp ke SMS, cukup ganti binding di service provider:

    $this->app->bind(NotificationChannel::class, SmsChannel::class);

    Controller dan semua kode yang pakai NotificationChannel tidak perlu diubah sama sekali.

    Contoh: Kontekstual Binding

    Kalau perlu inject implementasi berbeda di class berbeda:

    $this->app->when(AlertController::class)
              ->needs(NotificationChannel::class)
              ->give(WhatsAppChannel::class);
    
    $this->app->when(ReportController::class)
              ->needs(NotificationChannel::class)
              ->give(EmailChannel::class);

    Baca Juga

    Mau tim yang bantu bangun arsitektur aplikasi Laravel yang scalable? Lihat layanan pengembangan aplikasi kami.

  • Apa Itu Observer di Laravel 12 dan Kapan Menggunakannya

    Kalau Event dan Listener cocok untuk “sesuatu terjadi di aplikasi, beri tahu komponen lain”, Observer punya fokus berbeda: “ketika Eloquent model berubah, jalankan kode ini.”

    Artikel ini menjelaskan apa itu Observer di Laravel 12, perbedaannya dengan Event Listener, dan kapan sebaiknya dipakai.

    Apa Itu Observer?

    Observer adalah kelas yang merespons event lifecycle Eloquent model: saat model dibuat, diupdate, dihapus, dll. Daripada pasang listener di banyak tempat, Observer mengumpulkan semua hook untuk satu model di satu kelas.

    Event lifecycle yang bisa di-observe:

    • creating / created
    • updating / updated
    • saving / saved
    • deleting / deleted
    • restoring / restored (untuk soft deletes)
    • forceDeleting / forceDeleted

    Membuat Observer

    php artisan make:observer ArticleObserver --model=Article

    File dibuat di app/Observers/ArticleObserver.php:

    <?php
    
    namespace AppObservers;
    
    use AppModelsArticle;
    use IlluminateSupportStr;
    
    class ArticleObserver
    {
        public function creating(Article $article): void
        {
            // Auto-generate slug sebelum disimpan
            if (empty($article->slug)) {
                $article->slug = Str::slug($article->title);
            }
        }
    
        public function created(Article $article): void
        {
            // Kirim notifikasi ke admin
            cache()->forget('articles.count');
        }
    
        public function updating(Article $article): void
        {
            // Update slug kalau title berubah
            if ($article->isDirty('title')) {
                $article->slug = Str::slug($article->title);
            }
        }
    
        public function deleting(Article $article): void
        {
            // Hapus gambar terkait sebelum artikel dihapus
            $article->images()->each(fn ($img) => $img->delete());
        }
    }

    Registrasi Observer

    Di app/Providers/AppServiceProvider.php:

    <?php
    
    namespace AppProviders;
    
    use AppModelsArticle;
    use AppObserversArticleObserver;
    use IlluminateSupportServiceProvider;
    
    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Article::observe(ArticleObserver::class);
        }
    }

    Atau bisa dengan attribute di model (Laravel 12):

    use AppObserversArticleObserver;
    use IlluminateDatabaseEloquentAttributesObservedBy;
    
    #[ObservedBy([ArticleObserver::class])]
    class Article extends Model
    {
        // ...
    }

    Observer vs Event Listener: Kapan Pakai Yang Mana?

    Kondisi Pilihan
    Hook spesifik ke satu Eloquent model Observer
    Aksi yang dipicu dari banyak model berbeda Event Listener
    Logika bisnis yang tidak terkait model Eloquent Event Listener
    Semua hook untuk satu model ingin dikumpulkan di satu tempat Observer

    Menonaktifkan Observer Sementara

    Berguna saat import data massal, agar Observer tidak dipanggil ribuan kali:

    // Import tanpa trigger Observer
    Article::withoutObservers(function () {
        foreach ($rows as $row) {
            Article::create($row);
        }
    });
    
    // Atau nonaktifkan event model seluruhnya
    Article::unsetEventDispatcher();

    Setelah import selesai, event otomatis aktif kembali.

    Baca Juga

    Mau tim yang bantu setup arsitektur model-driven di aplikasi Laravel Anda? Lihat layanan pengembangan aplikasi kami.

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