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 ekstensi | Dipakai untuk | Kita lihat di |
|---|---|---|
| Service container binding | Menukar implementasi interface | Service Container |
| Event dan listener | Bereaksi tanpa mengubah kode pemicu | Konsep Event-Driven |
| Macro | Menambah method ke class framework | Macros |
| Custom validation rule | Logika validasi domain sendiri | Custom Validation Rules |
| Custom cast | Cara khusus membaca kolom | Custom Casts |
| Custom Eloquent builder | Bahasa query domain | Custom Eloquent Builders |
| Middleware | Memotong request/response | Pola Middleware Lanjutan |
| Paket | Membagi kapabilitas antar aplikasi | Package 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
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:
$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:
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:
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:
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:
Event::listen(PembayaranBerhasil::class, function (PembayaranBerhasil $e) use (&$tercatat) {
$tercatat[] = "{$e->driver} {$e->jumlah}";
});Output yang Dihasilkan:
listener menerima: sandbox 150000Bedanya 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.