Inventaris Lab Laravel 12 : Bagian #08 - MEMBUAT HALAMAN CRUD ITEM
Belajar Bareng Minarsih - Edisi Ngoding
10 Oktober 2025
Tujuan: Di bagian ini, kita akan membangun fitur CRUD yang lebih kompleks, yaitu untuk mengelola data barang/item. Kita akan menerapkan kembali teknik refactoring dan juga belajar cara menampilkan data yang berhubungan antar tabel (misalnya, menampilkan nama kategori pada setiap item).
Seperti biasa, kita siapkan "jalan" dan "otak" terlebih dahulu.
use App\Http\Controllers\ItemController;
Route::middleware('auth')->group(function () {
// ... route dashboard, users, category
Route::resource('item', ItemController::class);
});
php artisan route:list --name=item
Sekarang, kita isi logika pada controller untuk mengambil data semua item dari database.
use App\Models\Item;
class ItemController extends Controller
{
// Variabel untuk data umum
protected $title = 'Item';
protected $menu = 'item';
protected $directory = 'admin.item'; // Diubah ke folder view item
public function index()
{
// Menyiapkan array untuk dikirim ke view
$data['title'] = $this->title;
$data['menu'] = $this->menu;
// Mengambil data dari database
$data['items'] = Item::with('category')->latest()->get();
// Me-return view beserta data
return view($this->directory . '.index', $data);
}
Pe
Kita akan kembali menggunakan metode duplikasi untuk mempercepat pekerjaan.
Sekarang kita akan mengubah file index.blade.php di dalam folder item agar sesuai untuk menampilkan data barang.
@extends('admin.layouts.app')
@section('css')
{{-- CSS Tambahan --}}
@endsection
@section('content')
<div class="card">
<div class="card-body">
<h5 class="card-title fw-semibold mb-4">Data {{ $title }}</h5>
<a href="{{ route('category.create') }}" class="btn btn-primary mb-4">Tambah Data {{ $title }}</a>
<div class="table-responsive">
<table id="datatable" class="table table-striped">
<thead>
<th>No</th>
<th>Nama</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach ($categories as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->name }}</td>
<td>
<a href="{{ route('category.edit', $item->id) }}" class="btn btn-warning btn-sm">Ubah</a>
<form id="deleteForm{{ $item->id }}"
action="{{ route('category.destroy', $item->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="button" class="btn btn-danger btn-sm"
onclick="confirmDelete({{ $item->id }})">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
@section('js')
<script>
$(document).ready(function() {
$('#datatable').DataTable();
});
// Script untuk SweetAlert
function confirmDelete(id) {
swal({
title: "Apakah anda yakin?",
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((willDelete) => {
if (willDelete) {
// Jika pengguna menekan "OK", submit form
$('#deleteForm' + id).submit();
} else {
// Jika pengguna menekan "Cancel"
swal("Data tidak jadi dihapus!", {
icon: "error",
});
}
});
}
</script>
@endsection
@extends('admin.layouts.app')
@section('css')
{{-- CSS Tambahan --}}
@endsection
@section('content')
<div class="card">
<div class="card-body">
<h5 class="card-title fw-semibold mb-4">Data {{ $title }}</h5>
<a href="{{ route('item.create') }}" class="btn btn-primary mb-4">Tambah Data {{ $title }}</a>
<div class="table-responsive">
<table id="datatable" class="table table-striped">
<thead>
<tr>
<th>No</th>
<th>Nama</th>
<th>Foto</th>
<th>Kategori</th>
<th>Kode Unik</th>
<th>Kondisi</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->name }}</td>
<td><a href="{{ asset('photos/' . $item->photo) }}" target="_blank">Lihat Foto</a>
</td>
<td>{{ $item->category->name }}</td>
<td>{{ $item->unique_code }}</td>
<td>{{ $item->condition }}</td>
<td>
<a href="{{ route('item.edit', $item->id) }}" class="btn btn-warning btn-sm">Ubah</a>
<form id="deleteForm{{ $item->id }}"
action="{{ route('item.destroy', $item->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="button" class="btn btn-danger btn-sm"
onclick="confirmDelete({{ $item->id }})">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
@section('js')
<script>
$(document).ready(function() {
$('#datatable').DataTable();
});
// Script untuk SweetAlert
function confirmDelete(id) {
swal({
title: "Apakah anda yakin?",
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((willDelete) => {
if (willDelete) {
// Jika pengguna menekan "OK", submit form
$('#deleteForm' + id).submit();
} else {
// Jika pengguna menekan "Cancel"
swal("Data tidak jadi dihapus!", {
icon: "error",
});
}
});
}
</script>
@endsection
Agar halaman daftar item yang baru kita buat bisa diakses, kita perlu menambahkan link menunya di sidebar.
{{-- MENU DATAMASTER --}}
<li class="nav-small-cap">
<i class="ti ti-dots nav-small-cap-icon fs-4"></i>
<span class="hide-menu">Datamaster</span>
</li>
{{-- Menu Users --}}
<li class="sidebar-item {{ Request::is('users*') ? 'selected' : '' }}">
<a class="sidebar-link {{ Request::is('users*') ? 'active' : '' }}"
href="{{ route('users.index') }}" aria-expanded="false">
<span><i class="ti ti-users"></i></span>
<span class="hide-menu">Data Users</span>
</a>
</li>
{{-- Menu Category --}}
<li class="sidebar-item {{ Request::is('category*') ? 'selected' : '' }}">
<a class="sidebar-link {{ Request::is('category*') ? 'active' : '' }}"
href="{{ route('category.index') }}" aria-expanded="false">
<span><i class="ti ti-category"></i></span>
<span class="hide-menu">Data Category</span>
</a>
</li>
{{-- Menu Item --}}
<li class="sidebar-item {{ Request::is('item*') ? 'selected' : '' }}">
<a class="sidebar-link {{ Request::is('item*') ? 'active' : '' }}"
href="{{ route('item.index') }}" aria-expanded="false">
<span><i class="ti ti-box"></i></span>
<span class="hide-menu">Data Item</span>
</a>
</li>
Membuat form untuk item akan sedikit lebih kompleks karena melibatkan lebih banyak jenis input, termasuk dropdown untuk memilih kategori dan input untuk mengunggah foto.
use App\Models\Category;
public function create()
{
$data['title'] = $this->title;
$data['menu'] = $this->menu;
// Ambil semua data kategori untuk dikirim ke view
$data['categories'] = Category::all();
return view($this->directory . '.create', $data);
}
@extends('admin.layouts.app')
@section('css')
{{-- CSS Tambahan --}}
@endsection
@section('content')
<div class="card">
<div class="card-body">
<h5 class="card-title fw-semibold mb-4">Tambah Data {{ $title }}</h5>
<div class="card">
<div class="card-body">
<form action="{{ route('item.store') }}" method="POST" enctype="multipart/form-data">
@csrf
{{-- Nama Item --}}
<div class="mb-3">
<label for="name" class="form-label">Nama Item</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" name="name"
id="name" placeholder="Nama Item" value="{{ old('name') }}">
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kategori --}}
<div class="mb-3">
<label for="category_id" class="form-label">Kategori</label>
<select class="form-select @error('category_id') is-invalid @enderror" name="category_id" id="category_id">
<option value="" disabled selected>Pilih Kategori</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
@endforeach
</select>
@error('category_id')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Foto --}}
<div class="mb-3">
<label for="photo" class="form-label">Foto</label>
<input type="file" class="form-control @error('photo') is-invalid @enderror" name="photo" id="photo">
@error('photo')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kode Unik --}}
<div class="mb-3">
<label for="unique_code" class="form-label">Kode Unik</label>
<input type="text" class="form-control @error('unique_code') is-invalid @enderror" name="unique_code"
id="unique_code" placeholder="Contoh: MM-001" value="{{ old('unique_code') }}">
@error('unique_code')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kondisi --}}
<div class="mb-3">
<label for="condition" class="form-label">Kondisi</label>
<select class="form-select @error('condition') is-invalid @enderror" name="condition" id="condition">
<option value="Baik" {{ old('condition') == 'Baik' ? 'selected' : '' }}>Baik</option>
<option value="Rusak" {{ old('condition') == 'Rusak' ? 'selected' : '' }}>Rusak</option>
</select>
@error('condition')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Tombol --}}
<button type="submit" class="btn btn-primary">Simpan</button>
<a href="{{ route('item.index') }}" class="btn btn-warning">Kembali</a>
</form>
</div>
</div>
</div>
</div>
@endsection
@section('js')
{{-- JS Tambahan --}}
@endsection
Penjelasan Baru:
enctype="multipart/form-data": Atribut ini wajib ada pada form jika di dalamnya terdapat input untuk unggah file.
Logika penyimpanan item akan lebih kompleks karena kita perlu menangani unggahan file foto.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
protected $fillable = [
'category_id',
'name',
'photo',
'unique_code',
'condition',
];
// Mendefinisikan relasi "belongsTo" ke model Category
public function category()
{
return $this->belongsTo(Category::class);
}
}
public function store(Request $request)
{
// 1. Validasi data
$validatedData = $request->validate([
'name' => 'required|max:255',
'category_id' => 'required',
'photo' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'unique_code' => 'nullable|unique:items',
'condition' => 'required',
]);
// 2. Proses upload foto jika ada
if ($request->hasFile('photo')) {
$image = $request->file('photo');
// Buat nama file unik berdasarkan waktu
$imageName = time() . '.' . $image->getClientOriginalExtension();
// Pindahkan file ke folder public/photos
$image->move(public_path('photos'), $imageName);
// Simpan nama file ke dalam data yang akan divalidasi
$validatedData['photo'] = $imageName;
}
// 3. Simpan data ke database
$item = Item::create($validatedData);
// 4. Redirect dengan pesan sukses
if ($item) {
return redirect()->route('item.index')->with([
'status' => 'success',
'title' => 'Berhasil',
'message' => 'Data Berhasil Ditambahkan!'
]);
} else {
return redirect()->route('item.index')->with([
'status' => 'danger',
'title' => 'Gagal',
'message' => 'Data Gagal Ditambahkan!'
]);
}
}
Prosesnya akan sangat mirip dengan membuat form tambah, kita akan mengadaptasi kode yang sudah ada.
public function edit(Item $item)
{
// Menyiapkan data umum
$data['title'] = $this->title;
$data['menu'] = $this->menu;
// Mencari data item berdasarkan ID menggunakan Model Binding
$data['item'] = $item;
// Ambil semua data kategori untuk dikirim ke view
$data['categories'] = Category::all();
// Me-return view beserta data
return view($this->directory . '.edit', $data);
}
@extends('admin.layouts.app')
@section('css')
{{-- CSS Tambahan --}}
@endsection
@section('content')
<div class="card">
<div class="card-body">
<h5 class="card-title fw-semibold mb-4">Ubah Data {{ $title }}</h5>
<div class="card">
<div class="card-body">
<form action="{{ route('item.update', $item->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
{{-- Nama Item --}}
<div class="mb-3">
<label for="name" class="form-label">Nama Item</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" name="name"
id="name" placeholder="Nama Item" value="{{ old('name', $item->name) }}">
@error('name')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kategori --}}
<div class="mb-3">
<label for="category_id" class="form-label">Kategori</label>
<select class="form-select @error('category_id') is-invalid @enderror" name="category_id" id="category_id">
<option value="" disabled>Pilih Kategori</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id', $item->category_id) == $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
@endforeach
</select>
@error('category_id')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Foto --}}
<div class="mb-3">
<label for="photo" class="form-label">Ganti Foto (Opsional)</label>
@if ($item->photo)
<p>Foto saat ini: <img src="{{ asset('photos/' . $item->photo) }}" alt="Foto Item" width="100"></p>
@endif
<input type="file" class="form-control @error('photo') is-invalid @enderror" name="photo" id="photo">
@error('photo')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kode Unik --}}
<div class="mb-3">
<label for="unique_code" class="form-label">Kode Unik</label>
<input type="text" class="form-control @error('unique_code') is-invalid @enderror" name="unique_code"
id="unique_code" placeholder="Contoh: MM-001" value="{{ old('unique_code', $item->unique_code) }}">
@error('unique_code')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Kondisi --}}
<div class="mb-3">
<label for="condition" class="form-label">Kondisi</label>
<select class="form-select @error('condition') is-invalid @enderror" name="condition" id="condition">
<option value="Baik" {{ old('condition', $item->condition) == 'Baik' ? 'selected' : '' }}>Baik</option>
<option value="Rusak" {{ old('condition', $item->condition) == 'Rusak' ? 'selected' : '' }}>Rusak</option>
</select>
@error('condition')
<small class="text-danger">{{ $message }}</small>
@enderror
</div>
{{-- Tombol --}}
<button type="submit" class="btn btn-primary">Simpan</button>
<a href="{{ route('item.index') }}" class="btn btn-warning">Kembali</a>
</form>
</div>
</div>
</div>
</div>
@endsection
@section('js')
{{-- JS Tambahan --}}
@endsection
Logika update akan sedikit berbeda karena kita perlu memeriksa apakah ada file foto baru yang diunggah. Jika ada, kita hapus foto lama dan unggah yang baru.
use Illuminate\Support\Facades\File;
public function update(Request $request, Item $item)
{
// 1. Gunakan Route Model Binding, variabel $item sudah siap pakai.
$validatedData = $request->validate([
'name' => 'required|max:255',
'category_id' => 'required',
'photo' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'unique_code' => 'nullable|unique:items,unique_code,' . $item->id,
'condition' => 'required',
]);
// 2. Siapkan data dari hasil validasi.
$updateData = $validatedData;
// 3. Proses foto baru jika diupload: hapus yang lama, simpan yang baru.
if ($request->hasFile('photo')) {
// Hapus foto lama jika ada.
if ($item->photo && File::exists(public_path('photos/' . $item->photo))) {
File::delete(public_path('photos/' . $item->photo));
}
// Simpan foto baru dan tambahkan namanya ke data update.
$image = $request->file('photo');
$imageName = time() . '.' . $image->getClientOriginalExtension();
$image->move(public_path('photos'), $imageName);
$updateData['photo'] = $imageName;
}
// 4. Update data item di database.
$updateProcess = $item->update($updateData);
// 5. Redirect dengan pesan status berdasarkan hasil update.
if ($updateProcess) {
return redirect()->route('item.index')->with([
'status' => 'success',
'title' => 'Berhasil',
'message' => 'Data Berhasil Diubah!'
]);
} else {
return redirect()->route('item.index')->with([
'status' => 'danger',
'title' => 'Gagal',
'message' => 'Data Gagal Diubah!'
]);
}
}
Ini adalah langkah terakhir untuk melengkapi CRUD Item. Logikanya akan sedikit berbeda karena kita juga harus menghapus file foto yang tersimpan di server.
public function destroy(Item $item)
{
// 1. Hapus file foto dari folder public jika ada.
if ($item->photo && File::exists(public_path('photos/' . $item->photo))) {
File::delete(public_path('photos/' . $item->photo));
}
// 2. Hapus data item dari database.
$deleteProcess = $item->delete();
// 3. Redirect dengan pesan status berdasarkan hasil proses hapus.
if ($deleteProcess) {
return redirect()->route('item.index')->with([
'status' => 'success',
'title' => 'Berhasil',
'message' => 'Data Berhasil Dihapus!'
]);
} else {
return redirect()->route('item.index')->with([
'status' => 'danger',
'title' => 'Gagal',
'message' => 'Data Gagal Dihapus!'
]);
}
}
Penjelasan Kode:
Saatnya memastikan semua bagian (Tombol -> SweetAlert -> Controller -> Hapus File -> Hapus Data -> Redirect) bekerja dengan sempurna.
Selamat! Anda telah berhasil membangun fungsionalitas CRUD yang lebih kompleks untuk halaman Item, termasuk menangani unggahan dan penghapusan file. Ini adalah fondasi yang sangat kuat untuk membangun fitur-fitur lain di masa depan.
Artikel Lainnya Dengan Kategori Terkait :
1. Inventaris Lab Laravel 12 : Bagian #01 - INSTALASI LARAGON, PHP, PHPMYADMIN & LARAVEL
2. Inventaris Lab Laravel 12 : Bagian #02 - KONFIGURASI DATABASE & FONDASI PROYEK
3. Inventaris Lab Laravel 12 : Bagian #03 - MEMBANGUN HALAMAN ADMIN DENGAN BLADE TEMPLATING
4. Inventaris Lab Laravel 12 : Bagian #04 - MEMBUAT HALAMAN DASHBOARD DINAMIS
5. Inventaris Lab Laravel 12 : Bagian #05 - MEMBUAT HALAMAN CRUD USERS
6. Inventaris Lab Laravel 12 : Bagian #06 - MEMBUAT FITUR AUTENTIKASI (LOGIN) & PENYESUAIAN UI
7. Inventaris Lab Laravel 12 : Bagian #07 - MEMBUAT HALAMAN CRUD CATEGORY
8. Inventaris Lab Laravel 12 : Bagian #08 - MEMBUAT HALAMAN CRUD ITEM
9. Inventaris Lab Laravel 12 : Bagian #09 - MEMBUAT HALAMAN TRANSAKSI PEMINJAMAN (LOAN)
10. Inventaris Lab Laravel 12 : Bagian #10 - HAK AKSES (AUTHORIZATION) & HALAMAN SISWA
Mahardika Oktadiansyah - 15 Juli 2025
Belajar CSS Lanjutan #395 | CSS padding-inline Property
Mahardika Oktadiansyah - 15 Juli 2025
Belajar CSS Lanjutan #394 | CSS padding-bottom Property
Mahardika Oktadiansyah - 15 Juli 2025
Belajar CSS Lanjutan #393 | CSS padding-block-start Property