← Semua pembelajaran / Laravel Nol → Enterprise
Fase 2 · Database & Eloquent

Eloquent — model

Eloquent adalah Active Record: satu objek mewakili satu baris, dan tahu cara menyimpan dirinya. Kekuatannya konvensi; risikonya adalah query yang tak terlihat.

Sumber asli laravel.com Resmi Rangkuman ~8 menit baca

Intisari

  • Konvensi: model Produk → tabel produks. Untuk nama Indonesia, sebutkan $table sendiri.
  • $fillable adalah pertahanan mass assignment. Jangan pernah memakai $guarded = [].
  • Scope memindahkan kondisi query yang berulang ke dalam model, dan bisa dirangkai.
  • find mengembalikan null; findOrFail melempar 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
        ];
    }
}
KonvensiTebakan LaravelCara mengubah
Nama tabelBentuk jamak snake_case (produks)protected $table
Primary keyid, auto-increment$primaryKey, $incrementing
Timestampcreated_at, updated_atpublic $timestamps = false
Foreign key<model>_idArgumen 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.