Raw MySQLi vs Eloquent ORM: Dampak untuk Aplikasi Payment dan Membership
Website BisnisAnalisis dampak raw MySQLi vs ORM untuk aplikasi payment. Kapan harus upgrade, solusi praktis, dan timeline pengembangan tanpa migrasi langsung ke Laravel.
Raw MySQLi vs Eloquent ORM: Kapan Dampaknya Signifikan?
Pertanyaan ini sering muncul saat aplikasi PHP custom berkembang: "Apakah dampak menggunakan raw mysqli vs Eloquent ORM? Apakah harus upgrade?" Jawaban yang jujur adalah: tergantung kompleksitas operasi yang Anda lakukan. Mari analisis secara konkret per use case.
Tingkatan Dampak: Dari Minimal Hingga Kritis
Level 1: Operasi CRUD Sederhana — Dampak MINIMAL ✅
Untuk operasi INSERT, SELECT, UPDATE dasar — raw mysqli bekerja perfectly fine dan malah lebih cepat.
Contoh:
// Raw mysqli dengan prepared statement — aman dan efisien
$stmt = $db->prepare('UPDATE pembayaran SET pesanan_id=?, jumlah=? WHERE id=?');
$stmt->bind_param('iii', $pesanan_id, $jumlah, $id);
$stmt->execute();
Kecepatan: Raw mysqli malah lebih cepat 10-20% karena tidak ada overhead abstraction layer.
Keamanan: Prepared statement + parameter binding aman dari SQL injection.
Kesimpulan: Di level ini, tidak ada dampak signifikan. Upgrade ke ORM justru menambah kompleksitas tanpa nilai tambah.
---Level 2: Query Kompleks & Dinamis — Dampak SIGNIFICANT ⚠️
Saat query menjadi kompleks dengan kondisi yang berubah-ubah, raw mysqli mulai menunjukkan kelemahan: verbose, error-prone, dan sulit dimaintain.
Contoh 1: Update Dengan Kondisi Berbeda
// Raw mysqli — harus manual build query string
$where = '';
if ($orderId > 0) {
$where = "WHERE pesanan_id = $orderId"; // ⚠️ Rentan jika tidak hati-hati
} elseif ($customerId > 0) {
$where = "WHERE pelanggan_id = $customerId";
}
$sql = "UPDATE pembayaran SET status='verified' $where";
// Panjang, rentan error, sulit dibaca
vs Eloquent:
// Eloquent — query builder otomatis aman dan readable
Payment::when($orderId > 0, fn($q) => $q->where('pesanan_id', $orderId))
->when($customerId > 0, fn($q) => $q->where('pelanggan_id', $customerId))
->update(['status' => 'verified']);
Contoh 2: Multi-Table JOIN dengan Aggregate
// Raw mysqli — JOIN kompleks, mudah ada typo
$sql = "SELECT p.id, p.kode_pesanan,
COALESCE(SUM(b.jumlah), 0) total_bayar,
(SELECT total_harga FROM penawaran WHERE pesanan_id = p.id) total_tagih
FROM pesanan p
LEFT JOIN pembayaran b
ON b.pesanan_id = p.id AND b.site_id = p.site_id
WHERE p.site_id = ? AND p.status = ?
GROUP BY p.id
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?";
// Query panjang, JOIN condition mudah typo, hard to maintain
vs Eloquent:
// Eloquent — struktur jelas, JOIN otomatis aman
Order::with(['payments' => fn($q) => $q->where('type', '!=', 'refund')])
->with('offer')
->where('status', 'verified')
->withSum('payments as total_paid', 'jumlah')
->orderByDesc('created_at')
->paginate();
Dampak: Maintenance cost meningkat, bug lebih sering, refactoring lebih riskan.
---Level 3: Transaction Logic untuk Payment/Membership — Dampak CRITICAL 🔴
Di sini raw mysqli mulai menjadi liability serius untuk business logic yang kompleks.
Kasus 1: Subscription Renewal Dengan Multiple Updates
Skenario: Member melakukan renewal subscription. Operasi yang harus terjadi:
- Insert charge transaction baru
- Update tanggal expiry subscription
- Debit dari balance member
- Log ke audit trail
Raw mysqli — Manual Transaction Handling:
$db->begin_transaction();
try {
// 1. Insert charge untuk renewal
$stmt1 = $db->prepare("INSERT INTO member_transactions
(member_id, amount, type, created_at)
VALUES (?, ?, 'renewal', NOW())");
$stmt1->bind_param('ii', $memberId, $amount);
if (!$stmt1->execute()) throw new Exception("Failed to insert transaction");
$txn_id = $db->insert_id;
// 2. Update member subscription
$stmt2 = $db->prepare("UPDATE member_subscriptions
SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY)
WHERE id = ?");
$stmt2->bind_param('i', $member_sub_id);
if (!$stmt2->execute()) throw new Exception("Failed to update subscription");
// 3. Update member_balance
$stmt3 = $db->prepare("UPDATE members SET balance = balance - ? WHERE id = ?");
$stmt3->bind_param('ii', $amount, $memberId);
if (!$stmt3->execute()) throw new Exception("Failed to update balance");
// 4. Insert audit log
$stmt4 = $db->prepare("INSERT INTO audit_logs
(entity, entity_id, action, old_value, new_value, created_at)
VALUES (?, ?, ?, ?, ?, NOW())");
$oldExpiry = /* query fetch */;
$stmt4->bind_param('iisss', 'member_subscription', $member_sub_id,
'renewal', $oldExpiry, $newExpiry);
if (!$stmt4->execute()) throw new Exception("Failed to log");
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
Masalah dengan raw mysqli approach:
- Verbose & repetitif: Banyak boilerplate code untuk 4 operasi sederhana
- Error-prone: Mudah lupa
bind_param, salah type binding, atau typo field name - Hard to test: Sulit untuk unit test tanpa database real
- Maintenance nightmare: Jika requirement berubah (tambah field), harus touch banyak tempat
- Logic tersebar: Business logic tercampur dengan SQL implementation details
Dengan Eloquent + Transaction:
DB::transaction(function() use ($member, $amount) {
$transaction = MemberTransaction::create([
'member_id' => $member->id,
'amount' => $amount,
'type' => 'renewal',
]);
$subscription = $member->activeSubscription();
$oldExpiry = $subscription->expires_at;
$subscription->update([
'expires_at' => now()->addDays(30),
'renewal_count' => $subscription->renewal_count + 1,
]);
$member->decrement('balance', $amount);
AuditLog::create([
'entity' => 'member_subscription',
'entity_id' => $subscription->id,
'action' => 'renewal',
'old_value' => $oldExpiry->format('Y-m-d'),
'new_value' => $subscription->expires_at->format('Y-m-d'),
]);
// Event triggered untuk side effects (email, webhook, etc.)
MemberRenewed::dispatch($member, $subscription);
});
Keuntungan Eloquent approach:
- Clean & readable: Logic flow jelas, mudah dipahami
- Atomic transactions: Built-in, tidak perlu manual error handling
- Testable: Bisa mock models, refactor dengan confidence
- Maintainable: Jika ada field baru, hanya update model, tidak perlu touch SQL
- Event-driven: Side effects terpisah via events, bukan hardcoded di transaction
Kasus 2: Member Status Workflow Dengan Validation
Skenario: Member dapat bertransisi dari status 'active' ke 'suspended' atau 'canceled', tapi tidak boleh dari 'canceled' ke 'active'.
Raw mysqli — Manual Status Validation:
// Harus define status transitions di tempat yang berbeda
$validTransitions = [
'active' => ['suspended', 'canceled'],
'suspended' => ['active', 'canceled'],
'expired' => ['active'],
'canceled' => [], // terminal state
];
// Fetch current status
$result = $db->query("SELECT status FROM members WHERE id = $memberId");
$currentStatus = $result->fetch_assoc()['status'];
// Validasi transition
if (!isset($validTransitions[$currentStatus]) ||
!in_array($newStatus, $validTransitions[$currentStatus])) {
throw new Exception("Invalid status transition: $currentStatus -> $newStatus");
}
// Update
$db->begin_transaction();
try {
$stmt = $db->prepare("UPDATE members SET status = ?, updated_at = NOW() WHERE id = ?");
$stmt->bind_param('si', $newStatus, $memberId);
if (!$stmt->execute()) throw new Exception("Update failed");
$stmt2 = $db->prepare("INSERT INTO member_status_history
(member_id, status_from, status_to, changed_at)
VALUES (?, ?, ?, NOW())");
$stmt2->bind_param('iss', $memberId, $currentStatus, $newStatus);
if (!$stmt2->execute()) throw new Exception("History insert failed");
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
Dengan Eloquent + Enum + Event:
// Define status sebagai Enum (tipe-safe)
enum MemberStatus: string {
case ACTIVE = 'active';
case SUSPENDED = 'suspended';
case EXPIRED = 'expired';
case CANCELED = 'canceled';
}
// Model dengan status machine
class Member extends Model {
protected $casts = ['status' => MemberStatus::class];
public function changeStatus(MemberStatus $newStatus): void {
if (!$this->canTransitionTo($newStatus)) {
throw new InvalidStatusTransition(
"Cannot transition from {$this->status->value} to {$newStatus->value}"
);
}
$this->update(['status' => $newStatus]);
MemberStatusChanged::dispatch($this, $newStatus);
}
private function canTransitionTo(MemberStatus $newStatus): bool {
$transitions = [
MemberStatus::ACTIVE => [MemberStatus::SUSPENDED, MemberStatus::CANCELED],
MemberStatus::SUSPENDED => [MemberStatus::ACTIVE, MemberStatus::CANCELED],
MemberStatus::EXPIRED => [MemberStatus::ACTIVE],
MemberStatus::CANCELED => [],
];
return in_array($newStatus, $transitions[$this->status] ?? []);
}
}
// Event listener untuk side effects
class MemberStatusChanged {
public function handle($member, $newStatus) {
MemberStatusHistory::create([
'member_id' => $member->id,
'status_from' => $member->getOriginal('status'),
'status_to' => $newStatus,
]);
// Trigger notifications async
SendMemberStatusNotification::dispatch($member, $newStatus);
}
}
Keuntungan:
- Status validation logic terpusat di satu tempat (method
canTransitionTo) - Enum mencegah invalid status values
- Events memisahkan side effects, mudah test logic core tanpa side effects
- Perubahan workflow tidak perlu ubah query, hanya update method
Tabel Ringkas: Dampak Per Aspek
| Aspek | Raw MySQLi | Eloquent ORM | Dampak untuk Membership/Payment |
|---|---|---|---|
| Simple CRUD | ✅ Cepat, aman | ✅ Clean | Minimal |
| Complex Query | ⚠️ Verbose, error-prone | ✅ Clean, maintainable | Medium |
| Transaction Logic | ❌ Manual, kompleks, boilerplate | ✅ Built-in, atomic | HIGH |
| Status Machine | ❌ Manual validation logic | ✅ Enum, Events | HIGH |
| Unit Testing | ❌ Hard tanpa DB real | ✅ Mockable, testable | HIGH |
| Refactoring | ⚠️ Risky, banyak tempat berubah | ✅ Safe, centralized logic | Medium |
| Onboarding Tim Baru | ⚠️ Perlu training SQL detail | ✅ Standard framework pattern | Medium |
Solusi Praktis: Tidak Perlu Migrasi Sekarang
Anda tidak perlu langsung pindah ke Laravel untuk menangani kompleksitas membership dan payment. Ada jalan tengah: buat abstraction layer sendiri tanpa butuh full ORM.
Opsi 1: Buat Transaction Wrapper Reusable
// app/Support/DbTransaction.php
class DbTransaction {
public static function run(callable $fn, mysqli $db) {
$db->begin_transaction();
try {
$result = $fn();
$db->commit();
return $result;
} catch (Throwable $e) {
$db->rollback();
throw new TransactionFailedException($e->getMessage(), 0, $e);
}
}
}
// Usage di payment logic
DbTransaction::run(function() use ($db, $memberId, $amount) {
// Semua operasi di sini otomatis wrapped dalam transaction
$stmt1 = $db->prepare("INSERT INTO member_transactions ...");
$stmt1->execute();
$stmt2 = $db->prepare("UPDATE member_subscriptions ...");
$stmt2->execute();
}, $db);
Opsi 2: Buat Base Model Class Minimal
// app/Support/Model.php
abstract class Model {
protected static string $table;
protected static string $primaryKey = 'id';
protected array $attributes = [];
protected static mysqli $db;
public static function create(array $data): static {
// Generic insert with auto-audit
}
public function update(array $data): void {
// Generic update dengan auto-log
}
public function delete(): void {
// Generic delete
}
// Business logic stays here
}
// Konkret model
class Member extends Model {
protected static string $table = 'members';
public function changeStatus(string $newStatus) {
// Validation + update menggunakan base class method
if (!$this->canTransitionTo($newStatus)) {
throw new InvalidStatusException();
}
$this->update(['status' => $newStatus]);
// Log otomatis via base class
}
}
Opsi 3: Fokus pada Consistency
Jangan perlu ORM, tapi pastikan semua payment/membership logic follow pattern yang konsisten:
- Semua operasi keuangan harus wrap di transaction
- Semua state change harus log ke audit trail
- Semua error harus log ke error monitor
- Semua async task (email, webhook) harus trigger via event/queue
Rekomendasi Timeline
Sekarang (Bulan 1-3):
- ✅ Tetap pakai raw mysqli untuk CRUD sederhana
- ✅ Buat
DbTransactionwrapper untuk semua payment/order logic - ✅ Buat
AuditHelperuntuk auto-log setiap transaksi - ✅ Standardisasi error handling di service layer
Setelah Membership v1 Launch (Bulan 3-6):
- ✅ Monitor dampak raw mysqli vs abstraction layer di production
- ✅ Ukur maintenance cost & refactoring difficulty
- ✅ Buat base Model class untuk mengurangi repetisi
Jika diperlukan (Bulan 6+):
- ✅ Evaluate Laravel migration jika kompleksitas terus naik
- ✅ Atau upgrade ke PHP ORM ringan seperti Medoo atau NotORM
Kesimpulan
Dampak raw mysqli baru signifikan saat operasi menjadi kompleks. Untuk payment dan membership — di mana transaction safety dan maintainability kritis — abstraction layer (ORM atau custom wrapper) sangat bermanfaat mengurangi error dan complexity.
Namun migrasi ke Laravel bukan keharusan sekarang. Dengan membuat transaction wrapper dan standardisasi pattern, Anda bisa menangani kompleksitas membership dengan raw mysqli secara aman. Evaluasi kembali kebutuhan ORM setelah fitur baru launch dan tim berkembang.
Key takeaway: Bukan framework-nya yang penting, melainkan konsistensi pattern dan abstraction layer yang tepat untuk problem domain Anda.