Dokumentasi CRUD Produk Laravel
Dokumentasi CRUD Produk Laravel
Dokumentasi ini menjelaskan tahapan membuat fitur CRUD Produk dari proses migrasi database hingga tampilan halaman view detail produk di Laravel.
untuk materi sebelumnya bisa lihat di :
Cara Menginstall Laravel 12 dengan MudahAplikasi yang Diperlukan
| Aplikasi | Keterangan |
|---|---|
| PHP (v8.1 atau lebih tinggi) | Bahasa pemrograman backend utama Laravel |
| Composer | Dependency manager untuk PHP |
| Laravel (v12) | Framework utama aplikasi |
| MySQL / MariaDB | Database sistem penyimpanan |
| Node.js & npm | Untuk frontend (compile asset: Tailwind, Vite, dsb) |
| Visual Studio Code / PHPStorm | Code editor |
| Git (opsional) | Untuk version control dan kolaborasi |
1. Migrasi Database
a. Membuat Migration
php artisan make:migration create_products_table php artisan make:migration create_customers_table php artisan make:migration create_orders_table php artisan make:migration create_order_details_table
b. Isi File Migration
products
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description');
$table->decimal('price', 10, 2);
$table->integer('stock');
$table->string('image')->nullable();
$table->timestamps();
});
customers
Schema::create('customers', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 255);
$table->string('email', 255)->unique();
$table->text('address')->nullable();
$table->timestamp('created_at')->nullable();
$table->timestamp('updated_at')->nullable();
});
orders
Schema::create('orders', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('customer_id');
$table->date('order_date');
$table->decimal('total_amount', 10, 2)->default(0.00);
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
$table->timestamp('created_at')->nullable();
$table->timestamp('updated_at')->nullable();
// Foreign Key
$table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade');
});
order_details
Schema::create('order_details', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('order_id');
$table->unsignedBigInteger('product_id');
$table->unsignedInteger('quantity')->default(1);
$table->decimal('unit_price', 10, 2);
$table->decimal('subtotal', 10, 2);
$table->timestamp('created_at')->nullable();
$table->timestamp('updated_at')->nullable();
// Foreign Keys
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
});
c. Jalankan Migration
php artisan migrate
2. Model
a. Membuat Model
php artisan make:model Products
b. isi model
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Products extends Model
{
use HasFactory;
protected $table = 'products'; // Sesuaikan dengan nama tabel di database
protected $fillable = [
'name',
'slug',
'category_id',
'price',
'stock',
'description',
'image',
'image_url',
];
}
3. Controller
a. Membuat Controller
php artisan make:controller ProductController --resource
b. isi Controller
$products]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$categories = Categories::all(); // pastikan data ini dikirim
return view('dashboard.products.create', compact('categories'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'category_id' => 'required|numeric',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'description' => 'nullable|string',
'image' => 'nullable|url', // Validasi URL
]);
// Buat slug dari nama produk
$validated['slug'] = Str::slug($validated['name']);
// Simpan URL gambar jika ada dan pastikan gambar dapat diakses
if ($request->filled('image')) {
$imageUrl = $request->input('image');
$headers = @get_headers($imageUrl);
if (strpos($headers[0], '200') === false) {
return back()->withErrors(['image' => 'The image URL is not accessible.'])->withInput();
}
$validated['image'] = $imageUrl;
} else {
$validated['image'] = null; // Jika tidak ada URL gambar
}
// Simpan produk
Products::create($validated);
// Cek kategori apakah ada
if (!Categories::where('id', $validated['category_id'])->exists()) {
return back()->withErrors(['category_id' => 'Category does not exist.'])->withInput();
}
return redirect()->route('products.index')->with('success', 'Product created successfully!');
}
/**
* Display the specified resource.
*/
public function show(Products $product)
{
return view('dashboard.products.show', compact('product'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
// Cari produk berdasarkan ID, jika tidak ditemukan akan lempar 404
$product = Products::findOrFail($id);
// Ambil semua kategori untuk dropdown select
$categories = Categories::all();
// Kembalikan view dengan data product dan categories
return view('dashboard.products.update', [
'product' => $product,
'categories' => $categories,
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Products $product)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'slug' => 'nullable|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'category_id' => 'required|numeric',
'image' => 'nullable|string',
]);
if (empty($validated['slug'])) {
$validated['slug'] = Str::slug($validated['name']);
}
// Cek kategori apakah ada
if (!Categories::where('id', $validated['category_id'])->exists()) {
return back()->withErrors(['category_id' => 'Category does not exist.'])->withInput();
}
$product->update($validated);
return redirect()->route('products.index')->with('success', 'Product updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Products $product)
{
// Hapus file gambar jika ada
if ($product->image && file_exists(public_path('storage/' . $product->image))) {
unlink(public_path('storage/' . $product->image));
}
$product->delete();
return redirect()->route('products.index')->with('success', 'Product deleted successfully.');
}
}
4. Route
a. Tambahkan di routes/web.php
Route::resource('dashboard/products',ProductController::class);
5. View Blade
a. Index Page (resources/views/dashboard/products/index.blade.php)
<x-layouts.app :title="__('Products')">
<div class="container mt-4">
<h1 class="text-center text-5xl text-bold">PRODUCTS</h1>
<div class="flex items-center justify-between mt-4">
{{-- Tombol Tambah --}}
<flux:button as="a" href="{{ route('products.create') }}" variant="filled" color="green">
Add New Product
</flux:button>
{{-- Refresh --}}
<div class="ml-auto flex items-center space-x-2">
<a href="{{ route('products.index') }}"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm rounded-md">
Refresh
</a>
</div>
</div>
<div class="overflow-x-auto text-white shadow rounded-lg mt-4 p-4">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">No.</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Image</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Slug</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Price</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Stock</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
@foreach ($products as $key => $product)
<tr class="hover:bg-gray-800">
<td class="px-6 py-4 text-sm">{{ $key + 1 }}</td>
<td class="px-6 py-4">
@if ($product->image)
<img src="{{ $product->image }}" alt="Image"
class="w-16 h-16 object-cover rounded-md border border-gray-700">
@else
<span class="text-gray-400 text-sm italic">No Image</span>
@endif
</td>
<td class="px-6 py-4 text-sm">{{ $product->name }}</td>
<td class="px-6 py-4 text-sm">{{ $product->slug }}</td>
<td class="px-6 py-4 text-sm">{{ $product->description }}</td>
<td class="px-6 py-4 text-sm">{{ $product->price }}</td>
<td class="px-6 py-4 text-sm">{{ $product->stock }}</td>
<td class="px-6 py-4 text-sm">
<div class="flex flex-wrap gap-2">
<a href="{{ route('products.show', $product->id) }}"
class="px-3 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded">
View
</a>
<a href="{{ route('products.edit', $product->id) }}"
class="px-3 py-1 text-xs font-medium text-black bg-yellow-400 hover:bg-yellow-500 rounded">
Edit
</a>
<form action="{{ route('products.destroy', $product->id) }}" method="POST"
onsubmit="return confirm('Are you sure want to delete this product?')">
@csrf
@method('DELETE')
<button type="submit"
class="px-3 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded">
Delete
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</x-layouts.app>
b. Show Page (resources/views/dashboard/products/show.blade.php)
<x-layouts.app :title="'Product Detail: ' . $product->name"> <div class="container mt-8 text-white max-w-3xl mx-auto"> <h1 class="text-3xl font-bold mb-6">Product Detail</h1> <div class="bg-gray-800 p-6 rounded-lg shadow-md"> <div class="mb-4"> <strong>Name:</strong> <p>{{ $product->name }}</p> </div> <div class="mb-4"> <strong>Slug:</strong> <p>{{ $product->slug }}</p> </div> <div class="mb-4"> <strong>Description:</strong> <p>{{ $product->description }}</p> </div> <div class="mb-4"> <strong>Price:</strong> <p>Rp. {{ number_format($product->price, 2) }}</p> </div> <div class="mb-4"> <strong>Stock:</strong> <p>{{ $product->stock }}</p> </div> <div class="mb-4"> <strong>Image:</strong><br> @if ($product->image) <img src="{{ $product->image }}" alt="Product Image" class="w-48 h-48 object-cover mt-2 rounded"> @else <p class="italic text-gray-400">No image available</p> @endif </div> <div class="mt-6"> <a href="{{ route('products.index') }}" class="inline-block px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded">Back</a> </div> </div> </div> </x-layouts.app>c. Create Page (resources/views/dashboard/products/create.blade.php)
<x-layouts.app> <div class="mx-auto p-4"> <!-- Header --> <div class="flex justify-between items-center mb-4"> <h1 class="text-2xl font-semibold text-white">Add New Product</h1> <a href="{{ route('products.index') }}" class="bg-black text-white font-bold py-2 px-4 rounded hover:bg-gray-800"> Back to List </a> </div> <!-- Form --> <div class="bg-gray-800 rounded-lg shadow p-6"> <form action="{{ route('products.store') }}" method="POST"> @csrf <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <!-- Name --> <div> <label for="name" class="block text-sm font-medium text-gray-300">Name</label> <input type="text" name="name" id="name" value="{{ old('name') }}" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> @error('name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> <!-- Category --> <div> <label for="category_id" class="block text-sm font-medium text-gray-300">Category</label> <select name="category_id" id="category_id" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> <option value="">Select Category</option> @foreach ($categories as $category) <option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}> {{ $category->name }} </option> @endforeach </select> @error('category_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> <!-- Price --> <div> <label for="price" class="block text-sm font-medium text-gray-300">Price</label> <input type="number" name="price" id="price" value="{{ old('price') }}" step="0.01" min="0" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> @error('price') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> <!-- Stock --> <div> <label for="stock" class="block text-sm font-medium text-gray-300">Stock</label> <input type="number" name="stock" id="stock" value="{{ old('stock', 0) }}" min="0" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> @error('stock') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> <!-- Description --> <div class="col-span-1 md:col-span-2"> <label for="description" class="block text-sm font-medium text-gray-300">Description</label> <textarea name="description" id="description" rows="4" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500">{{ old('description') }}</textarea> @error('description') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> <!-- Image URL --> <div class="col-span-1 md:col-span-2"> <label for="image" class="block text-sm font-medium text-gray-300">Image URL</label> <input type="text" name="image" id="image" value="{{ old('image') }}" placeholder="https://example.com/image.jpg" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> @error('image') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror </div> </div> <!-- Submit Button --> <div class="mt-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-black hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> Create Product </button> </div> </form> </div> </div> </x-layouts.app>d. Update Page (resources/views/dashboard/products/update.blade.php)
<x-layouts.app> <div class="mx-auto p-4"> <div class="flex justify-between items-center mb-4"> <h1 class="text-2xl font-semibold text-white">Edit Product</h1> <a href="/products" class="bg-black text-white font-bold py-2 px-4 rounded hover:bg-gray-800"> Back to List </a> </div> <div class="bg-gray-800 rounded-lg shadow p-6"> <form action="/products/update/1" method="POST"> <!-- Simulate CSRF token and method override in static HTML --> <input type="hidden" name="_token" value="csrf_token_here"> <input type="hidden" name="_method" value="PUT"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <!-- Name --> <div> <label for="name" class="block text-sm font-medium text-gray-300">Name</label> <input type="text" name="name" id="name" value="Sample Product" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> </div> <!-- Category --> <div> <label for="category_id" class="block text-sm font-medium text-gray-300">Category</label> <select name="category_id" id="category_id" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> <option value="" disabled>Select Category</option> <option value="1" selected>Electronics</option> <option value="2">Books</option> <option value="3">Clothing</option> </select> </div> <!-- Price --> <div> <label for="price" class="block text-sm font-medium text-gray-300">Price</label> <input type="number" name="price" id="price" value="199.99" step="0.01" min="0" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> </div> <!-- Stock --> <div> <label for="stock" class="block text-sm font-medium text-gray-300">Stock</label> <input type="number" name="stock" id="stock" value="50" min="0" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> </div> <!-- Description --> <div class="col-span-1 md:col-span-2"> <label for="description" class="block text-sm font-medium text-gray-300">Description</label> <textarea name="description" id="description" rows="4" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500">This is a sample product description.</textarea> </div> <!-- Image URL --> <div class="col-span-1 md:col-span-2"> <label for="image" class="block text-sm font-medium text-gray-300">Image URL</label> <input type="text" name="image" id="image" value="https://example.com/image.jpg" placeholder="https://example.com/image.jpg" class="mt-1 block w-full shadow-sm sm:text-sm border-gray-700 bg-gray-900 text-white rounded-md focus:ring-blue-500 focus:border-blue-500"> </div> </div> <!-- Submit Button --> <div class="mt-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-black hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> Update Product </button> </div> </form> </div> </div> </x-layouts.app>Selesai! Sekarang aplikasi Anda memiliki fitur lengkap CRUD untuk produk dengan desain rapi dan fungsional.
Komentar
Posting Komentar