Aplikasi yang sehat selalu bertumbuh: gateway pembayaran baru, notifikasi ke channel baru, aturan bisnis baru. Pertanyaan arsitekturalnya bukan apakah perubahan datang, melainkan berapa mahal setiap perubahan itu. Bila menambah metode pembayaran kedua berarti mengedit dua puluh file, fondasi kita gagal. Laravel sendiri dibangun sebagai kumpulan titik ekstensi — chapter ini menutup section dengan memetakan titik-titik itu, lalu merancang seam (sendi perubahan) sendiri lewat interface payment gateway yang implementasinya bisa ditukar tanpa menyentuh controller.

Peta Titik Ekstensi Laravel #

Hampir semua bab “custom” di section ini sebenarnya satu keluarga: cara berbeda untuk memperluas framework tanpa memotong kode vendor. Berikut petanya:

Titik ekstensiDipakai untukKita lihat di
Service container bindingMenukar implementasi interfaceService Container
Event dan listenerBereaksi tanpa mengubah kode pemicuKonsep Event-Driven
MacroMenambah method ke class frameworkMacros
Custom validation ruleLogika validasi domain sendiriCustom Validation Rules
Custom castCara khusus membaca kolomCustom Casts
Custom Eloquent builderBahasa query domainCustom Eloquent Builders
MiddlewareMemotong request/responsePola Middleware Lanjutan
PaketMembagi kapabilitas antar aplikasiPackage Development

Peta ini penting karena titik ekstensi yang salah pilih mahal: macro yang seharusnya event, atau interface yang seharusnya cukup method.

Menyusun Seam di Titik Volatil #

Titik paling volatil di aplikasi bisnis biasanya integrasi pihak ketiga — payment gateway contoh klasiknya. Kita definisikan kontrak dulu, dan hanya kontrak, di app/Contracts/PaymentGateway.php:

php
<?php

namespace App\Contracts;

interface PaymentGateway
{
    /**
     * Menagih sejumlah uang dari kartu; mengembalikan pesan hasil.
     */
    public function charge(string $kartu, int $jumlah): string;
}

Lalu dua implementasi: StripeGateway untuk produksi (di sini versi test-mode tanpa panggilan jaringan) dan SandboxGateway untuk pengembangan. Pilihan di antara keduanya tidak boleh jadi urusan controller, jadi binding di AppServiceProvider membaca config:

php
$this->app->singleton(\App\Contracts\PaymentGateway::class, function ($app) {
    return match ($app['config']->get('services.payment.driver')) {
        'stripe' => new \App\Services\Payments\StripeGateway(),
        default => new \App\Services\Payments\SandboxGateway(),
    };
});

Route hanya mengenal kontrak — dependency injection yang menyelipkan implementasi aktif:

php
Route::post('/tagihan', function (Request $request, PaymentGateway $gateway) {
    return $gateway->charge((string) $request->input('kartu'), (int) $request->input('jumlah'));
});

Membuktikan seam ini bekerja cukup dengan test kecil: satu test membiarkan config bawaan, satu test mengganti driver sebelum container resolve. Detail cara kerja binding dibahas di dokumentasi container :

Output yang Dihasilkan:

text
class  : App\Services\Payments\SandboxGateway
charge : OK: Rp150.000 (sandbox)

class  : App\Services\Payments\StripeGateway
charge : OK: Rp250.000 (stripe test-mode)

Satu baris config berpindah implementasi, nol baris controller berubah. Menambah gateway ketiga — Midtrans, misalnya — berarti menulis satu class baru dan satu entri match.

Event sebagai Titik Palang #

Interface menyelesaikan siapa yang bekerja; event menyelesaikan siapa yang perlu tahu. Kedua gateway di atas sama-sama memicu PembayaranBerhasil setelah tagihan sukses:

php
Event::dispatch(new PembayaranBerhasil($jumlah, 'sandbox'));

return 'OK: '.\Kas::rupiah($jumlah).' (sandbox)';

Dengan begitu, fitur lintas minat — kirim kwitansi, tambah poin loyalitas, catat audit — menempel dari luar tanpa menyentuh kode pembayaran:

php
Event::listen(PembayaranBerhasil::class, function (PembayaranBerhasil $e) use (&$tercatat) {
    $tercatat[] = "{$e->driver} {$e->jumlah}";
});

Output yang Dihasilkan:

text
listener menerima: sandbox 150000

Bedanya dengan memanggil Mail::send() langsung di gateway sederhana tapi fundamental: pemanggilan langsung mengunci satu konsumen, sedangkan event mengizinkan N konsumen datang kemudian. Mekanisme dispatch dan listener dijelaskan tuntas di dokumentasi events .

Ekstensi yang Disiapikan versus yang Ditunda #

Bukan berarti semua batas harus jadi interface hari ini. Aturan praktisnya begini: siapkan seam saat volatilitasnya terbukti — integrasi eksternal (network, pihak ketiga, uang), titik yang berbeda antar lingkungan (sandbox vs produksi), atau fitur yang roadmap-nya eksplisit. Tunda bila spekulatif: “mungkin nanti butuh dua database” bukan alasan membuat lapisan abstraksi DB sendiri — Laravel sudah menyediakan contracts untuk itu.

Config switch adalah bentuk ekstensi termurah: PAYMENT_DRIVER=stripe di .env mengganti seluruh perilaku tanpa deploy ulang kode. Sebaliknya, jebakan klasiknya adalah membangun “framework dalam framework” — lapisan abstraksi yang menerima semua kemungkinan, dipakai satu aplikasi, dan dipahami satu orang.

Mendokumentasikan Titik Ekstensi untuk Tim #

Seam yang tidak terdokumentasi sama dengan tidak ada: anggota tim baru akan tetap memanggil StripeGateway langsung karena lebih cepat terlihat. Dokumentasi minimal tiap seam cukup tiga baris — apa yang boleh berubah, bagaimana menukarnya, test mana yang membuktikannya — lalu letakkan di README tim, mengikuti pola dokumentasi arsitektur di Maintainable Laravel Applications .

Dari route pertama sampai seam pembayaran ini, course kita memakai satu ide yang sama: Laravel menyediakan bahan — container, provider, event, macro, paket — dan kita yang memilih di mana menaruh sendinya. Aplikasi yang mudah tumbuh bukan yang paling banyak abstraksinya, melainkan yang sendinya tepat berada di titik yang memang sering bergerak.