Karena template website ini aku bangun sendiri dari nol, aku harus selalu siaga memeriksa ulang semua aspek visualnya. Hari ini, saat mengecek sebuah postingan yang lumayan panjang melalui smartphone, aku menemukan sebuah case klasik namun menyebalkan: Layout Bocor alias Horizontal Overflow. Layar website tiba-tiba bisa digeser ke kanan-kiri seolah ada elemen gaib yang melebar.
Karena postingan tersebut memuat banyak block Gutenberg, aku harus mencari sumber masalahnya satu per satu.
Awalnya, kecurigaanku jatuh pada block code yang isinya cukup panjang ke bawah. Untuk membuktikannya, aku membuat post dummy khusus yang hanya berisi kode panjang tersebut. Anehnya, setelah di-publish dan dicek di mobile, tampilannya normal-normal saja. Kesimpulannya: bukan panjang baris kode secara vertikal yang menyebabkan masalah.
Karena sudah cukup pusing menebak-nebak, akhirnya aku menggunakan metode debugging paling barbar namun absolut: menghapus block satu per satu dari editor sampai layout-nya kembali normal.
Setelah proses eliminasi yang panjang, bingo! Aku menemukan penyebab aslinya. Masalah bersumber dari satu block code yang berisi data seperti ini:
a:3:{s:6:”_token”;s:40:”abc123…”;s:4:”cart”;a:2:{…};s:8:”_flash”;a:2:{…}}
Ternyata, masalah ini terjadi karena teks di dalam block code bawaan Gutenberg tersebut ditulis tanpa spasi sama sekali dan terus memanjang ke samping. Secara bawaan, HTML tidak akan memotong (wrap) teks yang tidak memiliki spasi. Akibatnya, teks tersebut secara paksa membentang menerobos batas layar, menyeret seluruh struktur layout bersamanya.
Solusi untuk menjinakkan elemen liar ini adalah dengan membatasi lebar maksimal (max-width) pada wadah kodenya, dan memaksa munculnya scroll horizontal di dalam kotak tersebut agar tidak mengganggu layout utama.
Before
.article__body pre {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 24px;
overflow-x: auto;
margin: 32px 0;
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.7;
color: var(--text-primary);
}
.article__body code {
font-family: var(--font-mono);
font-size: 0.85em;
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: var(--radius-sm);
color: var(--accent);
border: 1px solid var(--border);
}
.article__body pre code {
background: none;
border: none;
padding: 0;
color: inherit;
font-size: inherit;
}
After
.article__body pre {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 24px;
margin: 32px 0;
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.7;
color: var(--text-primary);
max-width: 90vw; /* Batas aman lebar di layar mobile */
overflow-x: auto !important; /* Paksa muncul scrollbar horizontal */
-webkit-overflow-scrolling: touch; /* Scroll mulus di perangkat sentuh */
}
.article__body code {
font-family: var(--font-mono);
font-size: 0.85em;
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: var(--radius-sm);
color: var(--accent);
border: 1px solid var(--border);
}
.article__body pre code {
background: none;
border: none;
padding: 0;
color: inherit;
font-size: inherit;
display: block; /* Mengubah sifat inline menjadi block */
white-space: pre !important; /* Mencegah teks dipaksa turun ke bawah */
word-break: normal !important;
}
Dengan tambahan display: block dan max-width: 90vw, sifat elemen code yang awalnya adalah inline kini bisa mematuhi aturan wadah induknya. Sekarang, seberapa pun panjang string tanpa spasi yang dimasukkan, teksnya akan tertahan rapi di dalam kotak dan memunculkan scroll horizontal yang elegan di layar HP. Kasus ditutup!
Beberapa waktu lalu, aku mendapat tugas yang sepertinya simpel di sebuah legacy project Laravel: menambahkan metode pembayaran baru. Langkah pertamaku tentu saja observasi, mencari tahu di mana fitur baru ini sebaiknya diletakkan. Mengingat ini project Laravel, ekspektasiku kodenya akan mengikuti pola MVC yang rapi.
Namun, ceritanya jadi menarik ketika aku menemukan kode penanganan payment yang sudah ada. Ternyata, semuanya ditaruh di dalam Controller! Mulai dari pemanggilan external resource sampai proses lainnya menumpuk di sana. Padahal, sepemahamanku, pemanggilan external resource sebaiknya diletakkan di Service class agar Controller tetap bersih dan ‘dumb‘.
class InvoiceController extends Controller
{
public function saveInvoice(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|exists:users,id',
'course_id' => 'required|exists:courses,id',
]);
$user = User::findOrFail($request->user_id);
$course = Course::findOrFail($request->course_id);
// Simpan data invoice
$invoice = Invoice::create([
'user_id' => $user->id,
'course_id' => $course->id,
'amount' => $course->price,
'status' => 'unpaid',
]);
Config::$serverKey = config('midtrans.server_key');
Config::$isProduction = config('midtrans.is_production');
Config::$isSanitized = true;
Config::$is3ds = true;
$orderId = 'EDU-' . time() . '-' . $user->id;
$params = [
'transaction_details' => [
'order_id' => $orderId,
'gross_amount' => $course->price,
],
'customer_details' => [
'first_name' => $user->name,
'email' => $user->email,
],
'item_details' => [
[
'id' => $course->id,
'price' => $course->price,
'quantity' => 1,
'name' => $course->title,
],
],
];
$snapToken = Snap::getSnapToken($params);
Transaction::create([
'user_id' => $user->id,
'course_id' => $course->id,
'invoice_id' => $invoice->id,
'midtrans_order_id' => $orderId,
'snap_token' => $snapToken,
'amount' => $course->price,
'status' => 'pending',
]);
return response()->json([
'invoice_id' => $invoice->id,
'order_id' => $orderId,
'snap_token' => $snapToken,
]);
}
public function handleWebhook(Request $request): JsonResponse
{
$payload = $request->all();
$transaction = Transaction::where('midtrans_order_id', $payload['order_id'])->firstOrFail();
$transactionStatus = $payload['transaction_status'];
if ($transactionStatus === 'settlement' || $transactionStatus === 'capture') {
DB::transaction(function () use ($transaction, $payload) {
$transaction->update([
'status' => 'paid',
'paid_at' => now(),
]);
Enrollment::updateOrCreate(
['user_id' => $transaction->user_id, 'course_id' => $transaction->course_id],
['status' => 'active', 'enrolled_at' => now()]
);
});
}
return response()->json(['message' => 'OK']);
}
}
Ini potongan kode yang ku temukan.
Melihat pola ini, aku mulai menebak. Jangan-jangan ada kode serupa di Controller lain? Benar saja, aku menemukan duplikasi kode yang sama persis di Controller ShippingCash dan Invoice.
class ShippingCashController extends Controller
{
public function saveShippingCash(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|exists:users,id',
'course_id' => 'required|exists:courses,id',
]);
$user = User::findOrFail($request->user_id);
$course = Course::findOrFail($request->course_id);
// Simpan data shipping cash
$shippingCash = ShippingCash::create([
'user_id' => $user->id,
'course_id' => $course->id,
'amount' => $course->price, // ❌ tidak pakai effective_price
'status' => 'unpaid',
]);
// ❌ Konfigurasi Midtrans copy-paste dari InvoiceController
Config::$serverKey = config('midtrans.server_key');
Config::$isProduction = config('midtrans.is_production');
Config::$isSanitized = true;
Config::$is3ds = true;
$orderId = 'EDU-' . time() . '-' . $user->id;
$params = [
'transaction_details' => [
'order_id' => $orderId,
'gross_amount' => $course->price,
],
'customer_details' => [
'first_name' => $user->name,
'email' => $user->email,
],
'item_details' => [
[
'id' => $course->id,
'price' => $course->price,
'quantity' => 1,
'name' => $course->title,
],
],
];
// ❌ HTTP call ke Midtrans copy-paste dari InvoiceController
$snapToken = Snap::getSnapToken($params);
Transaction::create([
'user_id' => $user->id,
'course_id' => $course->id,
'shipping_cash_id' => $shippingCash->id,
'midtrans_order_id' => $orderId,
'snap_token' => $snapToken,
'amount' => $course->price,
'status' => 'pending',
]);
return response()->json([
'shipping_cash_id' => $shippingCash->id,
'order_id' => $orderId,
'snap_token' => $snapToken,
]);
}
public function handleWebhook(Request $request): JsonResponse
{
$payload = $request->all();
// ❌ Tidak ada verifikasi signature — sama persis dengan InvoiceController
$transaction = Transaction::where('midtrans_order_id', $payload['order_id'])->firstOrFail();
$transactionStatus = $payload['transaction_status'];
if ($transactionStatus === 'settlement' || $transactionStatus === 'capture') {
DB::transaction(function () use ($transaction, $payload) {
$transaction->update([
'status' => 'paid',
'paid_at' => now(),
]);
Enrollment::updateOrCreate(
['user_id' => $transaction->user_id, 'course_id' => $transaction->course_id],
['status' => 'active', 'enrolled_at' => now()]
);
});
}
return response()->json(['message' => 'OK']);
}
}
Kondisi ini sebenarnya bisa diatasi dengan mudah menggunakan Interface. Jadi, ketika nanti ada penambahan metode pembayaran baru, kita tidak perlu repot.
Akhirnya, aku berdiskusi dengan klien. Aku jelaskan kondisi kode saat ini dan menyarankan pendekatan yang lebih baik. Singkat cerita, aku mendapat lampu hijau untuk melakukan refactoring 🛠️ pada sistem payment ini.
Langkah perbaikannya begini: Aku membuat PaymentInterface, lalu membuat dua Service terpisah yaitu Stripe dan Midtrans. Setelah itu, aku me-register (binding) mereka bertiga di AppServiceProvider. Terakhir, aku mengubah kode di Controller ShippingCash dan Invoice yang tadinya hardcode, menjadi hanya memanggil interface tersebut. Memang pemanggilannya masih di Controller karena aku menghindari perombakan sistem secara masif, tapi setidaknya kode ini sekarang jauh lebih rapi.
interface PaymentInterface
{
/**
* Buat transaksi pending dan ambil snap token dari gateway.
*
* @return array ['order_id', 'snap_token', 'snap_url', 'amount']
*/
public function initiatePayment(User $user, Course $course): array;
/**
* Proses notifikasi webhook dari gateway.
*/
public function handleWebhook(array $payload): void;
}
class InvoiceController extends Controller
{
public function __construct(private PaymentInterface $payment) {}
public function saveInvoice(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|exists:users,id',
'course_id' => 'required|exists:courses,id',
]);
$user = User::findOrFail($request->user_id);
$course = Course::findOrFail($request->course_id);
// Simpan data invoice
$invoice = Invoice::create([
'user_id' => $user->id,
'course_id' => $course->id,
'amount' => $course->effective_price,
'status' => 'unpaid',
]);
// Buat payment — tidak perlu tahu gateway apa yang jalan di baliknya
$payment = $this->payment->initiatePayment($user, $course);
return response()->json([
'invoice_id' => $invoice->id,
'order_id' => $payment['order_id'],
'snap_token' => $payment['snap_token'],
'amount' => $payment['amount'],
]);
}
public function handleWebhook(Request $request): JsonResponse
{
$this->payment->handleWebhook($request->all());
return response()->json(['message' => 'OK']);
}
}
Bagi yang penasaran tentang apa itu Interface dan bagaimana cara kerjanya, kalian bisa membaca artikelku yang ini 📖 [Link Artikel].