Ini adalah website personal Selamat M. Harjono

Kata-Kata Hari Ini

"Lihatlah Apa Yang Dikatakan, Jangan Lihat Siapa Yang Berkata"

239
Des 17
Bootcamp HMMI dengan Tema "Personal Blog & Portfolio Management System" - Fase ke-II

Bootcamp HMMI dengan Tema "Personal Blog & Portfolio Management System" - Fase ke-II

Rabu, 17 Desember 2025 | oleh Selamat Muliyadi Harjono | Materi


FASE 2 – LARAVEL CORE + DATABASE INTEGRATION (Pertemuan 6–9)

🔹 STRATEGI PEMBELAJARAN FASE 2

  • "From Database to Application": Implementasi desain database ke Laravel
  • "MVC Mastery": Pemahaman mendalam pola MVC Laravel
  • "Eloquent First": Prioritaskan Eloquent daripada Query Builder
  • "Security by Default": Authentication & Authorization sejak awal

📚 PERTEMUAN 6 – LARAVEL & MVC DEEP UNDERSTANDING

Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)

🎯 TUJUAN PEMBELAJARAN

  1. Memahami filosofi dan arsitektur Laravel
  2. Menguasai konsep MVC dalam konteks Laravel
  3. Mampu membuat halaman blog dasar dengan routing, controller, dan blade

📖 MATERI DETAIL

A. Opening & Review Fase 1 (30 menit)

  • Diskusi hasil tugas database design
  • Q&A tentang konsep database yang belum jelas
  • Preview: "Bagaimana database design kita diimplementasi di Laravel?"

B. Filosofi Framework Laravel (30 menit)

  • "The PHP Framework for Web Artisans"
  • Prinsip-prinsip Laravel:
    1. Expressive Syntax: Kode yang mudah dibaca
    2. Convention over Configuration: Default yang masuk akal
    3. DRY (Don't Repeat Yourself): Reusability
    4. Batteries Included: Semua sudah tersedia
  • Ekosistem Laravel:
    • text
    • Laravel Framework
      ├── Eloquent ORM
      ├── Blade Templating
      ├── Artisan CLI
      ├── Migration System
      ├── Authentication
      └── Queue, Cache, etc.
  • Perbandingan dengan Native PHP:
    • php
    • // 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 Response

Komponen MVC:

  1. Model (Eloquent):
    • Representasi tabel database
    • Business logic
    • Data validation rules
  2. View (Blade):
    • Presentation layer
    • Template dengan inheritance
    • Directive: @if, @foreach, @auth
  3. Controller:
    • Menghubungkan Model dan View
    • Handle business logic
    • Return response

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>
@endforeach

D. 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>
@endsection



Layout 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>&copy; {{ 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:

  1. Setup Project:
  2. bash
  3. laravel new my-blog
    cd my-blog
    # Konfigurasi .env dengan database yang sudah dibuat di Fase 1


  4. Buat Route Dasar:
  5. php
  6. // 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');


  7. Buat Layout:
    • layouts/app.blade.php dengan Bootstrap
    • Navigation dengan links
    • Responsive design
  8. Buat Views:
    • home.blade.php: Featured posts, recent posts
    • posts/index.blade.php: List semua post
    • posts/show.blade.php: Detail post dengan comments
    • categories/show.blade.php: Post berdasarkan kategori
  9. Test dengan Dummy Data:
  10. php
  11. // 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'));
    }


🎯 TUGAS PERTEMUAN 6

  1. Buat project Laravel baru blog-portfolio
  2. Implementasi halaman publik dengan:
    • Layout utama dengan Bootstrap 5
    • Halaman home (featuring posts)
    • Halaman list posts (dummy data)
    • Halaman detail post
    • Navigation yang functional
  3. Push ke repository Git dengan commit message yang jelas

📚 PERTEMUAN 7 – MIGRATION, MODEL & ELOQUENT

Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)

🎯 TUJUAN PEMBELAJARAN

  1. Memahami migration system Laravel
  2. Menguasai Eloquent ORM dan relationships
  3. Mampu mengkonversi ERD ke Laravel Models

📖 MATERI DETAIL


A. Review & Diskusi (30 menit)

  • Review tugas halaman publik
  • Diskusi challenges yang dihadapi
  • Best practices structure views


B. Migration vs Manual SQL (30 menit)

Keuntungan Migration:

  1. Version Control: Track perubahan skema
  2. Collaboration: Tim kerja dengan skema sama
  3. Rollback: Mudah undo perubahan
  4. Environment Consistency: Dev, staging, production sama

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:

  1. Posts Migration:

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');
    }
};



  1. Categories Migration:

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');
});



  1. Tags & Post Tag Junction:

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');
});



  1. Comments Migration:

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:

  1. Review ERD dari tugas Fase 1
  2. Buat migration untuk setiap entity
  3. bash
  4. 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


  5. Buat model untuk setiap entity
  6. bash
  7. 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


  8. Definisikan relationships di setiap model
    • hasMany, belongsTo, belongsToMany, morphMany
  9. Buat factories dan seeders
  10. Test dengan tinker:
  11. bash
  12. php artisan tinker
    
    # Test di tinker
    >>> $user = User::first()
    >>> $user->posts
    >>> $post = Post::first()
    >>> $post->tags
    >>> $post->comments


