Eloquent — model
Eloquent adalah Active Record: satu objek mewakili satu baris, dan tahu cara menyimpan dirinya. Kekuatannya konvensi; risikonya adalah query yang tak terlihat.
Intisari
- Konvensi: model
Produk→ tabelproduks. Untuk nama Indonesia, sebutkan$tablesendiri. $fillableadalah pertahanan mass assignment. Jangan pernah memakai$guarded = [].- Scope memindahkan kondisi query yang berulang ke dalam model, dan bisa dirangkai.
findmengembalikannull;findOrFailmelempar 404. Pilih dengan sadar.- Eloquent menjalankan query saat koleksi dibaca — inilah sumber N+1 yang dibahas di materi berikutnya.
Model minimal
class Produk extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'produk'; // wajib: konvensi Laravel menebak "produks"
protected $fillable = [
'kategori_id', 'nama', 'slug', 'harga', 'stok', 'status',
];
protected $hidden = ['harga_modal']; // tidak ikut saat diubah jadi JSON
protected function casts(): array
{
return [
'status' => StatusProduk::class, // kolom string ↔ enum PHP
'atribut' => 'array', // kolom JSON ↔ array
'terbit_pada' => 'datetime', // ↔ Carbon
'harga_modal' => 'encrypted', // terenkripsi di database
];
}
}
| Konvensi | Tebakan Laravel | Cara mengubah |
|---|---|---|
| Nama tabel | Bentuk jamak snake_case (produks) | protected $table |
| Primary key | id, auto-increment | $primaryKey, $incrementing |
| Timestamp | created_at, updated_at | public $timestamps = false |
| Foreign key | <model>_id | Argumen kedua di definisi relasi |
Untuk penamaan berbahasa Indonesia, sebutkan $table secara eksplisit. Pluralizer Laravel
memakai aturan bahasa Inggris, jadi Kategori akan ditebak jadi kategoris. Menyatakan
nama tabelnya sendiri lebih jujur daripada berharap tebakannya benar.
Mass assignment — pertahanan yang sering dilubangi
// $fillable menyatakan kolom apa yang boleh diisi massal
Produk::create($request->validated());
// Kalau pengguna mengirim "is_admin=1" dan kolomnya tidak ada di $fillable,
// field itu diabaikan begitu saja.
protected $guarded = []; mematikan pertahanan ini sepenuhnya. Kamu akan menemukannya di
banyak tutorial karena "praktis". Artinya: setiap kolom bisa diisi dari input pengguna, termasuk
role, saldo, atau user_id yang baru ditambahkan rekanmu bulan depan.
Pasangkan $fillable dengan $request->validated() — dua lapis, dan keduanya murah.
Membaca dan menulis
Produk::all(); // hati-hati: seluruh tabel
Produk::find(1); // null kalau tidak ada
Produk::findOrFail(1); // 404 kalau tidak ada
Produk::where('stok', 0)->first();
Produk::whereKey([1, 2, 3])->get();
Produk::create(['nama' => 'Kopi', 'harga' => 25000]);
$produk->update(['harga' => 27000]);
$produk->increment('stok', 10); // satu query UPDATE, bukan baca-tambah-simpan
$produk->delete(); // soft delete kalau modelnya memakai SoftDeletes
$produk->forceDelete(); // benar-benar hilang
Produk::withTrashed()->find(1); // termasuk yang sudah dihapus
Produk::firstOrCreate(['slug' => 'kopi'], ['nama' => 'Kopi', 'harga' => 25000]);
Produk::updateOrCreate(['slug' => 'kopi'], ['harga' => 27000]);
Scope — kondisi berulang yang punya nama
class Produk extends Model
{
#[Scope]
protected function tersedia(Builder $q): void
{
$q->where('stok', '>', 0)->where('status', StatusProduk::Aktif);
}
#[Scope]
protected function dariKategori(Builder $q, Kategori $k): void
{
$q->where('kategori_id', $k->id);
}
}
// Bisa dirangkai seperti method biasa
Produk::tersedia()->dariKategori($kategori)->latest()->paginate(20);
Manfaatnya bukan sekadar lebih pendek. Ketika definisi "tersedia" berubah — misalnya menambah syarat
terbit_pada <= now() — kamu mengubahnya di satu tempat, dan setiap query di aplikasi ikut benar.
Global scope: kuat dan berbahaya
// Selalu batasi ke tenant pengguna aktif — tanpa perlu diingat di setiap query
protected static function booted(): void
{
static::addGlobalScope('tenant', function (Builder $q) {
if ($id = auth()->user()?->tenant_id) {
$q->where('tenant_id', $id);
}
});
}
Global scope adalah pisau tajam. Ia bagus untuk isolasi multi-tenant karena melindungi bahkan query
yang lupa memfilter. Tapi ia juga membuat Produk::count() menghasilkan angka berbeda tergantung
siapa yang login — dan itu membingungkan di perintah Artisan atau job antrean, yang tidak punya pengguna.
Kalau memakainya, sediakan jalan keluar yang jelas (withoutGlobalScope('tenant')) dan pastikan
worker antrean tahu tenant mana yang sedang diproses. Dibahas lagi di Fase 10.
Collection: hasil query bukan array biasa
$produk = Produk::tersedia()->get();
$produk->sum('harga');
$produk->pluck('nama', 'id');
$produk->groupBy('kategori_id');
$produk->sortByDesc('harga')->take(5);
$produk->filter(fn ($p) => $p->harga > 50_000);
Beda tempat, beda biaya. ->filter() pada Collection bekerja di PHP,
setelah semua baris diambil dari database. ->where() pada query builder bekerja di
database. Untuk seratus baris, keduanya sama saja; untuk seratus ribu, yang pertama akan
menghabiskan memori dan yang kedua tidak. Aturannya: saring di database, olah di PHP.
Latihan: buat model Produk dengan $fillable, cast enum, dan scope
tersedia(). Lalu buktikan pertahanan mass assignment: kirim curl berisi field
stok_gaib=999 yang tidak ada di $fillable, dan pastikan ia tidak tersimpan.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.