Model, Relasi, dan Seeder Sistem Kasir Laravel
Diterbitkan 15 September 2026
Pada tahap ketiga kita akan menghubungkan struktur database yang sudah dibuat pada tahap sebelumnya dengan Eloquent Model Laravel.
Migration hanya mendefinisikan struktur tabel di database. Agar tabel tersebut mudah digunakan dari aplikasi Laravel, kita membutuhkan model.
Melalui model Eloquent kita dapat:
- mengambil data dari database;
- menambah data;
- memperbarui data;
- menghapus data;
- membuat relasi antar tabel;
- melakukan casting tipe data;
- mengakses data terkait menggunakan relasi.
Selain model dan relasi, pada tahap ini kita juga akan membuat seeder untuk mengisi data awal sistem kasir.
Data awal tersebut antara lain:
- akun administrator;
- akun kasir;
- kategori;
- satuan;
- produk contoh;
- stok awal dan histori stok;
- pengaturan toko.
Setelah tahap ini selesai, database tidak lagi kosong dan sudah siap digunakan pada tahap berikutnya untuk membuat CRUD master data.
Hasil yang Akan Dibuat
Pada tahap ini kita akan melengkapi model:
User
Category
Unit
Product
Customer
Supplier
Purchase
PurchaseDetail
Sale
SaleDetail
StockMovement
Setting
Kita juga akan membuat relasi utama:
Category -> Products
Unit -> Products
Product -> Category
Product -> Unit
Product -> SaleDetails
Product -> PurchaseDetails
Product -> StockMovements
User -> Sales
User -> Purchases
Customer -> Sales
Supplier -> Purchases
Sale -> User
Sale -> Customer
Sale -> SaleDetails
SaleDetail -> Sale
SaleDetail -> Product
Purchase -> User
Purchase -> Supplier
Purchase -> PurchaseDetails
PurchaseDetail -> Purchase
PurchaseDetail -> Product
StockMovement -> Product
Kemudian kita akan membuat:
KasirSeeder
untuk mengisi data awal.
Persiapan
Pastikan Tahap 01 dan Tahap 02 sudah selesai.
Masuk ke folder project:
cd kasir-laravel
Pastikan migration sudah berjalan:
php artisan migrate:status
Seluruh migration utama harus berstatus:
Ran
Jika belum:
php artisan migrate
1. Memahami Model Eloquent
Eloquent adalah ORM bawaan Laravel.
ORM adalah singkatan dari:
Object Relational Mapping
Secara sederhana, satu model Laravel biasanya mewakili satu tabel database.
Contoh:
Model Product
akan menggunakan tabel:
products
Model:
Category
akan menggunakan:
categories
Laravel mengikuti konvensi penamaan sehingga kita tidak perlu menulis nama tabel secara manual selama nama model dan tabel mengikuti pola Laravel.
Contoh:
Product -> products
Sale -> sales
PurchaseDetail -> purchase_details
StockMovement -> stock_movements
2. Lokasi Model
Model disimpan pada:
app/Models
Setelah Tahap 02, file berikut seharusnya sudah tersedia:
app/Models/Category.php
app/Models/Unit.php
app/Models/Product.php
app/Models/Customer.php
app/Models/Supplier.php
app/Models/Purchase.php
app/Models/PurchaseDetail.php
app/Models/Sale.php
app/Models/SaleDetail.php
app/Models/StockMovement.php
app/Models/Setting.php
Model:
app/Models/User.php
sudah tersedia sejak awal project Laravel dan sudah kita ubah pada Tahap 01.
3. Memahami $fillable
Pada banyak model kita akan menggunakan:
protected $fillable = [
// ...
];
$fillable menentukan atribut yang boleh diisi menggunakan mass assignment.
Contoh:
Product::create([
'sku' => 'BRG-001',
'name' => 'Air Mineral',
'selling_price' => 4000,
]);
Agar kode tersebut dapat digunakan, atribut yang dikirim melalui create() harus diizinkan pada $fillable.
Kita tetap harus melakukan validasi pada controller sebelum menyimpan data.
$fillable bukan pengganti validasi.
4. Memahami Casting
Database menyimpan beberapa tipe data seperti:
decimal
boolean
datetime
integer
Laravel dapat mengubah nilai atribut secara otomatis melalui casting.
Contoh:
protected function casts(): array
{
return [
'is_active' => 'boolean',
];
}
Dengan casting tersebut:
$product->is_active
akan diperlakukan sebagai boolean:
true
atau:
false
Untuk nilai uang kita akan menggunakan:
'decimal:2'
Contoh:
'selling_price' => 'decimal:2',
Untuk tanggal transaksi:
'sale_date' => 'datetime',
5. Memahami Relasi belongsTo dan hasMany
Dua jenis relasi yang paling banyak digunakan pada sistem kasir ini adalah:
belongsTo
hasMany
Contoh:
categories
products
Satu kategori mempunyai banyak produk.
Pada model Category:
public function products()
{
return $this->hasMany(Product::class);
}
Sedangkan satu produk dimiliki oleh satu kategori.
Pada model Product:
public function category()
{
return $this->belongsTo(Category::class);
}
Hubungan tersebut dapat digambarkan:
Category
|
| hasMany
v
Products
Dari arah produk:
Product
|
| belongsTo
v
Category
6. Model Category
Buka:
app/Models/Category.php
Ganti seluruh isinya menjadi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
protected $fillable = [
'name',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}
Model ini mengizinkan pengisian:
name
dan mempunyai relasi:
Category hasMany Products
Contoh penggunaan nanti:
$category = Category::find(1);
$products = $category->products;
Variabel:
$products
akan berisi kumpulan produk pada kategori tersebut.
7. Model Unit
Buka:
app/Models/Unit.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Unit extends Model
{
protected $fillable = [
'name',
'symbol',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}
Satu satuan dapat digunakan banyak produk.
Contoh:
Pieces
dapat digunakan oleh:
Air Mineral
Kopi Sachet
Mie Instan
Relasinya:
Unit hasMany Products
8. Model Product
Buka:
app/Models/Product.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Product extends Model
{
protected $fillable = [
'category_id',
'unit_id',
'sku',
'name',
'purchase_price',
'selling_price',
'stock',
'minimum_stock',
'is_active',
];
protected function casts(): array
{
return [
'purchase_price' => 'decimal:2',
'selling_price' => 'decimal:2',
'stock' => 'integer',
'minimum_stock' => 'integer',
'is_active' => 'boolean',
];
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function unit(): BelongsTo
{
return $this->belongsTo(Unit::class);
}
public function saleDetails(): HasMany
{
return $this->hasMany(SaleDetail::class);
}
public function purchaseDetails(): HasMany
{
return $this->hasMany(PurchaseDetail::class);
}
public function stockMovements(): HasMany
{
return $this->hasMany(StockMovement::class);
}
}
Model Product merupakan salah satu model utama pada aplikasi.
Relasi category
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
Digunakan untuk mengambil kategori produk.
Contoh:
$product = Product::first();
echo $product->category?->name;
Kita menggunakan:
?->
karena category_id pada migration dibuat nullable.
Jika produk tidak mempunyai kategori, kode tidak menghasilkan error.
Relasi unit
public function unit(): BelongsTo
{
return $this->belongsTo(Unit::class);
}
Digunakan untuk mengambil satuan produk.
Contoh:
echo $product->unit?->symbol;
Relasi saleDetails
public function saleDetails(): HasMany
{
return $this->hasMany(SaleDetail::class);
}
Satu produk dapat muncul pada banyak detail penjualan.
Relasi purchaseDetails
public function purchaseDetails(): HasMany
{
return $this->hasMany(PurchaseDetail::class);
}
Satu produk dapat muncul pada banyak detail pembelian.
Relasi stockMovements
public function stockMovements(): HasMany
{
return $this->hasMany(StockMovement::class);
}
Digunakan untuk melihat histori perubahan stok produk.
Contoh:
$product->stockMovements;
9. Catatan Tentang Kolom Stock pada $fillable
Pada model di atas stock tetap dimasukkan ke $fillable karena akan digunakan oleh proses internal aplikasi.
Namun pada CRUD produk nanti, kita tidak akan memberikan kebebasan kepada pengguna untuk mengubah stok secara langsung melalui form edit produk.
Perubahan stok dilakukan melalui proses:
pembelian
penjualan
penyesuaian stok
pembatalan transaksi
Tujuannya agar perubahan stok tetap mempunyai histori pada:
stock_movements
Jadi:
$fillable
tidak berarti setiap atribut harus ditampilkan sebagai input pada form CRUD.
10. Model Customer
Buka:
app/Models/Customer.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Customer extends Model
{
protected $fillable = [
'name',
'phone',
'address',
];
public function sales(): HasMany
{
return $this->hasMany(Sale::class);
}
}
Satu pelanggan dapat mempunyai banyak transaksi penjualan.
Contoh:
$customer = Customer::find(1);
$sales = $customer->sales;
11. Model Supplier
Buka:
app/Models/Supplier.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Supplier extends Model
{
protected $fillable = [
'name',
'phone',
'address',
];
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
}
Satu supplier dapat mempunyai banyak transaksi pembelian.
12. Model Sale
Buka:
app/Models/Sale.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Sale extends Model
{
protected $fillable = [
'invoice_number',
'user_id',
'customer_id',
'sale_date',
'subtotal',
'discount',
'grand_total',
'paid_amount',
'change_amount',
'status',
'notes',
];
protected function casts(): array
{
return [
'sale_date' => 'datetime',
'subtotal' => 'decimal:2',
'discount' => 'decimal:2',
'grand_total' => 'decimal:2',
'paid_amount' => 'decimal:2',
'change_amount' => 'decimal:2',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function details(): HasMany
{
return $this->hasMany(SaleDetail::class);
}
}
Relasi user
Setiap penjualan dibuat oleh satu pengguna.
$sale->user
dapat digunakan untuk mengambil kasir yang melakukan transaksi.
Relasi customer
Pelanggan bersifat opsional.
Karena itu gunakan:
$sale->customer?->name
ketika ingin menampilkan nama pelanggan.
Relasi details
Satu transaksi mempunyai banyak item:
$sale->details
Setiap item merupakan record pada:
sale_details
13. Model SaleDetail
Buka:
app/Models/SaleDetail.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SaleDetail extends Model
{
protected $fillable = [
'sale_id',
'product_id',
'quantity',
'price',
'discount',
'subtotal',
];
protected function casts(): array
{
return [
'quantity' => 'integer',
'price' => 'decimal:2',
'discount' => 'decimal:2',
'subtotal' => 'decimal:2',
];
}
public function sale(): BelongsTo
{
return $this->belongsTo(Sale::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
Pada file awal tutorial, sale_id dapat tidak terlihat pada $fillable apabila detail selalu dibuat menggunakan relasi:
$sale->details()->create(...)
Pada versi lengkap ini sale_id tetap dimasukkan agar model lebih fleksibel ketika dibutuhkan pada pengujian atau proses internal.
Walaupun demikian, pada transaksi penjualan nanti kita tetap akan membuat detail melalui relasi milik Sale.
14. Mengapa Harga Disimpan pada SaleDetail?
Model Product mempunyai:
selling_price
Namun SaleDetail juga mempunyai:
price
Hal ini memang disengaja.
Misalnya:
1 September 2026:
Air Mineral = Rp4.000
Kemudian harga berubah:
1 Oktober 2026:
Air Mineral = Rp5.000
Transaksi pada 1 September harus tetap menunjukkan:
Rp4.000
Karena itu harga pada saat transaksi disimpan di:
sale_details.price
Harga tersebut merupakan snapshot historis.
15. Model Purchase
Buka:
app/Models/Purchase.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Purchase extends Model
{
protected $fillable = [
'invoice_number',
'supplier_id',
'user_id',
'purchase_date',
'grand_total',
'status',
];
protected function casts(): array
{
return [
'purchase_date' => 'datetime',
'grand_total' => 'decimal:2',
];
}
public function supplier(): BelongsTo
{
return $this->belongsTo(Supplier::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function details(): HasMany
{
return $this->hasMany(PurchaseDetail::class);
}
}
Relasinya:
Purchase belongsTo Supplier
Purchase belongsTo User
Purchase hasMany PurchaseDetail
Supplier bersifat opsional berdasarkan migration sebelumnya.
Ketika menampilkan supplier:
$purchase->supplier?->name
16. Model PurchaseDetail
Buka:
app/Models/PurchaseDetail.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PurchaseDetail extends Model
{
protected $fillable = [
'purchase_id',
'product_id',
'quantity',
'price',
'subtotal',
];
protected function casts(): array
{
return [
'quantity' => 'integer',
'price' => 'decimal:2',
'subtotal' => 'decimal:2',
];
}
public function purchase(): BelongsTo
{
return $this->belongsTo(Purchase::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
Harga pembelian juga disimpan pada detail transaksi agar histori harga beli tidak berubah ketika harga produk diperbarui.
17. Model StockMovement
Buka:
app/Models/StockMovement.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class StockMovement extends Model
{
protected $fillable = [
'product_id',
'type',
'quantity_in',
'quantity_out',
'reference_type',
'reference_id',
'notes',
];
protected function casts(): array
{
return [
'quantity_in' => 'integer',
'quantity_out' => 'integer',
'reference_id' => 'integer',
];
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
Model ini digunakan untuk mencatat perubahan stok.
Contoh:
Pembelian 20 pcs
dapat menghasilkan:
type = purchase
quantity_in = 20
quantity_out = 0
Penjualan 2 pcs:
type = sale
quantity_in = 0
quantity_out = 2
18. Mengapa StockMovement Memiliki product_id pada $fillable?
Migration mempunyai:
product_id
Pada versi singkat sebelumnya atribut ini belum dimasukkan ke $fillable.
Pada versi detail kita memasukkannya karena model dapat digunakan melalui dua pola.
Pola pertama:
$product->stockMovements()->create([
'type' => 'sale',
// ...
]);
Pada pola ini product_id akan diisi oleh relasi.
Pola kedua:
StockMovement::create([
'product_id' => $product->id,
'type' => 'sale',
// ...
]);
Pola kedua membutuhkan product_id tersedia pada $fillable.
19. Model Setting
Pada versi ringkas sebelumnya model Setting belum dibahas secara lengkap.
Buka:
app/Models/Setting.php
Isi:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = [
'key',
'value',
];
}
Tabel settings menggunakan format:
key
value
Contoh:
store_name -> Toko RumahSourceCode
store_address -> Denpasar
store_phone -> 081234567890
receipt_footer -> Terima kasih telah berbelanja
Nantinya pengaturan tersebut dapat digunakan pada nota.
20. Melengkapi Model User
Buka:
app/Models/User.php
Pada Tahap 01 model ini sudah diubah agar mempunyai:
role
is_active
Sekarang tambahkan relasi transaksi.
Gunakan isi berikut:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'role',
'is_active',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
];
}
public function sales(): HasMany
{
return $this->hasMany(Sale::class);
}
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
}
Relasinya:
User hasMany Sales
User hasMany Purchases
Artinya satu pengguna dapat membuat banyak transaksi.
21. Tentang Cast Password hashed
Model User menggunakan:
'password' => 'hashed',
Dengan cast tersebut, password yang diberikan kepada model akan disimpan dalam bentuk hash apabila diperlukan.
Walaupun demikian, pada seeder kita juga dapat menggunakan Hash::make() secara eksplisit agar alur bagi pemula lebih mudah terlihat.
Yang penting password tidak disimpan sebagai plain text.
22. Ringkasan Relasi Model
Setelah seluruh file selesai, relasi kita adalah:
Category
└── hasMany Products
Unit
└── hasMany Products
Product
├── belongsTo Category
├── belongsTo Unit
├── hasMany SaleDetails
├── hasMany PurchaseDetails
└── hasMany StockMovements
Customer
└── hasMany Sales
Supplier
└── hasMany Purchases
User
├── hasMany Sales
└── hasMany Purchases
Sale
├── belongsTo User
├── belongsTo Customer
└── hasMany SaleDetails
SaleDetail
├── belongsTo Sale
└── belongsTo Product
Purchase
├── belongsTo User
├── belongsTo Supplier
└── hasMany PurchaseDetails
PurchaseDetail
├── belongsTo Purchase
└── belongsTo Product
StockMovement
└── belongsTo Product
23. Menguji Model Sebelum Membuat Seeder
Sebelum membuat data awal, pastikan tidak ada syntax error.
Jalankan:
php artisan about
Jika aplikasi dapat melakukan boot tanpa error, lanjutkan.
Kita juga dapat menjalankan:
php artisan tinker
Kemudian:
App\Models\Product::count();
Karena database masih kosong, hasil dapat berupa:
0
Hal yang penting adalah tidak muncul error class atau database.
Keluar:
exit
24. Membuat Seeder
Buat seeder:
php artisan make:seeder KasirSeeder
Laravel akan membuat:
database/seeders/KasirSeeder.php
Seeder digunakan untuk membuat data awal secara otomatis.
Ini lebih baik daripada memasukkan seluruh data contoh secara manual melalui phpMyAdmin.
25. Data Apa Saja yang Akan Diisi Seeder?
Kita akan membuat:
2 pengguna
3 kategori
4 satuan
3 produk contoh
3 stock movement stok awal
4 pengaturan toko
Akun:
Administrator
Kasir
Kategori:
Minuman
Makanan
Sembako
Satuan:
Pieces
Botol
Kotak
Kilogram
Produk contoh:
Air Mineral
Kopi Sachet
Mie Instan
26. Mengapa Stok Awal Harus Mempunyai Stock Movement?
Pada versi awal tutorial, produk contoh dapat dibuat langsung dengan:
'stock' => 20,
Hal tersebut memang membuat saldo stok menjadi 20.
Namun Tahap 02 sudah menetapkan bahwa perubahan stok sebaiknya dapat ditelusuri.
Karena itu pada versi lengkap ini stok awal tetap disimpan pada:
products.stock
dan kita membuat histori yang sesuai pada:
stock_movements
Contoh:
Air Mineral
products.stock = 20
mempunyai:
stock_movements
type = initial
quantity_in = 20
Dengan demikian saldo awal dan histori tetap konsisten.
27. Isi Lengkap KasirSeeder
Buka:
database/seeders/KasirSeeder.php
Ganti seluruh isinya menjadi:
<?php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Product;
use App\Models\Setting;
use App\Models\StockMovement;
use App\Models\Unit;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class KasirSeeder extends Seeder
{
public function run(): void
{
DB::transaction(function () {
$admin = User::updateOrCreate(
[
'email' => 'admin@example.com',
],
[
'name' => 'Administrator',
'password' => Hash::make('admin12345'),
'role' => 'admin',
'is_active' => true,
]
);
$cashier = User::updateOrCreate(
[
'email' => 'kasir@example.com',
],
[
'name' => 'Kasir',
'password' => Hash::make('kasir12345'),
'role' => 'cashier',
'is_active' => true,
]
);
$minuman = Category::firstOrCreate([
'name' => 'Minuman',
]);
$makanan = Category::firstOrCreate([
'name' => 'Makanan',
]);
$sembako = Category::firstOrCreate([
'name' => 'Sembako',
]);
$pcs = Unit::firstOrCreate(
[
'name' => 'Pieces',
],
[
'symbol' => 'pcs',
]
);
$botol = Unit::firstOrCreate(
[
'name' => 'Botol',
],
[
'symbol' => 'btl',
]
);
$kotak = Unit::firstOrCreate(
[
'name' => 'Kotak',
],
[
'symbol' => 'box',
]
);
$kg = Unit::firstOrCreate(
[
'name' => 'Kilogram',
],
[
'symbol' => 'kg',
]
);
$airMineral = Product::updateOrCreate(
[
'sku' => 'BRG-001',
],
[
'category_id' => $minuman->id,
'unit_id' => $botol->id,
'name' => 'Air Mineral',
'purchase_price' => 2500,
'selling_price' => 4000,
'stock' => 20,
'minimum_stock' => 5,
'is_active' => true,
]
);
$kopi = Product::updateOrCreate(
[
'sku' => 'BRG-002',
],
[
'category_id' => $minuman->id,
'unit_id' => $pcs->id,
'name' => 'Kopi Sachet',
'purchase_price' => 1500,
'selling_price' => 2500,
'stock' => 30,
'minimum_stock' => 10,
'is_active' => true,
]
);
$mie = Product::updateOrCreate(
[
'sku' => 'BRG-003',
],
[
'category_id' => $makanan->id,
'unit_id' => $pcs->id,
'name' => 'Mie Instan',
'purchase_price' => 2500,
'selling_price' => 3500,
'stock' => 25,
'minimum_stock' => 10,
'is_active' => true,
]
);
$this->syncInitialStock($airMineral);
$this->syncInitialStock($kopi);
$this->syncInitialStock($mie);
$settings = [
'store_name' => 'Toko RumahSourceCode',
'store_address' => 'Denpasar, Bali',
'store_phone' => '081234567890',
'receipt_footer' => 'Terima kasih telah berbelanja.',
];
foreach ($settings as $key => $value) {
Setting::updateOrCreate(
[
'key' => $key,
],
[
'value' => $value,
]
);
}
});
}
private function syncInitialStock(Product $product): void
{
StockMovement::updateOrCreate(
[
'product_id' => $product->id,
'type' => 'initial',
'reference_type' => 'seeder',
'reference_id' => $product->id,
],
[
'quantity_in' => $product->stock,
'quantity_out' => 0,
'notes' => 'Stok awal dari KasirSeeder',
]
);
}
}
28. Penjelasan DB::transaction pada Seeder
Seeder dibungkus:
DB::transaction(function () {
// ...
});
Artinya data awal dianggap sebagai satu proses.
Jika terjadi error di tengah proses, perubahan database pada transaksi tersebut dapat dibatalkan.
Konsep transaksi database ini akan digunakan lagi pada proses:
penjualan
pembelian
pembatalan transaksi
Pada transaksi kasir nanti penggunaan database transaction menjadi lebih penting karena satu proses menyentuh beberapa tabel sekaligus.
29. Penjelasan updateOrCreate untuk User
Kode:
User::updateOrCreate(
[
'email' => 'admin@example.com',
],
[
// ...
]
);
berarti:
- jika email belum ada, buat user baru;
- jika email sudah ada, perbarui datanya.
Dengan cara ini seeder lebih aman dijalankan berulang kali dibanding menggunakan create() tanpa pengecekan.
30. Akun Administrator
Akun admin:
Email : admin@example.com
Password : admin12345
Role : admin
Akun ini sama dengan akun contoh pada Tahap 01.
Admin nantinya dapat mengakses fitur manajemen sistem.
31. Akun Kasir
Akun kasir:
Email : kasir@example.com
Password : kasir12345
Role : cashier
Kasir nantinya digunakan untuk transaksi penjualan.
Pada aplikasi produksi, ganti akun contoh dan password tersebut.
Jangan menggunakan password tutorial pada website produksi.
32. Penjelasan firstOrCreate untuk Category
Kode:
$minuman = Category::firstOrCreate([
'name' => 'Minuman',
]);
akan mencari kategori:
Minuman
Jika belum ada, Laravel membuatnya.
Jika sudah ada, record yang sama digunakan.
Dengan demikian menjalankan seeder lagi tidak membuat kategori duplikat.
33. Penjelasan firstOrCreate untuk Unit
Untuk satuan kita menggunakan:
Unit::firstOrCreate(
[
'name' => 'Pieces',
],
[
'symbol' => 'pcs',
]
);
Parameter pertama digunakan sebagai kondisi pencarian.
Parameter kedua digunakan sebagai nilai tambahan ketika record dibuat.
34. Penjelasan updateOrCreate untuk Product
Produk menggunakan:
sku
sebagai identitas pencarian.
Contoh:
Product::updateOrCreate(
[
'sku' => 'BRG-001',
],
[
// data produk
]
);
Jika BRG-001 sudah ada, data produk diperbarui.
Jika belum ada, produk dibuat.
35. Fungsi syncInitialStock
Fungsi:
private function syncInitialStock(Product $product): void
digunakan agar setiap produk contoh mempunyai record stok awal.
Contoh hasil:
product_id = 1
type = initial
quantity_in = 20
quantity_out = 0
reference_type = seeder
reference_id = 1
notes = Stok awal dari KasirSeeder
updateOrCreate() digunakan agar menjalankan seeder kembali tidak membuat histori stok awal berkali-kali.
36. Catatan Penting Tentang Seeder dan Stok
Seeder ini ditujukan untuk:
database pengembangan
database tutorial
database pengujian
Bukan untuk memperbarui stok pada database produksi yang sudah memiliki transaksi nyata.
Jika aplikasi sudah dipakai, jangan menjalankan seeder produk yang dapat mengubah products.stock tanpa memahami akibatnya.
Untuk tahap tutorial, database masih berada dalam fase pengembangan sehingga data contoh tersebut aman digunakan.
37. Seeder Setting
Bagian:
$settings = [
'store_name' => 'Toko RumahSourceCode',
'store_address' => 'Denpasar, Bali',
'store_phone' => '081234567890',
'receipt_footer' => 'Terima kasih telah berbelanja.',
];
digunakan untuk membuat konfigurasi awal.
Loop:
foreach ($settings as $key => $value) {
Setting::updateOrCreate(
[
'key' => $key,
],
[
'value' => $value,
]
);
}
membuat atau memperbarui masing-masing setting.
Nantinya kita dapat mengambil nama toko:
Setting::where('key', 'store_name')->value('value');
38. Mendaftarkan KasirSeeder pada DatabaseSeeder
Buka:
database/seeders/DatabaseSeeder.php
Isi:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
KasirSeeder::class,
]);
}
}
Dengan demikian ketika menjalankan:
php artisan db:seed
Laravel akan menjalankan:
KasirSeeder
39. Menjalankan Seeder
Jalankan:
php artisan db:seed
Jika berhasil, terminal akan menampilkan proses seeding tanpa error.
Karena kita menggunakan:
firstOrCreate
updateOrCreate
seeder dapat dijalankan kembali tanpa membuat sebagian besar data duplikat.
40. Menjalankan Migration dan Seeder Sekaligus
Jika database masih tahap pengembangan dan seluruh datanya boleh dihapus, kita dapat menjalankan:
php artisan migrate:fresh --seed
Perintah ini:
- menghapus seluruh tabel;
- menjalankan semua migration;
- menjalankan
DatabaseSeeder.
Jangan gunakan perintah ini pada database produksi yang sudah mempunyai data penting.
41. Memeriksa User Hasil Seeder
Jalankan:
php artisan tinker
Kemudian:
App\Models\User::select('name', 'email', 'role', 'is_active')->get();
Hasil kurang lebih:
Administrator
admin@example.com
admin
true
dan:
Kasir
kasir@example.com
cashier
true
42. Memeriksa Category
Di Tinker:
App\Models\Category::all();
Pastikan terdapat:
Minuman
Makanan
Sembako
43. Memeriksa Unit
Jalankan:
App\Models\Unit::all();
Pastikan terdapat:
Pieces
Botol
Kotak
Kilogram
44. Memeriksa Product
Jalankan:
App\Models\Product::select(
'sku',
'name',
'purchase_price',
'selling_price',
'stock',
'minimum_stock',
'is_active'
)->get();
Pastikan produk contoh tersedia.
45. Menguji Relasi Product ke Category
Jalankan:
$product = App\Models\Product::where('sku', 'BRG-001')->first();
Kemudian:
$product->category?->name;
Hasil:
Minuman
46. Menguji Relasi Product ke Unit
Jalankan:
$product->unit?->name;
Hasil:
Botol
Kemudian:
$product->unit?->symbol;
Hasil:
btl
47. Menguji Relasi Category ke Products
Jalankan:
$category = App\Models\Category::where('name', 'Minuman')->first();
Kemudian:
$category->products;
Hasil harus berisi minimal:
Air Mineral
Kopi Sachet
48. Menguji Stock Movement
Jalankan:
$product->stockMovements;
Untuk Air Mineral harus terdapat catatan stok awal.
Kita juga dapat menjalankan:
$product->stockMovements()
->select('type', 'quantity_in', 'quantity_out', 'notes')
->get();
Hasil kurang lebih:
type = initial
quantity_in = 20
quantity_out = 0
49. Memastikan Saldo Produk dan Histori Sama
Untuk produk yang baru dibuat oleh seeder:
$product->stock;
hasil:
20
Jumlah stok masuk dikurangi stok keluar:
$stockFromMovements = $product->stockMovements()->sum('quantity_in')
- $product->stockMovements()->sum('quantity_out');
Kemudian:
$stockFromMovements;
hasil:
20
Artinya saldo produk dan histori stok awal konsisten.
50. Menguji Setting
Jalankan:
App\Models\Setting::where('key', 'store_name')->value('value');
Hasil:
Toko RumahSourceCode
Cek footer:
App\Models\Setting::where('key', 'receipt_footer')->value('value');
Hasil:
Terima kasih telah berbelanja.
51. Keluar dari Tinker
Setelah selesai:
exit
52. Eager Loading untuk Menghindari Query Berulang
Relasi dapat diakses langsung:
Product::all();
lalu di Blade:
$product->category?->name
Namun ketika menampilkan banyak produk, lebih baik menggunakan eager loading:
Product::with(['category', 'unit'])->get();
Dengan demikian Laravel mengambil relasi yang dibutuhkan lebih efisien.
Contoh untuk daftar penjualan:
Sale::with(['user', 'customer', 'details.product'])->get();
Contoh pembelian:
Purchase::with(['supplier', 'user', 'details.product'])->get();
Teknik ini akan sering digunakan pada controller berikutnya.
53. Contoh Mengambil Produk dengan Relasi
Jalankan di Tinker:
$products = App\Models\Product::with(['category', 'unit'])->get();
Kemudian:
foreach ($products as $product) {
echo $product->name
.' | '
.($product->category?->name ?? '-')
.' | '
.($product->unit?->symbol ?? '-')
.PHP_EOL;
}
Contoh hasil:
Air Mineral | Minuman | btl
Kopi Sachet | Minuman | pcs
Mie Instan | Makanan | pcs
54. Contoh Query Produk Aktif
Karena model Product mempunyai:
is_active
kita dapat mengambil produk aktif:
App\Models\Product::where('is_active', true)->get();
Produk tidak aktif tidak perlu dihapus.
Ini penting karena produk yang sudah mempunyai histori transaksi sebaiknya tetap tersimpan.
55. Contoh Query Produk Stok Menipis
Untuk menampilkan produk dengan stok kurang dari atau sama dengan batas minimum:
App\Models\Product::whereColumn('stock', '<=', 'minimum_stock')->get();
Query ini akan digunakan pada dashboard nantinya.
56. Contoh Mengakses Harga yang Sudah Dicasting
Jalankan:
$product = App\Models\Product::first();
$product->selling_price;
Karena menggunakan:
'selling_price' => 'decimal:2',
nilai direpresentasikan dengan dua angka desimal sesuai cast Eloquent.
Untuk tampilan rupiah nanti kita tetap akan memformatnya pada view.
Contoh:
number_format((float) $product->selling_price, 0, ',', '.');
57. Relasi Belum Memiliki Data Transaksi
Pada tahap ini:
$user->sales
$product->saleDetails
$supplier->purchases
kemungkinan masih kosong.
Itu normal.
Kita baru membuat data master.
Data transaksi akan dibuat setelah bagian transaksi penjualan dan pembelian selesai.
Yang perlu dipastikan sekarang adalah relasi dapat dipanggil tanpa error.
58. Menguji Relasi User
Masuk Tinker:
php artisan tinker
Kemudian:
$user = App\Models\User::where('email', 'admin@example.com')->first();
Coba:
$user->sales;
Hasil kemungkinan:
Collection kosong
Coba:
$user->purchases;
Hasil juga dapat kosong.
Ini benar karena belum ada transaksi.
59. Menguji Relasi Supplier
Karena seeder belum membuat supplier, kita dapat membuat data sementara dari Tinker:
$supplier = App\Models\Supplier::create([
'name' => 'Supplier Demo',
'phone' => '081234567890',
'address' => 'Denpasar',
]);
Cek:
$supplier->purchases;
Hasil dapat berupa collection kosong.
Setelah pengujian, data ini boleh dibiarkan atau dihapus selama belum mempunyai transaksi:
$supplier->delete();
60. Menguji Relasi Customer
Buat sementara:
$customer = App\Models\Customer::create([
'name' => 'Pelanggan Demo',
'phone' => '081234567891',
'address' => 'Denpasar',
]);
Cek:
$customer->sales;
Kemudian hapus jika tidak diperlukan:
$customer->delete();
61. Apa yang Tidak Perlu Dibuat pada Tahap Ini?
Pada tahap model dan seeder kita belum membuat:
controller CRUD
form kategori
form satuan
form produk
form pelanggan
form supplier
halaman kasir
transaksi penjualan
transaksi pembelian
dashboard
laporan
nota
Semua itu akan dibuat pada tahap berikutnya.
Tujuan Tahap 03 adalah memastikan lapisan data aplikasi sudah rapi.
62. Struktur Folder Setelah Tahap 03
Bagian penting project sekarang:
app/
└── Models/
├── Category.php
├── Customer.php
├── Product.php
├── Purchase.php
├── PurchaseDetail.php
├── Sale.php
├── SaleDetail.php
├── Setting.php
├── StockMovement.php
├── Supplier.php
├── Unit.php
└── User.php
database/
├── migrations/
│ └── ...
└── seeders/
├── DatabaseSeeder.php
└── KasirSeeder.php
63. Kesalahan Umum: Class Tidak Ditemukan
Jika muncul:
Class "App\Models\Product" not found
pastikan file:
app/Models/Product.php
mempunyai namespace:
namespace App\Models;
dan nama class:
class Product extends Model
Jika masih bermasalah:
composer dump-autoload
64. Kesalahan Umum: MassAssignmentException
Jika muncul error:
MassAssignmentException
periksa apakah atribut yang dikirim melalui:
Model::create(...)
atau:
Model::updateOrCreate(...)
sudah ada pada:
$fillable
Contoh Product harus mengizinkan:
sku
name
category_id
unit_id
purchase_price
selling_price
stock
minimum_stock
is_active
65. Kesalahan Umum: Call to Undefined Relationship
Jika muncul error karena relasi tidak ditemukan, periksa nama method.
Contoh benar:
$product->category
karena method pada model adalah:
public function category()
Jika method diberi nama:
categories()
maka pemanggilannya berbeda.
Gunakan nama relasi seperti pada tutorial agar seluruh bagian berikutnya konsisten.
66. Kesalahan Umum: category_id Bernilai NULL
Jika:
$product->category
menghasilkan:
null
periksa:
$product->category_id;
Kemudian pastikan record kategori dengan ID tersebut masih tersedia.
Pada migration, jika kategori dihapus:
category_id
memang akan menjadi:
NULL
karena menggunakan:
nullOnDelete()
67. Kesalahan Umum: Seeder Membuat Data Ganda
Jika menggunakan:
Category::create(...)
setiap seeding dapat membuat data yang sama.
Pada tutorial kita menggunakan:
firstOrCreate
updateOrCreate
untuk data awal yang harus tetap unik.
Tetap perhatikan bahwa seeder ditujukan untuk data pengembangan.
68. Kesalahan Umum: Password Tidak Bisa Digunakan
Jika login gagal setelah seeding, periksa password seeder.
Akun admin:
admin@example.com
admin12345
Akun kasir:
kasir@example.com
kasir12345
Pastikan seeder sudah dijalankan:
php artisan db:seed
Periksa user:
php artisan tinker
Kemudian:
App\Models\User::where('email', 'admin@example.com')->first();
69. Kesalahan Umum: Stok Awal Tidak Sesuai Histori
Periksa:
$product = App\Models\Product::where('sku', 'BRG-001')->first();
Saldo:
$product->stock;
Histori:
$product->stockMovements()->sum('quantity_in')
- $product->stockMovements()->sum('quantity_out');
Pada kondisi setelah seeder pertama kali, keduanya harus sama.
70. Kesalahan Umum: Menggunakan migrate:fresh --seed pada Data Penting
Perintah:
php artisan migrate:fresh --seed
sangat berguna saat belajar.
Namun perintah tersebut menghapus seluruh tabel.
Jangan gunakan pada database produksi yang telah mempunyai transaksi.
71. Checklist Pengujian Tahap 03
Sebelum melanjutkan ke CRUD, lakukan pengujian berikut.
Pengujian 1 — Model dapat digunakan
Jalankan:
php artisan tinker
Kemudian:
App\Models\Product::count();
Tidak boleh muncul error.
Pengujian 2 — Seeder berhasil
Pastikan:
php artisan db:seed
berjalan tanpa error.
Pengujian 3 — Akun admin tersedia
App\Models\User::where('email', 'admin@example.com')->exists();
Harus:
true
Pengujian 4 — Akun kasir tersedia
App\Models\User::where('email', 'kasir@example.com')->exists();
Harus:
true
Pengujian 5 — Produk contoh tersedia
App\Models\Product::where('sku', 'BRG-001')->exists();
Harus:
true
Pengujian 6 — Relasi kategori berjalan
$product = App\Models\Product::where('sku', 'BRG-001')->first();
$product->category?->name;
Harus:
Minuman
Pengujian 7 — Relasi satuan berjalan
$product->unit?->symbol;
Harus:
btl
Pengujian 8 — Stok awal mempunyai histori
$product->stockMovements()->exists();
Harus:
true
Pengujian 9 — Setting tersedia
App\Models\Setting::where('key', 'store_name')->exists();
Harus:
true
Pengujian 10 — Seeder dapat dijalankan kembali
Keluar dari Tinker:
exit
Kemudian jalankan lagi:
php artisan db:seed
Tidak boleh muncul error unique untuk:
email
category
unit
sku
setting key
72. Checkpoint Hasil
Jika seluruh tahap berhasil, kondisi aplikasi sekarang adalah:
Login : selesai
Role admin/kasir : selesai
Middleware role : selesai
Struktur database : selesai
Model Eloquent : selesai
Relasi Eloquent : selesai
Casting atribut : selesai
Seeder user : selesai
Seeder kategori/satuan : selesai
Seeder produk : selesai
Histori stok awal : selesai
Seeder settings : selesai
CRUD master data : belum
CRUD produk : belum
Transaksi penjualan : belum
Pembelian dan stok : belum
Dashboard dan laporan : belum
Tahap ini menjadi fondasi untuk controller dan halaman CRUD.
73. Ringkasan File yang Diubah
File yang kita lengkapi:
app/Models/Category.php
app/Models/Unit.php
app/Models/Product.php
app/Models/Customer.php
app/Models/Supplier.php
app/Models/Purchase.php
app/Models/PurchaseDetail.php
app/Models/Sale.php
app/Models/SaleDetail.php
app/Models/StockMovement.php
app/Models/Setting.php
app/Models/User.php
database/seeders/KasirSeeder.php
database/seeders/DatabaseSeeder.php
74. Ringkasan Relasi Penting
Produk:
Product -> Category
Product -> Unit
Product -> SaleDetail
Product -> PurchaseDetail
Product -> StockMovement
Penjualan:
Sale -> User
Sale -> Customer
Sale -> SaleDetails
Pembelian:
Purchase -> User
Purchase -> Supplier
Purchase -> PurchaseDetails
75. Ringkasan
Pada tahap ketiga kita telah menghubungkan struktur database dengan Eloquent Model.
Kita telah membuat dan melengkapi:
User
Category
Unit
Product
Customer
Supplier
Sale
SaleDetail
Purchase
PurchaseDetail
StockMovement
Setting
Kita juga sudah mengatur:
$fillable
casts
belongsTo
hasMany
Seeder telah menyiapkan akun:
admin@example.com
kasir@example.com
serta kategori, satuan, produk contoh, histori stok awal, dan pengaturan toko.
Kita juga sudah menguji relasi melalui Tinker.
Pada tahap berikutnya kita akan mulai membuat antarmuka pengelolaan data melalui CRUD master data.
Bagian berikutnya akan fokus pada kategori, satuan, pelanggan, dan supplier. Produk akan dibahas lebih mendalam pada bagian khusus agar tutorial tidak terlalu padat dan lebih mudah diikuti pemula.





Komentar
Tulis Komentar