🎯 TUGAS PERTEMUAN 7

  1. Buat semua migration untuk sistem blog+portfolio
  2. Buat semua model dengan relationships yang tepat
  3. Buat factories dan seeders untuk generate dummy data
  4. Test dengan:
    • php artisan migrate:refresh --seed
    • php artisan tinker untuk verify relationships
  5. Dokumentasi di README.md tentang:
    • Entity relationships
    • How to seed database
    • Important model methods

📚 PERTEMUAN 8 – VALIDATION, AUTH & POLICY

Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)

🎯 TUJUAN PEMBELAJARAN

  1. Menguasai form validation di Laravel
  2. Implementasi authentication system
  3. Menerapkan authorization dengan policies
  4. Memahami soft delete dan audit trail

📖 MATERI DETAIL


A. Review & Demo (30 menit)

  • Demo hasil migration dan models
  • Diskusi relationships yang complex
  • Q&A tentang Eloquent


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
@endcan



6. 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:

  1. Install Laravel Breeze
  2. Extend User model dengan role field
  3. Create policies untuk Post, Comment, Portfolio
  4. Implement middleware untuk role-based access
  5. Create admin dashboard dengan stats
  6. Implement user management (hanya admin)

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.');
}



🎯 TUGAS PERTEMUAN 8

  1. Implementasi authentication dengan Laravel Breeze
  2. Extend User model dengan:
    • Role field (admin, author, guest)
    • Profile fields (bio, avatar, website)
  3. Buat policies untuk:
    • Post (create, update, delete, publish)
    • Comment (moderate, delete)
    • Portfolio (create, update, delete)
  4. Implementasi admin dashboard dengan:
    • Statistics (posts, users, comments)
    • Recent activity
    • User management (CRUD)
  5. Implementasi soft delete untuk posts dan comments
  6. Test semua authorization scenarios

📚 PERTEMUAN 9 – QUERY OPTIMIZATION & DEBUGGING

Durasi: 3 jam (Teori: 1 jam, Praktik: 2 jam)

🎯 TUJUAN PEMBELAJARAN

  1. Memahami dan menghindari N+1 problem
  2. Menguasai eager loading dan lazy loading
  3. Menggunakan query log untuk debugging
  4. Menerapkan best practices query optimization

📖 MATERI DETAIL


