Ini adalah website personal Selamat M. Harjono
Rabu, 17 Desember 2025 | oleh Selamat Muliyadi Harjono | Materi
Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)
A. Opening & Review Fase 1 (30 menit)
B. Filosofi Framework Laravel (30 menit)
Laravel Framework ├── Eloquent ORM ├── Blade Templating ├── Artisan CLI ├── Migration System ├── Authentication └── Queue, Cache, etc.
// NATIVE PHP session_start(); $conn = new mysqli(...); $sql = "SELECT * FROM posts"; $result = $conn->query($sql); // LARAVEL $posts = Post::all();
C. MVC Architecture Flow (45 menit)
Flow Lengkap Laravel MVC:
text
HTTP Request
→ routes/web.php
→ Controller
→ Model (Database)
→ View (Blade)
→ HTTP ResponseKomponen MVC:
Contoh Implementasi Blog:
php
// Route
Route::get('/posts', [PostController::class, 'index']);
// Controller
class PostController extends Controller {
public function index() {
$posts = Post::where('status', 'published')->get();
return view('posts.index', compact('posts'));
}
}
// View: resources/views/posts/index.blade.php
@foreach($posts as $post)
<h2>{{ $post->title }}</h2>
@endforeachD. Instalasi Laravel & Project Setup (60 menit - PRAKTIKUM)
Langkah 1: Instalasi via Composer
bash
# Pastikan composer sudah terinstall composer --version # Install Laravel global (opsional) composer global require laravel/installer # Buat project baru laravel new blog-portfolio # atau composer create-project laravel/laravel blog-portfolio
Langkah 2: Environment Configuration
bash
cd blog-portfolio # Copy environment file cp .env.example .env # Generate app key php artisan key:generate # Konfigurasi database di .env DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=blog_portfolio DB_USERNAME=root DB_PASSWORD=
Langkah 3: Struktur Folder Overview
text
blog-portfolio/ ├── app/ │ ├── Console/ │ ├── Exceptions/ │ ├── Http/ │ │ ├── Controllers/ │ │ ├── Middleware/ │ │ └── Kernel.php │ ├── Models/ # Semua model disini │ └── Providers/ ├── bootstrap/ ├── config/ # Semua konfigurasi ├── database/ │ ├── factories/ │ ├── migrations/ # Skema database │ ├── seeders/ # Data dummy │ └── seeders/ ├── public/ # Web root ├── resources/ │ ├── css/ │ ├── js/ │ └── views/ # Blade templates ├── routes/ # web.php, api.php ├── storage/ ├── tests/ └── vendor/
Langkah 4: Test Server
bash
# Jalankan development server php artisan serve # Buka http://localhost:8000 # Harus tampil welcome page
E. Routing System (45 menit)
Dasar Routing:
php
// routes/web.php
// Basic route
Route::get('/', function () {
return 'Welcome to Blog';
});
// Route dengan parameter
Route::get('/posts/{id}', function ($id) {
return "Post ID: $id";
});
// Route dengan optional parameter
Route::get('/posts/{id?}', function ($id = 1) {
return "Post ID: $id";
});
// Route dengan constraint
Route::get('/posts/{id}', function ($id) {
return "Post ID: $id";
})->where('id', '[0-9]+');
// Named route
Route::get('/posts/create', function () {
return view('posts.create');
})->name('posts.create');
// Menggunakan named route di view
// <a href="{{ route('posts.create') }}">Create Post</a>Route Groups & Middleware:
php
// Group dengan prefix
Route::prefix('admin')->group(function () {
Route::get('/dashboard', function () {
return 'Admin Dashboard';
});
Route::get('/users', function () {
return 'Manage Users';
});
});
// Group dengan middleware
Route::middleware(['auth'])->group(function () {
Route::get('/profile', function () {
return 'User Profile';
});
});Route Resource (CRUD):
php
// Generate semua route CRUD untuk Post
Route::resource('posts', PostController::class);
// Hasilnya:
// GET /posts → index
// GET /posts/create → create
// POST /posts → store
// GET /posts/{post} → show
// GET /posts/{post}/edit → edit
// PUT/PATCH /posts/{post} → update
// DELETE /posts/{post} → destroy
F. Controller (45 menit)
Membuat Controller:
bash
# Buat controller dengan Artisan php artisan make:controller PostController # Buat resource controller (CRUD lengkap) php artisan make:controller PostController --resource # Buat controller dengan model sekaligus php artisan make:controller PostController --resource --model=Post
Struktur Controller:
php
<?php
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
// Ambil semua post yang published
$posts = Post::where('status', 'published')
->orderBy('published_at', 'desc')
->paginate(10);
return view('posts.index', compact('posts'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('posts.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
// Validasi
$validated = $request->validate([
'title' => 'required|max:200',
'content' => 'required',
'category_id' => 'nullable|exists:categories,id'
]);
// Simpan ke database
$post = Post::create($validated);
return redirect()->route('posts.show', $post)
->with('success', 'Post created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Post $post)
{
// Increment view count
$post->increment('view_count');
return view('posts.show', compact('post'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|max:200',
'content' => 'required',
'status' => 'required|in:draft,published,archived'
]);
$post->update($validated);
return redirect()->route('posts.show', $post)
->with('success', 'Post updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index')
->with('success', 'Post deleted successfully.');
}
}Route Model Binding:
php
// Implicit binding (otomatis)
Route::get('/posts/{post}', function (Post $post) {
return view('posts.show', compact('post'));
});
// Custom binding
Route::get('/posts/{post:slug}', function (Post $post) {
return view('posts.show', compact('post'));
});
// Multiple parameters
Route::get('/categories/{category}/posts/{post:slug}',
function (Category $category, Post $post) {
return view('posts.show', compact('category', 'post'));
}
);
G. Blade Templating System (60 menit)
Blade Fundamentals:
blade
{{-- resources/views/posts/index.blade.php --}}
{{-- Extend layout --}}
@extends('layouts.app')
{{-- Define section content --}}
@section('title', 'Blog Posts')
@section('content')
<div class="container">
<h1>Blog Posts</h1>
{{-- Display flash message --}}
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
{{-- Loop through posts --}}
@forelse($posts as $post)
<article class="mb-5">
<h2>
<a href="{{ route('posts.show', $post) }}">
{{ $post->title }}
</a>
</h2>
<div class="meta">
By {{ $post->author->name }}
on {{ $post->published_at->format('F d, Y') }}
in {{ $post->category->name ?? 'Uncategorized' }}
</div>
<p>{{ Str::limit($post->excerpt, 200) }}</p>
<div class="tags">
@foreach($post->tags as $tag)
<span class="badge bg-secondary">{{ $tag->name }}</span>
@endforeach
</div>
</article>
@empty
<p>No posts found.</p>
@endforelse
{{-- Pagination --}}
{{ $posts->links() }}
</div>
@endsectionLayout System:
blade
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'My Blog')</title>
{{-- CSS --}}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
{{-- Additional head content --}}
@stack('styles')
</head>
<body>
{{-- Navigation --}}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{{ route('home') }}">My Blog</a>
<div class="navbar-nav">
<a class="nav-link" href="{{ route('posts.index') }}">Blog</a>
<a class="nav-link" href="{{ route('portfolio.index') }}">Portfolio</a>
@auth
<a class="nav-link" href="{{ route('admin.dashboard') }}">Dashboard</a>
@endauth
</div>
</div>
</nav>
{{-- Main content --}}
<main class="py-4">
@yield('content')
</main>
{{-- Footer --}}
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p>© {{ date('Y') }} My Blog. All rights reserved.</p>
</div>
</footer>
{{-- JavaScript --}}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{{-- Additional scripts --}}
@stack('scripts')
</body>
</html>Components & Includes:
blade
{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info', 'message'])
<div class="alert alert-{{ $type }} alert-dismissible fade show" role="alert">
{{ $message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{{-- Penggunaan --}}
<x-alert type="success" message="Post created successfully!" />
{{-- Includes --}}
{{-- resources/views/posts/partials/meta.blade.php --}}
<div class="post-meta">
<span class="author">By {{ $post->author->name }}</span>
<span class="date">{{ $post->published_at->diffForHumans() }}</span>
<span class="category">{{ $post->category->name }}</span>
</div>
{{-- Include di view --}}
@include('posts.partials.meta', ['post' => $post])
H. Praktikum: Halaman Publik Blog (60 menit)
Tugas Individu:
laravel new my-blog cd my-blog # Konfigurasi .env dengan database yang sudah dibuat di Fase 1
// routes/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/about', [PageController::class, 'about'])->name('about');
Route::resource('posts', PostController::class);
Route::get('/categories/{category}', [CategoryController::class, 'show'])->name('categories.show');// app/Http/Controllers/HomeController.php
public function index() {
$featuredPosts = collect([
['title' => 'Getting Started with Laravel', 'excerpt' => '...'],
['title' => 'Database Design Best Practices', 'excerpt' => '...'],
]);
$recentPosts = collect([
['title' => 'Recent Post 1', 'excerpt' => '...'],
['title' => 'Recent Post 2', 'excerpt' => '...'],
]);
return view('home', compact('featuredPosts', 'recentPosts'));
}Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)
A. Review & Diskusi (30 menit)
B. Migration vs Manual SQL (30 menit)
Keuntungan Migration:
Perbandingan:
sql
-- MANUAL SQL
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
-- ... lainnya
);
-- LARAVEL MIGRATION
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 200);
// ... lainnya
});Migration Workflow:
bash
# Buat migration php artisan make:migration create_posts_table # Jalankan migration php artisan migrate # Rollback terakhir php artisan migrate:rollback # Rollback semua php artisan migrate:reset # Refresh (rollback + migrate) php artisan migrate:refresh # Status migration php artisan migrate:status
C. Migration Implementation (60 menit - PRAKTIKUM)
Membuat Migration untuk Blog System:
php
// database/migrations/xxxx_create_posts_table.php
<?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::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 200);
$table->string('slug', 220)->unique();
$table->text('excerpt')->nullable();
$table->longText('content');
$table->string('featured_image')->nullable();
$table->enum('status', ['draft', 'published', 'archived'])
->default('draft');
$table->timestamp('published_at')->nullable();
// Foreign keys
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->nullable()->constrained()->onDelete('set null');
// Meta fields
$table->string('meta_title', 200)->nullable();
$table->text('meta_description')->nullable();
$table->integer('view_count')->default(0);
// Timestamps
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('slug');
$table->index('status');
$table->index('published_at');
$table->index(['status', 'published_at']);
$table->fullText(['title', 'content']);
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};php
// database/migrations/xxxx_create_categories_table.php
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique();
$table->string('slug', 60)->unique();
$table->text('description')->nullable();
$table->foreignId('parent_id')->nullable()->constrained('categories')->onDelete('cascade');
$table->timestamps();
$table->index('slug');
});php
// Tags table
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique();
$table->string('slug', 60)->unique();
$table->timestamps();
});
// Post Tag junction table
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->primary(['post_id', 'tag_id']);
$table->index('tag_id');
});php
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->enum('status', ['pending', 'approved', 'spam'])->default('pending');
// Relationships
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('parent_id')->nullable()->constrained('comments')->onDelete('cascade');
// Guest comment info
$table->string('author_name', 100)->nullable();
$table->string('author_email', 100)->nullable();
$table->string('author_ip', 45);
$table->timestamps();
// Indexes
$table->index('post_id');
$table->index('status');
$table->index(['post_id', 'status']);
});
D. Eloquent ORM Fundamentals (45 menit)
Model Creation:
bash
# Buat model dengan migration php artisan make:model Post -m # Buat model dengan migration, factory, seeder, controller, dan policy php artisan make:model Post -a
Basic Model:
php
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Post extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'slug',
'excerpt',
'content',
'featured_image',
'status',
'published_at',
'user_id',
'category_id',
'meta_title',
'meta_description',
'view_count'
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'published_at' => 'datetime',
'view_count' => 'integer'
];
/**
* Default values for attributes.
*
* @var array
*/
protected $attributes = [
'status' => 'draft',
'view_count' => 0
];
/**
* Scope a query to only include published posts.
*/
public function scopePublished($query)
{
return $query->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
/**
* Scope a query to only include posts by a specific user.
*/
public function scopeByAuthor($query, $userId)
{
return $query->where('user_id', $userId);
}
/**
* Get the excerpt with a limit.
*/
public function getShortExcerptAttribute()
{
return Str::limit($this->excerpt ?? $this->content, 150);
}
/**
* Get the reading time in minutes.
*/
public function getReadingTimeAttribute()
{
$wordCount = str_word_count(strip_tags($this->content));
$minutes = ceil($wordCount / 200); // 200 words per minute
return $minutes . ' min read';
}
}CRUD Operations with Eloquent:
php
// CREATE
$post = new Post();
$post->title = 'My First Post';
$post->content = 'Post content...';
$post->save();
// atau
$post = Post::create([
'title' => 'My First Post',
'content' => 'Post content...'
]);
// READ
$post = Post::find(1); // by ID
$post = Post::where('slug', 'my-first-post')->first();
$posts = Post::all();
$publishedPosts = Post::where('status', 'published')->get();
// UPDATE
$post = Post::find(1);
$post->title = 'Updated Title';
$post->save();
// atau
Post::where('status', 'draft')->update(['status' => 'published']);
// DELETE
$post = Post::find(1);
$post->delete(); // soft delete jika ada SoftDeletes trait
// Force delete
$post->forceDelete();
// Restore soft deleted
Post::withTrashed()->find(1)->restore();
E. Eloquent Relationships (60 menit)
1. One-to-Many (User has many Posts):
php
// app/Models/User.php
class User extends Authenticatable
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function portfolios()
{
return $this->hasMany(Portfolio::class);
}
}
// app/Models/Post.php
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
}
// Penggunaan
$user = User::find(1);
$posts = $user->posts;
$post = Post::find(1);
$author = $post->user;2. Many-to-Many (Post has many Tags):
php
// app/Models/Post.php
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
// app/Models/Tag.php
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}
// Penggunaan
$post = Post::find(1);
// Attach tags
$post->tags()->attach([1, 2, 3]);
// Sync tags (replace all)
$post->tags()->sync([1, 3]);
// Detach tag
$post->tags()->detach(2);
// Detach all
$post->tags()->detach();
// Get posts with tags
$posts = Post::with('tags')->get();
// Get tags for a post
foreach($post->tags as $tag) {
echo $tag->name;
}3. One-to-One (User has one Profile):
php
// Migration untuk user_profiles
Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained();
$table->string('bio')->nullable();
$table->string('website')->nullable();
$table->string('twitter')->nullable();
$table->string('github')->nullable();
$table->timestamps();
});
// app/Models/User.php
public function profile()
{
return $this->hasOne(UserProfile::class);
}
// app/Models/UserProfile.php
class UserProfile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
// Penggunaan
$user = User::find(1);
$profile = $user->profile;
// Create profile
$user->profile()->create([
'bio' => 'Web developer...'
]);4. Has Many Through (Category has many Comments through Posts):
php
// app/Models/Category.php
class Category extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function comments()
{
return $this->hasManyThrough(Comment::class, Post::class);
}
}5. Polymorphic Relationships (Comments for Posts and Portfolios):
php
// Migration comments table
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->morphs('commentable'); // adds commentable_id and commentable_type
// ... other fields
});
// app/Models/Comment.php
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
// app/Models/Post.php
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// app/Models/Portfolio.php
class Portfolio extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// Penggunaan
$post = Post::find(1);
$comments = $post->comments;
$portfolio = Portfolio::find(1);
$comments = $portfolio->comments;
F. Seeder & Factory (45 menit)
Factory untuk User:
php
// database/factories/UserFactory.php
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => bcrypt('password'),
'role' => fake()->randomElement(['admin', 'author', 'guest']),
'bio' => fake()->paragraph(),
'created_at' => fake()->dateTimeBetween('-1 year', 'now'),
];
}
public function admin()
{
return $this->state(fn (array $attributes) => [
'role' => 'admin',
'email' => 'admin@blog.com',
]);
}
public function author()
{
return $this->state(fn (array $attributes) => [
'role' => 'author',
]);
}
}Factory untuk Post:
php
// database/factories/PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(6),
'slug' => fn (array $attributes) => Str::slug($attributes['title']),
'excerpt' => fake()->paragraph(),
'content' => fake()->text(2000),
'status' => fake()->randomElement(['draft', 'published', 'archived']),
'published_at' => fake()->optional(0.8)->dateTimeBetween('-6 months', 'now'),
'featured_image' => fake()->optional(0.7)->imageUrl(1200, 630, 'business', true),
'view_count' => fake()->numberBetween(0, 10000),
'user_id' => User::factory(),
'category_id' => Category::factory(),
'created_at' => fake()->dateTimeBetween('-1 year', 'now'),
];
}
public function published()
{
return $this->state(fn (array $attributes) => [
'status' => 'published',
'published_at' => now(),
]);
}
public function draft()
{
return $this->state(fn (array $attributes) => [
'status' => 'draft',
'published_at' => null,
]);
}
}Database Seeder:
php
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Create users
$admin = User::factory()->admin()->create([
'name' => 'Administrator',
'email' => 'admin@blog.com',
]);
$authors = User::factory()
->count(5)
->author()
->create();
// Create categories
$categories = Category::factory()
->count(10)
->create();
// Create tags
$tags = Tag::factory()
->count(20)
->create();
// Create posts
$posts = Post::factory()
->count(50)
->create();
// Attach tags to posts
$posts->each(function ($post) use ($tags) {
$post->tags()->attach(
$tags->random(rand(2, 5))->pluck('id')->toArray()
);
});
// Create comments
Comment::factory()
->count(200)
->create();
// Create portfolios
Portfolio::factory()
->count(15)
->create(['user_id' => $admin->id]);
}
}Running Seeders:
bash
# Seed semua php artisan db:seed # Seed specific seeder php artisan db:seed --class=DatabaseSeeder # Refresh dan seed php artisan migrate:refresh --seed # Fresh install (drop semua table, lalu migrate + seed) php artisan migrate:fresh --seed
G. Sinkronisasi ERD → Laravel (60 menit - PRAKTIKUM)
Tugas Kelompok: Implementasi database design dari Fase 1
Langkah-langkah:
php artisan make:migration create_categories_table php artisan make:migration create_posts_table php artisan make:migration create_tags_table php artisan make:migration create_post_tag_table php artisan make:migration create_comments_table php artisan make:migration create_portfolios_table
php artisan make:model Category -mfs php artisan make:model Post -mfs php artisan make:model Tag -mfs php artisan make:model Comment -mfs php artisan make:model Portfolio -mfs
php artisan tinker # Test di tinker >>> $user = User::first() >>> $user->posts >>> $post = Post::first() >>> $post->tags >>> $post->comments
Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)
A. Review & Demo (30 menit)
B. Form Request Validation (45 menit)
1. Basic Validation in Controller:
php
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:200',
'content' => 'required|string|min:100',
'category_id' => 'nullable|exists:categories,id',
'status' => 'required|in:draft,published,archived',
'published_at' => 'nullable|date',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'featured_image' => 'nullable|image|max:2048',
'meta_title' => 'nullable|string|max:200',
'meta_description' => 'nullable|string|max:500',
]);
// Handle file upload
if ($request->hasFile('featured_image')) {
$path = $request->file('featured_image')->store('posts', 'public');
$validated['featured_image'] = $path;
}
// Create post
$post = auth()->user()->posts()->create($validated);
// Sync tags if provided
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
return redirect()->route('posts.show', $post)
->with('success', 'Post created successfully.');
}2. Custom Validation Rules:
php
// app/Rules/SlugRule.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class SlugRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value)) {
$fail('The :attribute must be a valid slug (lowercase, hyphens, numbers).');
}
}
}
// Penggunaan
use App\Rules\SlugRule;
$request->validate([
'slug' => ['required', new SlugRule, 'unique:posts,slug'],
]);3. Form Request Classes:
bash
php artisan make:request StorePostRequest
php
<?php
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return auth()->check(); // Hanya user yang login
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'title' => 'required|string|max:200',
'slug' => [
'required',
'alpha_dash',
'max:220',
Rule::unique('posts')->ignore($this->route('post'))
],
'excerpt' => 'nullable|string|max:500',
'content' => 'required|string|min:100',
'category_id' => 'nullable|exists:categories,id',
'status' => 'required|in:draft,published,archived',
'published_at' => 'nullable|date|after_or_equal:today',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'featured_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:5120',
'meta_title' => 'nullable|string|max:200',
'meta_description' => 'nullable|string|max:500',
];
}
/**
* Custom messages for validation errors.
*/
public function messages(): array
{
return [
'title.required' => 'Judul artikel wajib diisi.',
'slug.unique' => 'Slug sudah digunakan. Coba judul yang berbeda.',
'content.min' => 'Konten minimal 100 karakter.',
'featured_image.max' => 'Ukuran gambar maksimal 5MB.',
'published_at.after_or_equal' => 'Tanggal publikasi tidak boleh di masa lalu.',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Auto-generate slug if empty
if (!$this->has('slug') && $this->has('title')) {
$this->merge([
'slug' => Str::slug($this->title)
]);
}
// Set published_at to null if status is draft
if ($this->status === 'draft') {
$this->merge([
'published_at' => null
]);
}
}
}4. Validation in Controller dengan Form Request:
php
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
// Validation sudah dilakukan, data sudah valid
$validated = $request->validated();
// Create post
$post = auth()->user()->posts()->create($validated);
// Handle tags
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
return redirect()->route('posts.show', $post);
}
public function update(UpdatePostRequest $request, Post $post)
{
$validated = $request->validated();
$post->update($validated);
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
return redirect()->route('posts.show', $post);
}
}
C. Authentication System (45 menit)
1. Laravel Breeze Installation:
bash
composer require laravel/breeze --dev # Install dengan Blade stack php artisan breeze:install blade # Jalankan migration php artisan migrate # Install NPM dependencies npm install npm run dev
2. Customizing Authentication:
php
// app/Models/User.php
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
protected $fillable = [
'name',
'email',
'password',
'role',
'bio',
'avatar',
'email_verified_at',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
// Role checking methods
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isAuthor(): bool
{
return $this->role === 'author';
}
public function isGuest(): bool
{
return $this->role === 'guest';
}
// Avatar URL
public function getAvatarUrlAttribute()
{
return $this->avatar
? asset('storage/' . $this->avatar)
: 'https://ui-avatars.com/api/?name=' . urlencode($this->name) . '&color=7F9CF5&background=EBF4FF';
}
}3. Registration with Additional Fields:
php
// app/Http/Controllers/Auth/RegisteredUserController.php
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|confirmed|min:8',
'bio' => 'nullable|string|max:500',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'bio' => $request->bio,
'role' => 'author', // Default role
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}4. Protecting Routes:
php
// routes/web.php
// Auth middleware
Route::middleware('auth')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard');
Route::resource('posts', PostController::class)->except(['index', 'show']);
Route::resource('portfolios', PortfolioController::class)->except(['index', 'show']);
});
// Guest middleware (hanya untuk non-logged in users)
Route::middleware('guest')->group(function () {
Route::get('/register', [RegisteredUserController::class, 'create']);
Route::post('/register', [RegisteredUserController::class, 'store']);
Route::get('/login', [AuthenticatedSessionController::class, 'create']);
Route::post('/login', [AuthenticatedSessionController::class, 'store']);
});
// Public routes (tanpa middleware)
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::resource('posts', PostController::class)->only(['index', 'show']);
Route::resource('portfolios', PortfolioController::class)->only(['index', 'show']);
D. Authorization dengan Policies & Gates (60 menit)
1. Creating Policies:
bash
# Buat policy untuk Post php artisan make:policy PostPolicy --model=Post # Buat policy untuk Comment php artisan make:policy CommentPolicy --model=Comment
2. Post Policy Implementation:
php
<?php
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true; // Semua user bisa lihat list posts
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Post $post): bool
{
// Admin bisa lihat semua
if ($user->isAdmin()) {
return true;
}
// Author hanya bisa lihat post mereka sendiri atau yang published
return $post->status === 'published' || $user->id === $post->user_id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Hanya admin dan author yang bisa create post
return $user->isAdmin() || $user->isAuthor();
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Post $post): bool
{
// Admin bisa update semua post
if ($user->isAdmin()) {
return true;
}
// Author hanya bisa update post mereka sendiri
return $user->id === $post->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Post $post): bool
{
// Admin bisa delete semua post
if ($user->isAdmin()) {
return true;
}
// Author hanya bisa delete post mereka sendiri yang masih draft
return $user->id === $post->user_id && $post->status === 'draft';
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Post $post): bool
{
return $user->isAdmin(); // Hanya admin yang bisa restore
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Post $post): bool
{
return $user->isAdmin(); // Hanya admin yang bisa force delete
}
/**
* Determine whether the user can publish the post.
*/
public function publish(User $user, Post $post): bool
{
// Admin bisa publish semua
if ($user->isAdmin()) {
return true;
}
// Author hanya bisa publish post mereka sendiri
return $user->id === $post->user_id;
}
/**
* Determine whether the user can view draft posts.
*/
public function viewDraft(User $user, Post $post): bool
{
// Admin bisa lihat semua draft
if ($user->isAdmin()) {
return true;
}
// Author hanya bisa lihat draft mereka sendiri
return $user->id === $post->user_id;
}
}3. Register Policies:
php
// app/Providers/AuthServiceProvider.php
protected $policies = [
Post::class => PostPolicy::class,
Comment::class => CommentPolicy::class,
Portfolio::class => PortfolioPolicy::class,
];4. Using Policies in Controllers:
php
class PostController extends Controller
{
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
public function edit(Post $post)
{
// Authorization sudah dihandle oleh authorizeResource
return view('posts.edit', compact('post'));
}
public function publish(Request $request, Post $post)
{
// Manual authorization check
$this->authorize('publish', $post);
$post->update([
'status' => 'published',
'published_at' => now(),
]);
return redirect()->route('posts.show', $post);
}
}5. Using Policies in Blade Views:
blade
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}" class="btn btn-sm btn-outline-primary">
Edit
</a>
@endcan
@can('delete', $post)
<form action="{{ route('posts.destroy', $post) }}" method="POST" class="d-inline">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure?')">
Delete
</button>
</form>
@endcan
@can('publish', $post)
@if($post->status === 'draft')
<form action="{{ route('posts.publish', $post) }}" method="POST" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-success">
Publish
</button>
</form>
@endif
@endcan6. Gates (Global Authorization Rules):
php
// app/Providers/AuthServiceProvider.php
public function boot(): void
{
// Define gates
Gate::define('access-admin-panel', function (User $user) {
return $user->isAdmin();
});
Gate::define('manage-users', function (User $user) {
return $user->isAdmin();
});
Gate::define('moderate-comments', function (User $user) {
return $user->isAdmin() || $user->isAuthor();
});
// Conditional based on subscription or other factors
Gate::define('create-portfolio', function (User $user) {
return $user->isAdmin() ||
($user->isAuthor() && $user->portfolios()->count() < 10);
});
}
E. Soft Delete & Audit Trail (30 menit)
1. Implementing Soft Deletes:
php
// app/Models/Post.php
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
}
// Querying soft deleted models
$posts = Post::withTrashed()->get(); // Dengan yang soft deleted
$posts = Post::onlyTrashed()->get(); // Hanya yang soft deleted
// Restore
$post->restore();
// Force delete
$post->forceDelete();2. Audit Trail with Observers:
bash
php artisan make:observer PostObserver --model=Post
php
// app/Observers/PostObserver.php
namespace App\Observers;
use App\Models\Post;
use App\Models\AuditLog;
class PostObserver
{
/**
* Handle the Post "created" event.
*/
public function created(Post $post): void
{
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'created',
'model_type' => Post::class,
'model_id' => $post->id,
'old_values' => null,
'new_values' => $post->toArray(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
/**
* Handle the Post "updated" event.
*/
public function updated(Post $post): void
{
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'updated',
'model_type' => Post::class,
'model_id' => $post->id,
'old_values' => $post->getOriginal(),
'new_values' => $post->getChanges(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
/**
* Handle the Post "deleted" event.
*/
public function deleted(Post $post): void
{
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'deleted',
'model_type' => Post::class,
'model_id' => $post->id,
'old_values' => $post->toArray(),
'new_values' => null,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
}php
// app/Providers/EventServiceProvider.php
protected $observers = [
Post::class => [PostObserver::class],
Comment::class => [CommentObserver::class],
];
F. Praktikum: Proteksi Admin & User Management (60 menit)
Tugas: Implementasi sistem autentikasi dan autorisasi lengkap
Langkah-langkah:
Admin Dashboard Controller:
php
<?php
// app/Http/Controllers/Admin/DashboardController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\User;
use App\Models\Comment;
use App\Models\Portfolio;
class DashboardController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('can:access-admin-panel');
}
public function index()
{
$stats = [
'total_posts' => Post::count(),
'published_posts' => Post::where('status', 'published')->count(),
'total_users' => User::count(),
'pending_comments' => Comment::where('status', 'pending')->count(),
'total_portfolios' => Portfolio::count(),
];
$recentPosts = Post::with('user')
->latest()
->take(5)
->get();
$recentUsers = User::latest()
->take(5)
->get();
return view('admin.dashboard', compact('stats', 'recentPosts', 'recentUsers'));
}
}Admin User Management:
php
// app/Http/Controllers/Admin/UserController.php
public function update(Request $request, User $user)
{
// Authorization check
$this->authorize('manage-users');
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
'role' => 'required|in:admin,author,guest',
'bio' => 'nullable|string|max:500',
]);
$user->update($validated);
return redirect()->route('admin.users.index')
->with('success', 'User updated successfully.');
}Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)
A. Review & Performance Issues (30 menit)
B. Eager Loading vs Lazy Loading (45 menit)
1. The N+1 Problem:
php
// BAD: N+1 QUERIES
$posts = Post::where('status', 'published')->get();
foreach ($posts as $post) {
// Query untuk setiap post (N queries)
echo $post->user->name;
// Query untuk setiap post (N queries)
echo $post->category->name;
// Query untuk setiap post (N queries)
foreach ($post->tags as $tag) {
echo $tag->name;
}
}
// Total queries: 1 (posts) + N (user) + N (category) + N*M (tags) = Banyak!2. Eager Loading Solution:
php
// GOOD: 4 QUERIES TOTAL
$posts = Post::where('status', 'published')
->with(['user', 'category', 'tags'])
->get();
foreach ($posts as $post) {
echo $post->user->name; // No query - already loaded
echo $post->category->name; // No query - already loaded
foreach ($post->tags as $tag) {
echo $tag->name; // No query - already loaded
}
}3. Nested Eager Loading:
php
// Load comments with their authors
$posts = Post::with(['comments.user', 'category', 'tags'])->get();
// Constrained eager loading
$posts = Post::with([
'comments' => function ($query) {
$query->where('status', 'approved')
->orderBy('created_at', 'desc')
->limit(5);
},
'category',
'tags'
])->get();
// Load counts without loading relationships
$posts = Post::withCount(['comments', 'tags'])->get();
foreach ($posts as $post) {
echo $post->comments_count;
echo $post->tags_count;
}4. Lazy Eager Loading:
php
// Useful when you don't know initially what you need
$posts = Post::where('status', 'published')->get();
// Later, load relationships for specific posts
$posts->load(['user', 'category', 'tags']);
// Or load missing relationships
if ($someCondition) {
$posts->loadMissing(['category']);
}
C. Query Scopes for Reusability (30 menit)
1. Local Scopes:
php
// app/Models/Post.php
class Post extends Model
{
// Published scope
public function scopePublished($query)
{
return $query->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
// By category scope
public function scopeByCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
// Popular posts (high views)
public function scopePopular($query, $days = 30)
{
return $query->where('created_at', '>=', now()->subDays($days))
->orderBy('view_count', 'desc');
}
// Search scope
public function scopeSearch($query, $searchTerm)
{
return $query->where(function ($q) use ($searchTerm) {
$q->where('title', 'like', "%{$searchTerm}%")
->orWhere('content', 'like', "%{$searchTerm}%");
});
}
// With relationships scope
public function scopeWithRelations($query)
{
return $query->with(['user', 'category', 'tags', 'comments' => function ($q) {
$q->where('status', 'approved');
}]);
}
}
// Penggunaan
$posts = Post::published()
->byCategory($categoryId)
->popular(7)
->withRelations()
->paginate(10);2. Global Scopes:
php
// app/Scopes/PublishedScope.php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
// Untuk admin, tampilkan semua
if (auth()->check() && auth()->user()->isAdmin()) {
return;
}
// Untuk user biasa, hanya tampilkan yang published
$builder->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
// app/Models/Post.php
protected static function booted()
{
static::addGlobalScope(new PublishedScope);
}
// Bypass global scope
$posts = Post::withoutGlobalScope(PublishedScope::class)->get();
$allPosts = Post::withoutGlobalScopes()->get();
D. Query Optimization Techniques (45 menit)
1. Select Only Needed Columns:
php
// BAD: Select semua kolom
$posts = Post::all();
// GOOD: Select hanya yang dibutuhkan
$posts = Post::select(['id', 'title', 'slug', 'excerpt', 'published_at', 'user_id'])
->with(['user:id,name,avatar'])
->get();2. Use Database Indexes Effectively:
php
// Migration untuk indexes
Schema::table('posts', function (Blueprint $table) {
// Composite index untuk query yang sering digunakan
$table->index(['status', 'published_at']);
// Full-text index untuk search
$table->fullText(['title', 'content']);
});
// Query yang memanfaatkan index
$posts = Post::where('status', 'published')
->where('published_at', '<=', now())
->orderBy('published_at', 'desc')
->get(); // Menggunakan index (status, published_at)3. Pagination vs Chunking:
php
// Pagination untuk UI
$posts = Post::published()->paginate(15);
// Chunking untuk batch processing
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// Process each post
}
});
// Cursor pagination untuk large datasets
$posts = Post::orderBy('id')->cursorPaginate(15);4. Raw Expressions & Subqueries:
php
// Raw expression untuk calculated fields
$posts = Post::select([
'posts.*',
DB::raw('(SELECT COUNT(*) FROM comments WHERE comments.post_id = posts.id AND status = "approved") as approved_comments_count')
])
->get();
// Subquery untuk ordering
$posts = Post::addSelect(['latest_comment' => Comment::select('created_at')
->whereColumn('post_id', 'posts.id')
->where('status', 'approved')
->latest()
->limit(1)
])->orderBy('latest_comment', 'desc')
->get();5. Caching Expensive Queries:
php
use Illuminate\Support\Facades\Cache;
public function getPopularPosts()
{
return Cache::remember('popular_posts', 3600, function () {
return Post::published()
->with(['user:id,name', 'category:id,name'])
->orderBy('view_count', 'desc')
->limit(10)
->get();
});
}
// Cache dengan tags
public function getCategoryPosts($categoryId)
{
return Cache::tags(['posts', 'category-' . $categoryId])
->remember('category_posts_' . $categoryId, 1800, function () use ($categoryId) {
return Post::where('category_id', $categoryId)
->published()
->with(['user:id,name'])
->orderBy('published_at', 'desc')
->paginate(10);
});
}
// Clear cache ketika post diupdate
public function update(Request $request, Post $post)
{
// Update post
$post->update($request->validated());
// Clear relevant caches
Cache::forget('popular_posts');
Cache::tags(['posts', 'category-' . $post->category_id])->flush();
return redirect()->route('posts.show', $post);
}
E. Debugging & Query Logging (45 menit)
1. Enable Query Log:
php
// Di controller atau tinker DB::enableQueryLog(); // Jalankan queries $posts = Post::with(['user', 'category'])->get(); // Get query log $queries = DB::getQueryLog(); dd($queries);
2. Laravel Debugbar:
bash
composer require barryvdh/laravel-debugbar --dev
Features:
3. Custom Logging for Slow Queries:
php
// app/Providers/AppServiceProvider.php
public function boot()
{
if (app()->environment('local')) {
DB::listen(function ($query) {
if ($query->time > 100) { // Queries slower than 100ms
Log::warning('Slow Query Detected', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time . 'ms',
'connection' => $query->connectionName,
]);
}
});
}
}4. Telescope for Advanced Debugging:
bash
composer require laravel/telescope --dev php artisan telescope:install php artisan migrate
Features:
F. Studi Kasus: Optimizing Blog Queries (60 menit - PRAKTIKUM)
Tugas: Optimasi query untuk halaman blog yang lambat
Scenario: Blog dengan 10,000 posts, 50,000 comments, 5,000 tags
1. Home Page Optimization:
php
// BEFORE (Slow)
public function index()
{
$posts = Post::where('status', 'published')
->orderBy('published_at', 'desc')
->take(10)
->get();
return view('home', compact('posts'));
}
// AFTER (Optimized)
public function index()
{
$posts = Cache::remember('homepage_posts', 300, function () {
return Post::select(['id', 'title', 'slug', 'excerpt', 'featured_image', 'published_at', 'user_id', 'category_id'])
->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now())
->with([
'user:id,name,avatar',
'category:id,name,slug',
'tags:id,name,slug'
])
->orderBy('published_at', 'desc')
->take(10)
->get();
});
$popularPosts = Cache::remember('popular_posts_weekly', 3600, function () {
return Post::select(['id', 'title', 'slug', 'view_count'])
->where('status', 'published')
->where('published_at', '>=', now()->subDays(7))
->orderBy('view_count', 'desc')
->take(5)
->get();
});
return view('home', compact('posts', 'popularPosts'));
}2. Posts Index with Filters:
php
public function index(Request $request)
{
$query = Post::published()
->select(['id', 'title', 'slug', 'excerpt', 'featured_image', 'published_at', 'user_id', 'category_id', 'view_count'])
->with([
'user:id,name,avatar',
'category:id,name,slug',
'tags:id,name,slug'
]);
// Apply filters
if ($request->has('category')) {
$query->whereHas('category', function ($q) use ($request) {
$q->where('slug', $request->category);
});
}
if ($request->has('tag')) {
$query->whereHas('tags', function ($q) use ($request) {
$q->where('slug', $request->tag);
});
}
if ($request->has('search')) {
$query->where(function ($q) use ($request) {
$q->where('title', 'like', '%' . $request->search . '%')
->orWhere('content', 'like', '%' . $request->search . '%');
});
}
// Ordering
$order = $request->get('order', 'latest');
switch ($order) {
case 'popular':
$query->orderBy('view_count', 'desc');
break;
case 'oldest':
$query->orderBy('published_at', 'asc');
break;
default: // latest
$query->orderBy('published_at', 'desc');
}
$posts = $query->paginate(12)->withQueryString();
// Preload categories and tags for filter dropdowns
$categories = Cache::remember('categories_list', 3600, function () {
return Category::select(['id', 'name', 'slug'])
->withCount(['posts' => function ($q) {
$q->published();
}])
->orderBy('name')
->get();
});
$popularTags = Cache::remember('popular_tags', 3600, function () {
return Tag::select(['id', 'name', 'slug'])
->withCount('posts')
->orderBy('posts_count', 'desc')
->take(20)
->get();
});
return view('posts.index', compact('posts', 'categories', 'popularTags'));
}3. Post Show Page with Comments:
php
public function show($slug)
{
// Cache individual post
$post = Cache::remember('post_' . $slug, 1800, function () use ($slug) {
return Post::where('slug', $slug)
->with([
'user:id,name,email,avatar,bio',
'category:id,name,slug',
'tags:id,name,slug'
])
->firstOrFail();
});
// Increment view count (delayed - use queue for production)
dispatch(function () use ($post) {
$post->increment('view_count');
})->afterResponse();
// Get comments with pagination
$comments = $post->comments()
->where('status', 'approved')
->with(['user:id,name,avatar'])
->orderBy('created_at', 'desc')
->paginate(20);
// Related posts
$relatedPosts = Cache::remember('related_posts_' . $post->id, 3600, function () use ($post) {
return Post::where('id', '!=', $post->id)
->where('category_id', $post->category_id)
->published()
->select(['id', 'title', 'slug', 'excerpt', 'published_at'])
->with(['category:id,name,slug'])
->orderBy('published_at', 'desc')
->take(5)
->get();
});
return view('posts.show', compact('post', 'comments', 'relatedPosts'));
}4. Dashboard Statistics (Admin):
php
public function dashboard()
{
// Cache all statistics
$stats = Cache::remember('dashboard_stats', 300, function () {
return [
'total_posts' => Post::count(),
'published_posts' => Post::where('status', 'published')->count(),
'draft_posts' => Post::where('status', 'draft')->count(),
'total_comments' => Comment::count(),
'pending_comments' => Comment::where('status', 'pending')->count(),
'total_users' => User::count(),
'active_authors' => User::where('role', 'author')
->whereHas('posts', function ($q) {
$q->where('created_at', '>=', now()->subDays(30));
})
->count(),
];
});
// Recent activity (no cache needed)
$recentActivity = Post::with(['user:id,name'])
->select(['id', 'title', 'status', 'created_at', 'user_id'])
->latest()
->take(10)
->get();
// Posts by status chart data
$postsByStatus = Cache::remember('posts_by_status', 3600, function () {
return Post::select('status', DB::raw('COUNT(*) as count'))
->groupBy('status')
->get()
->pluck('count', 'status')
->toArray();
});
// Monthly posts chart
$monthlyPosts = Cache::remember('monthly_posts', 3600, function () {
return Post::select(
DB::raw('DATE_FORMAT(created_at, "%Y-%m") as month'),
DB::raw('COUNT(*) as count')
)
->where('created_at', '>=', now()->subMonths(12))
->groupBy('month')
->orderBy('month')
->get();
});
return view('admin.dashboard', compact(
'stats',
'recentActivity',
'postsByStatus',
'monthlyPosts'
));
}
G. Praktikum: Performance Audit & Optimization (60 menit)
Tugas Individu: Audit dan optimasi aplikasi blog Anda
Langkah-langkah:
Checklist Optimasi:
Buat Blog & Portfolio System yang Fully Functional:
Delivery Requirements:
🚀 CONGRATULATIONS! Setelah menyelesaikan Fase 2, peserta telah memiliki: