Berikut langkah singkatnya:
- Buat project
composer create-project laravel/laravel crud-laravel-test
2. Buat Model, Controller dan Request
php artisan make:controller UserController --resource --model=User
php artisan make:request StoreUserRequest
php artisan make:request UpdateUserRequest
php artisan make:model Hobby -m
php artisan make:migration add_profile_fields_to_users_table
php artisan make:migration create_hobby_user_table
3. Tambahkan kolom pada table user
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('jenis_kelamin', ['L', 'P'])->nullable();
$table->string('agama', 30)->nullable();
$table->date('tanggal_lahir')->nullable();
$table->text('alamat')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'jenis_kelamin',
'agama',
'tanggal_lahir',
'alamat',
]);
});
}
};
4. Tmbahkan kolom pada table pivot hobbies dan hobby_user
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('hobby_user', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('hobby_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('hobby_user');
}
};
6. Migrate
php artisan migrate
7. Tambahkan pada model User
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'jenis_kelamin',
'agama',
'tanggal_lahir',
'alamat',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'tanggal_lahir' => 'date',
'password' => 'hashed',
];
}
public function hobbies()
{
return $this->belongsToMany(Hobby::class);
}
}
8. Tambahkan pada model Hobbies
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Hobby extends Model
{
protected $fillable = [
'nama',
];
public function users()
{
return $this->belongsToMany(User::class);
}
}
9. Isi data seeder Hobbies
php artisan make:seeder HobbySeeder
<?php
namespace Database\Seeders;
use App\Models\Hobby;
use Illuminate\Database\Seeder;
class HobbySeeder extends Seeder
{
public function run(): void
{
$hobbies = [
'Membaca',
'Coding',
'Gaming',
'Musik',
'Olahraga',
'Traveling',
'Memasak',
];
foreach ($hobbies as $hobby) {
Hobby::firstOrCreate([
'nama' => $hobby,
]);
}
}
}
php artisan db:seed --class=HobbySeeder
10. Buat validasi untuk storeuser
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
],
'email' => [
'required',
'email',
'max:255',
'unique:users,email',
],
'password' => [
'required',
'string',
'min:8',
'confirmed',
],
'jenis_kelamin' => [
'required',
Rule::in(['L', 'P']),
],
'agama' => [
'required',
Rule::in([
'Islam',
'Kristen',
'Katolik',
'Hindu',
'Buddha',
'Konghucu',
'Lainnya',
]),
],
'tanggal_lahir' => [
'required',
'date',
'before:today',
],
'alamat' => [
'required',
'string',
'max:1000',
],
'hobbies' => [
'nullable',
'array',
],
'hobbies.*' => [
'integer',
'exists:hobbies,id',
],
];
}
public function messages(): array
{
return [
'name.required' => 'Nama wajib diisi.',
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
'email.unique' => 'Email sudah digunakan.',
'password.required' => 'Password wajib diisi.',
'password.min' => 'Password minimal 8 karakter.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
'jenis_kelamin.required' => 'Jenis kelamin wajib dipilih.',
'agama.required' => 'Agama wajib dipilih.',
'tanggal_lahir.required' => 'Tanggal lahir wajib diisi.',
'tanggal_lahir.before' => 'Tanggal lahir tidak valid.',
'alamat.required' => 'Alamat wajib diisi.',
'hobbies.*.exists' => 'Hobby yang dipilih tidak valid.',
];
}
}
11. Validasi untuk update user
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
],
'email' => [
'required',
'email',
'max:255',
Rule::unique('users', 'email')
->ignore($this->user),
],
'password' => [
'nullable',
'string',
'min:8',
'confirmed',
],
'jenis_kelamin' => [
'required',
Rule::in(['L', 'P']),
],
'agama' => [
'required',
Rule::in([
'Islam',
'Kristen',
'Katolik',
'Hindu',
'Buddha',
'Konghucu',
'Lainnya',
]),
],
'tanggal_lahir' => [
'required',
'date',
'before:today',
],
'alamat' => [
'required',
'string',
'max:1000',
],
'hobbies' => [
'nullable',
'array',
],
'hobbies.*' => [
'integer',
'exists:hobbies,id',
],
];
}
}
12. Route Users langsung menggunakan Resource
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;
Route::resource('users', UserController::class);
13. Membuat UserController
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\Hobby;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Throwable;
class UserController extends Controller
{
public function index(): View
{
$users = User::query()
->with('hobbies')
->latest()
->paginate(10);
return view('users.index', compact('users'));
}
public function create(): View
{
$hobbies = Hobby::query()
->orderBy('nama')
->get();
return view('users.create', compact('hobbies'));
}
public function store(StoreUserRequest $request): RedirectResponse
{
try {
DB::transaction(function () use ($request) {
$data = $request->validated();
/*
|--------------------------------------------------------------------------
| Pisahkan data relasi dari data user
|--------------------------------------------------------------------------
*/
$hobbies = $data['hobbies'] ?? [];
unset($data['hobbies']);
/*
|--------------------------------------------------------------------------
| Create User
|--------------------------------------------------------------------------
*/
$user = User::create($data);
/*
|--------------------------------------------------------------------------
| Simpan hobbies
|--------------------------------------------------------------------------
*/
$user->hobbies()->sync($hobbies);
});
return redirect()
->route('users.index')
->with('success', 'User berhasil ditambahkan.');
} catch (Throwable $e) {
Log::error('Gagal membuat user', [
'message' => $e->getMessage(),
'exception' => $e,
]);
return back()
->withInput()
->with('error', 'Terjadi kesalahan saat menyimpan user.');
}
}
public function edit(User $user): View
{
$user->load('hobbies');
$hobbies = Hobby::query()
->orderBy('nama')
->get();
return view('users.edit', compact(
'user',
'hobbies'
));
}
public function update(
UpdateUserRequest $request,
User $user
): RedirectResponse {
try {
DB::transaction(function () use ($request, $user) {
$data = $request->validated();
$hobbies = $data['hobbies'] ?? [];
unset($data['hobbies']);
/*
|--------------------------------------------------------------------------
| Jangan update password kalau kosong
|--------------------------------------------------------------------------
*/
if (empty($data['password'])) {
unset($data['password']);
}
$user->update($data);
$user->hobbies()->sync($hobbies);
});
return redirect()
->route('users.index')
->with('success', 'User berhasil diperbarui.');
} catch (Throwable $e) {
Log::error('Gagal memperbarui user', [
'user_id' => $user->id,
'message' => $e->getMessage(),
'exception' => $e,
]);
return back()
->withInput()
->with('error', 'Terjadi kesalahan saat memperbarui user.');
}
}
public function destroy(User $user): RedirectResponse
{
try {
DB::transaction(function () use ($user) {
$user->hobbies()->detach();
$user->delete();
});
return redirect()
->route('users.index')
->with('success', 'User berhasil dihapus.');
} catch (Throwable $e) {
Log::error('Gagal menghapus user', [
'user_id' => $user->id,
'message' => $e->getMessage(),
'exception' => $e,
]);
return back()
->with('error', 'User gagal dihapus.');
}
}
}
14. Tambahkan tampilan untuk Partial _form supaya create dan edit tidak membuat HTML yang sama dua kali
<div class="row g-3">
{{-- Nama --}}
<div class="col-md-6">
<label class="form-label">
Nama
</label>
<input
type="text"
name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $user->name ?? '') }}"
>
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
{{-- Email --}}
<div class="col-md-6">
<label class="form-label">
Email
</label>
<input
type="email"
name="email"
class="form-control @error('email') is-invalid @enderror"
value="{{ old('email', $user->email ?? '') }}"
>
@error('email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
{{-- Jenis Kelamin --}}
<div class="col-md-6">
<label class="form-label d-block">
Jenis Kelamin
</label>
@php
$gender = old(
'jenis_kelamin',
$user->jenis_kelamin ?? ''
);
@endphp
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="jenis_kelamin"
id="gender-l"
value="L"
@checked($gender === 'L')
>
<label
class="form-check-label"
for="gender-l"
>
Laki-laki
</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="jenis_kelamin"
id="gender-p"
value="P"
@checked($gender === 'P')
>
<label
class="form-check-label"
for="gender-p"
>
Perempuan
</label>
</div>
@error('jenis_kelamin')
<div class="text-danger small mt-1">
{{ $message }}
</div>
@enderror
</div>
{{-- Agama --}}
<div class="col-md-6">
<label class="form-label">
Agama
</label>
@php
$selectedAgama = old(
'agama',
$user->agama ?? ''
);
$agamaList = [
'Islam',
'Kristen',
'Katolik',
'Hindu',
'Buddha',
'Konghucu',
'Lainnya',
];
@endphp
<select
name="agama"
class="form-select @error('agama') is-invalid @enderror"
>
<option value="">
-- Pilih Agama --
</option>
@foreach($agamaList as $agama)
<option
value="{{ $agama }}"
@selected($selectedAgama === $agama)
>
{{ $agama }}
</option>
@endforeach
</select>
@error('agama')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
{{-- Tanggal Lahir --}}
<div class="col-md-6">
<label class="form-label">
Tanggal Lahir
</label>
<input
type="date"
name="tanggal_lahir"
class="form-control @error('tanggal_lahir') is-invalid @enderror"
value="{{ old(
'tanggal_lahir',
isset($user) && $user->tanggal_lahir
? $user->tanggal_lahir->format('Y-m-d')
: ''
) }}"
>
@error('tanggal_lahir')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
{{-- Password --}}
<div class="col-md-6">
<label class="form-label">
Password
</label>
<input
type="password"
name="password"
class="form-control @error('password') is-invalid @enderror"
>
@isset($user)
<small class="text-muted">
Kosongkan jika tidak ingin mengganti password.
</small>
@endisset
@error('password')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
{{-- Password Confirmation --}}
<div class="col-md-6">
<label class="form-label">
Konfirmasi Password
</label>
<input
type="password"
name="password_confirmation"
class="form-control"
>
</div>
{{-- Hobby --}}
<div class="col-12">
<label class="form-label d-block">
Hobby
</label>
@php
$selectedHobbies = old(
'hobbies',
isset($user)
? $user->hobbies->pluck('id')->toArray()
: []
);
@endphp
<div id="hobby-container">
@foreach($hobbies as $hobby)
<div class="form-check form-check-inline">
<input
type="checkbox"
name="hobbies[]"
value="{{ $hobby->id }}"
id="hobby-{{ $hobby->id }}"
class="form-check-input hobby-checkbox"
@checked(
in_array(
$hobby->id,
$selectedHobbies
)
)
>
<label
class="form-check-label"
for="hobby-{{ $hobby->id }}"
>
{{ $hobby->nama }}
</label>
</div>
@endforeach
</div>
@error('hobbies')
<div class="text-danger small">
{{ $message }}
</div>
@enderror
@error('hobbies.*')
<div class="text-danger small">
{{ $message }}
</div>
@enderror
</div>
{{-- Alamat --}}
<div class="col-12">
<label class="form-label">
Alamat
</label>
<textarea
name="alamat"
rows="4"
class="form-control @error('alamat') is-invalid @enderror"
>{{ old('alamat', $user->alamat ?? '') }}</textarea>
@error('alamat')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
15. Membuat halaman view Create User
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="card shadow-sm">
<div class="card-header">
Tambah User
</div>
<div class="card-body">
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form
action="{{ route('users.store') }}"
method="POST"
id="userForm"
novalidate
>
@csrf
@include('users._form')
<div class="mt-4">
<button
type="submit"
class="btn btn-primary"
>
Simpan
</button>
<a
href="{{ route('users.index') }}"
class="btn btn-secondary"
>
Kembali
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
16. Membuat view edit
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="card shadow-sm">
<div class="card-header">
Edit User
</div>
<div class="card-body">
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form
action="{{ route('users.update', $user) }}"
method="POST"
id="userForm"
novalidate
>
@csrf
@method('PUT')
@include('users._form')
<div class="mt-4">
<button
type="submit"
class="btn btn-primary"
>
Update
</button>
<a
href="{{ route('users.index') }}"
class="btn btn-secondary"
>
Kembali
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
17. Menambahkan validasi Jquery pada layouts > app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', config('app.name') . ' — SaaS CMS Platform')</title>
<meta name="description" content="@yield('meta_description', 'Platform CMS multi-tenant modern untuk membuat dan mengelola website bisnis Anda dengan mudah, cepat, dan profesional.')">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-validation/dist/jquery.validate.min.js"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
</style>
</head>
<body class="landing-page">
<main>
@yield('content')
</main>
<script>
$(function () {
$('#userForm').validate({
rules: {
name: {
required: true,
minlength: 3,
maxlength: 255
},
email: {
required: true,
email: true
},
jenis_kelamin: {
required: true
},
agama: {
required: true
},
tanggal_lahir: {
required: true,
date: true
},
alamat: {
required: true,
minlength: 5,
maxlength: 1000
},
@if(!isset($user))
password: {
required: true,
minlength: 8
},
password_confirmation: {
required: true,
equalTo: '[name="password"]'
}
@else
password: {
minlength: 8
},
password_confirmation: {
equalTo: '[name="password"]'
}
@endif
},
messages: {
name: {
required: 'Nama wajib diisi.',
minlength: 'Nama minimal 3 karakter.'
},
email: {
required: 'Email wajib diisi.',
email: 'Format email tidak valid.'
},
jenis_kelamin: {
required: 'Pilih jenis kelamin.'
},
agama: {
required: 'Pilih agama.'
},
tanggal_lahir: {
required: 'Tanggal lahir wajib diisi.'
},
alamat: {
required: 'Alamat wajib diisi.'
},
password: {
required: 'Password wajib diisi.',
minlength: 'Password minimal 8 karakter.'
},
password_confirmation: {
required: 'Konfirmasi password wajib diisi.',
equalTo: 'Konfirmasi password tidak sama.'
}
},
errorElement: 'div',
errorClass: 'invalid-feedback',
highlight: function(element) {
$(element)
.addClass('is-invalid')
.removeClass('is-valid');
},
unhighlight: function(element) {
$(element)
.removeClass('is-invalid');
},
errorPlacement: function(error, element) {
if (
element.attr('type') === 'radio'
) {
error.insertAfter(
element.closest('.col-md-6')
.find('.form-check')
.last()
);
}
else if (
element.attr('type') === 'checkbox'
) {
error.insertAfter(
element.closest('#hobby-container')
);
}
else {
error.insertAfter(element);
}
}
});
});
</script>
</body>
</html>
18. Menambahkan view untuk table pada Index
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between mb-3">
<h4>
Data User
</h4>
<a
href="{{ route('users.create') }}"
class="btn btn-primary"
>
+ Tambah User
</a>
</div>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead>
<tr>
<th>Nama</th>
<th>Email</th>
<th>Gender</th>
<th>Agama</th>
<th>Tanggal Lahir</th>
<th>Hobby</th>
<th width="180">Action</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>
{{ $user->name }}
</td>
<td>
{{ $user->email }}
</td>
<td>
{{ $user->jenis_kelamin }}
</td>
<td>
{{ $user->agama }}
</td>
<td>
{{ $user->tanggal_lahir?->format('d-m-Y') }}
</td>
<td>
@foreach($user->hobbies as $hobby)
<span class="badge bg-secondary">
{{ $hobby->nama }}
</span>
@endforeach
</td>
<td>
<a
href="{{ route('users.edit', $user) }}"
class="btn btn-sm btn-warning"
>
Edit
</a>
<form
action="{{ route('users.destroy', $user) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Hapus user ini?')"
>
@csrf
@method('DELETE')
<button
class="btn btn-sm btn-danger"
>
Hapus
</button>
</form>
</td>
</tr>
@empty
<tr>
<td
colspan="7"
class="text-center text-muted"
>
Belum ada user.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="mt-3">
{{ $users->links() }}
</div>
</div>
@endsection
19. View tabel dengan modal
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between mb-3">
<h4>
Data User
</h4>
<a
href="{{ route('users.create') }}"
class="btn btn-primary"
>
+ Tambah User
</a>
</div>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead>
<tr>
<th>Nama</th>
<th>Email</th>
<th>Gender</th>
<th width="220">Action</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>
{{ $user->jenis_kelamin === 'L'
? 'Laki-laki'
: 'Perempuan'
}}
</td>
<td>
<button
type="button"
class="btn btn-sm btn-info"
data-bs-toggle="modal"
data-bs-target="#detailUserModal{{ $user->id }}"
>
Detail
</button>
<a
href="{{ route('users.edit', $user) }}"
class="btn btn-sm btn-warning"
>
Edit
</a>
<form
action="{{ route('users.destroy', $user) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Hapus user ini?')"
>
@csrf
@method('DELETE')
<button
type="submit"
class="btn btn-sm btn-danger"
>
Hapus
</button>
</form>
</td>
</tr>
{{-- Modal --}}
<div
class="modal fade"
id="detailUserModal{{ $user->id }}"
tabindex="-1"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title mb-1">
{{ $user->name }}
</h5>
<small class="text-muted">
{{ $user->email }}
</small>
</div>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-5 fw-semibold">
Agama
</div>
<div class="col-7">
{{ $user->agama ?? '-' }}
</div>
</div>
<div class="row mb-3">
<div class="col-5 fw-semibold">
Tanggal Lahir
</div>
<div class="col-7">
{{ $user->tanggal_lahir
? $user->tanggal_lahir->format('d-m-Y')
: '-'
}}
</div>
</div>
<div class="row">
<div class="col-5 fw-semibold">
Hobi
</div>
<div class="col-7">
@forelse($user->hobbies as $hobby)
<span class="badge bg-primary me-1 mb-1">
{{ $hobby->nama }}
</span>
@empty
<span class="text-muted">
Tidak ada hobi
</span>
@endforelse
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Tutup
</button>
</div>
</div>
</div>
</div>
@empty
<tr>
<td colspan="4" class="text-center text-muted">
Belum ada user.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="mt-3">
{{ $users->links() }}
</div>
</div>
@endsection