A. Review & Performance Issues (30 menit)

  • Demo aplikasi dengan data real (500+ posts, 1000+ comments)
  • Identifikasi performance bottlenecks
  • Introduction to database query optimization


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:

  • Query execution time
  • N+1 problem detection
  • Memory usage
  • Request/Response info

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:

  • Request monitoring
  • Database queries
  • Mail preview
  • Job monitoring
  • Exception tracking


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:

  1. Install Debugbar dan aktifkan
  2. Buat sample data besar (1000+ posts, 5000+ comments)
  3. Test setiap halaman dan identifikasi:
    • N+1 queries
    • Slow queries (>100ms)
    • Memory issues
  4. Implement optimizations:
    • Eager loading
    • Select specific columns
    • Database indexes
    • Query caching
  5. Measure improvements dengan:
    • Query count reduction
    • Execution time improvement
    • Memory usage reduction

Checklist Optimasi:

  • Semua relationship menggunakan eager loading
  • Select hanya kolom yang dibutuhkan
  • Database indexes untuk WHERE, ORDER BY, JOIN
  • Pagination untuk large datasets
  • Cache untuk expensive queries
  • Queue untuk heavy operations (view counting)
  • Query log untuk monitoring

🎯 TUGAS AKHIR FASE 2

Buat Blog & Portfolio System yang Fully Functional:

  1. Backend Features:
    • Complete CRUD for Posts, Categories, Tags, Comments, Portfolios
    • User authentication with roles (admin, author, guest)
    • Authorization policies for all resources
    • Form validation with custom requests
    • Soft delete for posts and comments
  2. Frontend Features:
    • Public blog with listing, filtering, search
    • Single post view with comments
    • Portfolio showcase
    • Admin dashboard with statistics
    • Responsive design with Bootstrap 5
  3. Performance Optimizations:
    • Eager loading untuk semua relationships
    • Database indexes untuk query optimization
    • Cache untuk expensive queries
    • Pagination untuk semua lists
    • Select only needed columns
  4. Code Quality:
    • Clean controller methods (max 10 lines)
    • Repository pattern for complex queries (optional)
    • Service classes for business logic
    • Comprehensive validation
    • Error handling and user feedback
  5. Documentation:
    • README dengan setup instructions
    • Database schema documentation
    • API documentation (jika ada)
    • Deployment guide

Delivery Requirements:

  • GitHub repository dengan commit history yang jelas
  • Live demo (bisa di localhost dengan video demo)
  • Presentation slide (10 menit)
  • Performance benchmark sebelum dan sesudah optimasi

🎯 EVALUASI FASE 2

Kriteria Penilaian:

  1. Functionality (40%):
    • Semua CRUD operations bekerja
    • Authentication dan authorization
    • Validation dan error handling
  2. Code Quality (30%):
    • Clean code principles
    • Proper use of Laravel features
    • Database optimization
    • Security best practices
  3. Performance (20%):
    • No N+1 problems
    • Efficient database queries
    • Proper caching strategy
    • Fast page load times
  4. Documentation (10%):
    • Clear README
    • Code comments
    • Setup instructions

Kelulusan Minimum:

  • Mampu membuat aplikasi Laravel dengan authentication
  • Memahami dan menerapkan Eloquent relationships
  • Mampu melakukan basic query optimization
  • Siap untuk masuk ke FilamentPHP (Fase 3)

📚 RESOURCES TAMBAHAN FASE 2

Bacaan Wajib:

  1. Laravel Documentation
  2. Eloquent: Getting Started
  3. Laravel Authentication
  4. Database: Query Builder

Video Tutorials:

  1. Laracasts - Laravel 10 From Scratch
  2. CodeCourse - Laravel Beginner Series
  3. FreeCodeCamp - Laravel Course

Practice Platforms:

  1. Laravel Shift for code quality
  2. Laravel Pulse for performance monitoring
  3. Clockwork for debugging

Tools:

  1. Debugging: Laravel Debugbar, Telescope
  2. IDE: PHPStorm with Laravel plugin
  3. Database: TablePlus, MySQL Workbench
  4. API Testing: Postman, Insomnia

🚀 CONGRATULATIONS! Setelah menyelesaikan Fase 2, peserta telah memiliki:

  • Kemampuan membangun aplikasi Laravel full-stack
  • Pemahaman mendalam Eloquent ORM
  • Skill optimization database queries
  • Implementasi security best practices



Dilihat

239 kali

Trending

21