Tutorial Vibe Coding Bikin Projek Laravel 12 dengan Dockerfile Setup

Tutorial lengkap vibe coding setup projek Laravel 12 dengan Docker dari nol. Dalam tutorial ini, Angga Risky (Freelance Web Developer & Founder BuildWithAngga) akan membimbing kamu step-by-step cara menggunakan AI untuk setup development environment yang proper — dari Dockerfile, docker-compose, MySQL container, hingga migrations, models, seeders, dan verifikasi data di TablePlus. Cocok untuk freelancer yang ingin development environment yang konsisten dan developer yang mau belajar Docker untuk Laravel.


Bagian 1: Intro — Kenapa Docker untuk Laravel?

Halo! Gue Angga Risky, freelance web developer sekaligus founder BuildWithAngga.

Lo pernah ngalamin situasi kayak gini?

"Eh, code-nya works fine di laptop gue."

"Kok di laptop gue error ya? PHP version-nya beda."

"Bentar, gue install MySQL dulu... eh conflict sama PostgreSQL yang udah ada."

Kalau lo pernah ngalamin — welcome to the club. Ini masalah klasik yang hampir semua developer pernah rasain. Dan sebagai freelancer yang sering handle multiple projects dengan tech stack berbeda-beda, masalah ini makin kerasa.

Project A butuh PHP 8.1, project B butuh PHP 8.3. Project C pakai MySQL, project D pakai PostgreSQL. Belum lagi versi Node.js yang beda-beda. Laptop lo jadi kayak kapal pecah — penuh sama berbagai versi software yang saling tumpang tindih.

Solusinya? Docker.

Kenapa Docker Game-Changer untuk Freelancer?

Docker itu basically "kotak" yang nge-wrap semua yang dibutuhin project lo — PHP, database, web server — jadi satu package yang isolated. Setiap project punya "kotak" sendiri, gak ganggu satu sama lain.

TRADITIONAL SETUP (Tanpa Docker):
├── Laptop lo
│   ├── PHP 8.1 (untuk project A)
│   ├── PHP 8.3 (untuk project B) ← CONFLICT!
│   ├── MySQL 8.0
│   ├── PostgreSQL 15
│   ├── Redis
│   ├── Node 18
│   └── ... dan berbagai versi lainnya
│
└── PROBLEMS:
    ├── Version conflicts
    ├── "Works on my machine" syndrome
    ├── Susah onboard developer baru
    └── Setup berbeda di setiap laptop

DOCKER SETUP:
├── Project A (Container)
│   ├── PHP 8.1
│   ├── MySQL 5.7
│   └── Nginx
│
├── Project B (Container)
│   ├── PHP 8.3
│   ├── PostgreSQL 15
│   └── Nginx
│
└── Project C (Container)
    ├── PHP 8.2
    ├── MySQL 8.0
    └── Apache

BENEFITS:
├── ✅ Setiap project isolated
├── ✅ Gak ada conflict antar project
├── ✅ Environment consistent (dev = staging = production)
├── ✅ Onboard tim baru: 1 command aja
└── ✅ Easy deployment ke VPS/cloud

Before vs After Docker

Ini real comparison dari pengalaman gue:

BEFORE DOCKER (Setup Project Baru):
├── Install PHP versi yang dibutuhin → 15-30 menit
├── Setup MySQL/PostgreSQL → 10-20 menit
├── Configure virtual host → 10-15 menit
├── Fix permission issues → 15-30 menit
├── Debug "kok di gue gak jalan" → 30-60 menit
└── Total: 1.5 - 3 jam (per project)

AFTER DOCKER:
├── Clone repo
├── Copy .env.example ke .env
├── Run: docker-compose up -d
├── Run: docker-compose exec app composer install
├── Run: docker-compose exec app php artisan key:generate
└── Total: 5-10 menit ✅

HASIL: 10-20x lebih cepat untuk setup

Dan ini belum ngitung waktu debugging ketika ada masalah environment. Dengan Docker, kalau works di laptop lo, guaranteed works di laptop tim lo juga. Karena environment-nya identical.

Investasi yang Worth It

Gue akui, Docker punya learning curve. Pertama kali belajar, gue juga bingung — apa bedanya image sama container? Apa itu volume? Kenapa harus bikin Dockerfile?

Tapi trust me, ini investasi yang worth it. Sekali lo paham konsepnya, setup project baru jadi breeze. Dan yang lebih penting: lo gak perlu takut project lama rusak gara-gara update PHP atau install software baru.

Apa yang Akan Kita Bangun?

Di tutorial ini, kita akan setup Docker environment lengkap untuk Laravel 12. Bukan cuma setup doang — kita juga akan bikin simple database structure untuk e-commerce, lengkap dengan migrations, models, dan seeders.

PROJECT: LARAVEL 12 + DOCKER SETUP

DOCKER SERVICES:
├── app (PHP 8.3 FPM)
│   └── Laravel 12 application
│
├── webserver (Nginx)
│   └── Serve Laravel via port 8000
│
└── mysql (MySQL 8.0)
    └── Database server

DATABASE TABLES (Demo E-commerce):
├── users → Admin dan customers
├── categories → Kategori produk
├── products → Daftar produk
├── orders → Pesanan customer
└── order_items → Detail item per order

TOOLS:
├── Antigravity AI → Generate code dengan prompts
├── TablePlus → Verifikasi data di database
└── Terminal → Run Docker commands

Tech Stack

TECH STACK:
├── Laravel 12
│   └── PHP framework terbaru
│
├── PHP 8.3 FPM
│   └── Versi PHP terbaru dengan performance improvements
│
├── Nginx
│   └── Web server yang lightweight dan fast
│
├── MySQL 8.0
│   └── Database server
│
├── Docker & Docker Compose
│   └── Containerization platform
│
└── Antigravity AI
    └── AI coding assistant untuk generate code

Prerequisites

Sebelum mulai, pastikan lo udah install:

PREREQUISITES CHECKLIST:
□ Docker Desktop
  └── Download: <https://www.docker.com/products/docker-desktop>
  └── Pastikan sudah running (icon Docker di system tray)

□ Code Editor
  └── VS Code, PHPStorm, atau Antigravity
  └── Gue akan pakai Antigravity di tutorial ini

□ TablePlus (optional tapi recommended)
  └── Download: <https://tableplus.com>
  └── Untuk visualisasi database dengan GUI

□ Terminal
  └── Built-in terminal di Mac/Linux
  └── Windows: WSL2 atau Git Bash

□ Basic Laravel knowledge
  └── Paham artisan commands
  └── Paham konsep MVC

Kalau Docker Desktop belum ke-install, install dulu sekarang. Tutorial ini gak akan jalan tanpa Docker.

Roadmap Tutorial

Tutorial ini dibagi jadi 8 bagian yang bisa lo ikutin step-by-step:

ROADMAP TUTORIAL:

Bagian 1: ✓ Intro — Kenapa Docker? (lo di sini)
Bagian 2: Setup Docker Files
Bagian 3: Setup Laravel Project
Bagian 4: Database Migrations
Bagian 5: Models dengan Relationships
Bagian 6: Seeders dengan Data Realistic
Bagian 7: Verifikasi Data di TablePlus
Bagian 8: Useful Commands & Closing

Setiap bagian akan ada:

  • Prompt yang gue kasih ke Antigravity
  • Output code yang dihasilkan
  • Review dan penjelasan
  • Commands yang perlu lo jalanin
  • Checkpoint untuk verifikasi

Mindset Vibe Coding + Docker

Sebelum lanjut, gue mau tekanin satu hal: Docker dan vibe coding itu kombinasi yang powerful.

Dengan Docker, lo bisa generate code pakai AI tanpa takut merusak environment laptop lo. Kalau ada yang salah? Tinggal docker-compose down -v dan mulai fresh. Database corrupt? Rebuild container. PHP error? Check Dockerfile.

VIBE CODING + DOCKER MINDSET:

├── AI generate code → Lo review
├── Test di container → Isolated, aman
├── Ada error? → Rebuild container, fresh start
├── Works? → Commit, push, deploy
└── Environment sama di mana-mana

Ini level of confidence yang susah didapet kalau lo develop langsung di local machine.

Ready? Let's setup Docker! 🚀


Lanjut ke Bagian 2: Setup Docker Files →

Bagian 2: Setup Docker Files

Sekarang kita masuk ke bagian yang seru — setup Docker files dari nol. Di bagian ini, saya akan tunjukkan cara bikin konfigurasi Docker yang proper untuk Laravel development.

Konsep Dasar Docker (Singkat)

Sebelum mulai, ada 3 konsep yang perlu kamu pahami:

DOCKER CONCEPTS:

├── Image
│   └── "Blueprint" atau "template"
│   └── Berisi instruksi untuk build environment
│   └── Contoh: php:8.3-fpm, mysql:8.0, nginx:alpine
│
├── Container
│   └── Instance yang running dari image
│   └── Ibarat "VM mini" yang isolated
│   └── Bisa start, stop, restart, delete
│
└── Volume
    └── Persistent storage untuk container
    └── Data tetap ada walau container di-delete
    └── Penting untuk database!

Simpelnya: Image itu resep, Container itu masakan yang jadi, Volume itu tempat nyimpan bahan yang gak mau hilang.

Struktur Folder Docker

Pertama, kita buat struktur folder untuk Docker config:

company-profile/
├── docker/
│   ├── php/
│   │   └── Dockerfile
│   └── nginx/
│       └── default.conf
├── docker-compose.yml
├── .env
└── ... (Laravel files)

Buka terminal, jalankan:

# Buat folder structure
mkdir -p docker/php docker/nginx

PROMPT 1 — Dockerfile untuk PHP-FPM

Sekarang kita mulai generate files dengan Antigravity. Ini prompt pertama:

PROMPT KE ANTIGRAVITY:

"Buatkan Dockerfile untuk Laravel 12 dengan PHP 8.3 FPM.

Requirements:
- Base image: php:8.3-fpm
- Install extensions: pdo_mysql, mbstring, exif, pcntl, bcmath, gd, zip, intl, opcache
- Install Composer dari official image
- Set working directory /var/www
- Create user 'www' dengan uid 1000 untuk permission yang proper
- Copy php.ini configuration untuk development
- Expose port 9000

Output: docker/php/Dockerfile"

OUTPUT:

FROM php:8.3-fpm

# Arguments defined in docker-compose.yml
ARG user=www
ARG uid=1000

# Install system dependencies
RUN apt-get update && apt-get install -y \\
    git \\
    curl \\
    libpng-dev \\
    libonig-dev \\
    libxml2-dev \\
    libzip-dev \\
    libicu-dev \\
    zip \\
    unzip \\
    && apt-get clean \\
    && rm -rf /var/lib/apt/lists/*

# Install PHP extensions
RUN docker-php-ext-configure intl \\
    && docker-php-ext-install \\
        pdo_mysql \\
        mbstring \\
        exif \\
        pcntl \\
        bcmath \\
        gd \\
        zip \\
        intl \\
        opcache

# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u $uid -d /home/$user $user \\
    && mkdir -p /home/$user/.composer \\
    && chown -R $user:$user /home/$user

# Set working directory
WORKDIR /var/www

# Copy existing application directory permissions
COPY --chown=$user:$user . /var/www

# Change current user to www
USER $user

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

REVIEW:

DOCKERFILE REVIEW:
├── ✅ Base image php:8.3-fpm (latest stable)
├── ✅ System dependencies lengkap (git, curl, zip, dll)
├── ✅ PHP extensions yang dibutuhin Laravel
├── ✅ Composer dari official image
├── ✅ Custom user 'www' dengan uid 1000
│   └── Ini penting untuk avoid permission issues
├── ✅ Working directory /var/www
└── ✅ Expose port 9000 untuk FastCGI

Saya gak perlu ubah apa-apa, output sudah bagus. Langsung save ke docker/php/Dockerfile.


PROMPT 2 — Nginx Configuration

Selanjutnya kita butuh Nginx config untuk serve Laravel:

PROMPT KE ANTIGRAVITY:

"Buatkan Nginx configuration untuk Laravel.

Requirements:
- Listen port 80
- Server name localhost
- Root directory /var/www/public
- Index files: index.php, index.html
- Handle static files dengan try_files
- PHP processing via FastCGI ke container 'app' port 9000
- Proper location block untuk Laravel routing (semua request ke index.php)
- Deny access ke .htaccess dan hidden files
- Gzip compression enabled
- Client max body size 100M (untuk upload)

Output: docker/nginx/default.conf"

OUTPUT:

server {
    listen 80;
    server_name localhost;
    root /var/www/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php index.html;

    charset utf-8;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;

    # Max upload size
    client_max_body_size 100M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \\.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    # Deny access to hidden files
    location ~ /\\.(?!well-known).* {
        deny all;
    }
}

REVIEW:

NGINX CONFIG REVIEW:
├── ✅ Listen port 80
├── ✅ Root ke /var/www/public (Laravel public folder)
├── ✅ try_files untuk Laravel routing
├── ✅ FastCGI pass ke 'app:9000'
│   └── 'app' adalah nama service di docker-compose
├── ✅ Security headers (X-Frame-Options, X-Content-Type-Options)
├── ✅ Gzip compression enabled
├── ✅ Client max body size 100M
├── ✅ Deny hidden files (kecuali .well-known)
└── ✅ Hide X-Powered-By header

Output sudah production-ready. Save ke docker/nginx/default.conf.


PROMPT 3 — Docker Compose

Sekarang kita satukan semuanya dalam docker-compose.yml:

PROMPT KE ANTIGRAVITY:

"Buatkan docker-compose.yml untuk Laravel development environment.

Services yang dibutuhkan:

1. app (PHP-FPM)
   - Build dari ./docker/php dengan args user dan uid
   - Container name: laravel-app
   - Restart: unless-stopped
   - Volume: current directory ke /var/www
   - Network: laravel-network
   - Depends on: mysql

2. webserver (Nginx)
   - Image: nginx:alpine
   - Container name: laravel-nginx
   - Restart: unless-stopped
   - Ports: 8000:80
   - Volumes:
     - current directory ke /var/www
     - nginx config ke /etc/nginx/conf.d/default.conf
   - Network: laravel-network
   - Depends on: app

3. mysql
   - Image: mysql:8.0
   - Container name: laravel-mysql
   - Restart: unless-stopped
   - Environment dari .env file:
     - MYSQL_DATABASE
     - MYSQL_ROOT_PASSWORD
     - MYSQL_USER
     - MYSQL_PASSWORD
   - Ports: 3306:3306
   - Volume: mysql-data untuk persistence
   - Network: laravel-network
   - Health check untuk memastikan MySQL ready

Include:
- Named volume 'mysql-data'
- Custom network 'laravel-network' dengan bridge driver

Output: docker-compose.yml"

OUTPUT:

version: '3.8'

services:
  # PHP-FPM Service
  app:
    build:
      context: ./docker/php
      args:
        user: www
        uid: 1000
    container_name: laravel-app
    restart: unless-stopped
    working_dir: /var/www
    volumes:
      - ./:/var/www
    networks:
      - laravel-network
    depends_on:
      mysql:
        condition: service_healthy

  # Nginx Service
  webserver:
    image: nginx:alpine
    container_name: laravel-nginx
    restart: unless-stopped
    ports:
      - "8000:80"
    volumes:
      - ./:/var/www
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - laravel-network
    depends_on:
      - app

  # MySQL Service
  mysql:
    image: mysql:8.0
    container_name: laravel-mysql
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE:-laravel}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-secret}
      MYSQL_USER: ${DB_USERNAME:-laravel}
      MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - laravel-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-secret}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

# Docker Networks
networks:
  laravel-network:
    driver: bridge

# Docker Volumes
volumes:
  mysql-data:
    driver: local

REVIEW:

DOCKER-COMPOSE REVIEW:
├── ✅ 3 services: app, webserver, mysql
├── ✅ Build args untuk custom user
├── ✅ Container names yang descriptive
├── ✅ Restart policy: unless-stopped
├── ✅ Port mapping: 8000 → 80 (Nginx)
├── ✅ Port mapping: 3306 → 3306 (MySQL)
├── ✅ Environment variables dari .env
├── ✅ Named volume untuk MySQL persistence
├── ✅ Custom network untuk komunikasi antar container
├── ✅ Health check untuk MySQL
│   └── App akan tunggu MySQL ready sebelum start
├── ✅ depends_on dengan condition
└── ✅ Read-only mount untuk nginx config (:ro)

Ada satu hal penting di sini: health check. Ini memastikan container app baru jalan setelah MySQL benar-benar ready, bukan cuma container-nya running. Ini menghindari error "Connection refused" saat pertama kali start.


Struktur Folder Sekarang

Setelah semua files dibuat, struktur folder kamu harusnya seperti ini:

laravel-docker/
├── docker/
│   ├── php/
│   │   └── Dockerfile          ✅ Created
│   └── nginx/
│       └── default.conf        ✅ Created
├── docker-compose.yml          ✅ Created
└── ... (belum ada Laravel files)

CHECKPOINT

CHECKPOINT BAGIAN 2:
├── ✅ Folder docker/php/ dan docker/nginx/ sudah dibuat
├── ✅ Dockerfile untuk PHP-FPM sudah ada
├── ✅ Nginx config sudah ada
├── ✅ docker-compose.yml sudah ada
└── ⏳ Belum bisa test (butuh Laravel project dulu)

Tips: Kenapa Struktur Ini?

Kamu mungkin bertanya, kenapa gak langsung pakai image yang sudah jadi seperti laradock atau sail?

CUSTOM DOCKER vs LARADOCK/SAIL:

CUSTOM (yang kita buat):
├── ✅ Lightweight — cuma install yang dibutuhin
├── ✅ Full control — tau persis apa yang di-install
├── ✅ Easy to modify — mau tambah extension? edit Dockerfile
├── ✅ Learning — paham cara kerja Docker
└── ✅ Production-ready — bisa deploy ke server

LARADOCK/SAIL:
├── ✅ Quick start — tinggal pakai
├── ❌ Heavy — banyak service yang gak dipakai
├── ❌ Black box — gak tau isinya apa
├── ❌ Overhead — resource usage lebih besar
└── ❌ Learning curve tersembunyi

Untuk freelancer, saya recommend paham cara bikin Docker config sendiri. Ini skill yang berguna ketika deploy ke production atau debug issues di server client.


Lanjut ke Bagian 3: Setup Laravel Project →

Bagian 3: Setup Laravel Project

Sekarang Docker files sudah siap, saatnya kita setup Laravel project dan jalankan semuanya.

Step 1: Create Laravel Project

Ada dua cara untuk setup Laravel di dalam Docker environment:

OPSI SETUP LARAVEL:

├── Opsi A: Install Laravel dulu, baru Docker
│   └── composer create-project laravel/laravel .
│   └── Butuh PHP & Composer di local
│
└── Opsi B: Install Laravel via Docker (Recommended)
    └── Gak perlu PHP di local
    └── Semua via container

Kita pakai Opsi B karena lebih clean — kamu gak perlu install PHP di laptop sama sekali.

Pertama, kita perlu temporary container untuk install Laravel:

# Buat folder project (kalau belum ada)
mkdir laravel-docker
cd laravel-docker

# Copy Docker files yang sudah dibuat ke folder ini
# (Dockerfile, nginx config, docker-compose.yml)

# Download Laravel via temporary container
docker run --rm -v $(pwd):/app composer create-project laravel/laravel .

Tunggu proses download selesai. Ini akan install Laravel 12 dengan semua dependencies-nya.

SETELAH INSTALL:
laravel-docker/
├── app/
├── bootstrap/
├── config/
├── database/
├── docker/                 ← Docker files kita
│   ├── php/
│   └── nginx/
├── public/
├── resources/
├── routes/
├── storage/
├── tests/
├── vendor/
├── .env.example
├── artisan
├── composer.json
├── docker-compose.yml      ← Docker compose kita
└── ...

Step 2: Setup Environment File

PROMPT KE ANTIGRAVITY:

"Buatkan file .env untuk Laravel dengan Docker setup.

Requirements:
- APP_NAME: LaravelDocker
- APP_ENV: local
- APP_DEBUG: true
- APP_URL: <http://localhost:8000>

Database connection ke MySQL container:
- DB_CONNECTION: mysql
- DB_HOST: mysql (nama service di docker-compose)
- DB_PORT: 3306
- DB_DATABASE: laravel_docker
- DB_USERNAME: laravel
- DB_PASSWORD: secret

Cache, Session, Queue pakai file driver untuk development.

Tambahkan komentar untuk menjelaskan kenapa DB_HOST pakai 'mysql' bukan 'localhost'.

Output: .env"

OUTPUT:

APP_NAME=LaravelDocker
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=Asia/Jakarta
APP_URL=http://localhost:8000

# Locale settings
APP_LOCALE=id
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=id_ID

# Logging
LOG_CHANNEL=stack
LOG_LEVEL=debug

# Database Configuration
# PENTING: DB_HOST menggunakan 'mysql' karena itu nama service di docker-compose.yml
# Di dalam Docker network, containers berkomunikasi via service name, bukan localhost
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_docker
DB_USERNAME=laravel
DB_PASSWORD=secret

# Session & Cache (pakai file untuk development)
SESSION_DRIVER=file
SESSION_LIFETIME=120
CACHE_STORE=file
QUEUE_CONNECTION=sync

# Redis (optional, uncomment jika pakai Redis)
# REDIS_HOST=redis
# REDIS_PASSWORD=null
# REDIS_PORT=6379

# Mail (pakai log untuk development)
MAIL_MAILER=log
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"

# Filesystem
FILESYSTEM_DISK=local

# Vite
VITE_APP_NAME="${APP_NAME}"

REVIEW:

.ENV REVIEW:
├── ✅ APP_URL dengan port 8000 (sesuai docker-compose)
├── ✅ DB_HOST: mysql (nama service, bukan localhost!)
│   └── Ini mistake paling umum saat setup Docker
├── ✅ Timezone Asia/Jakarta
├── ✅ Faker locale id_ID untuk data Indonesia
├── ✅ Session & Cache pakai file (simple untuk dev)
└── ✅ Komentar penjelasan untuk DB_HOST

Copy file ini dan simpan sebagai .env:

# Dari .env.example yang sudah dimodifikasi
cp .env.example .env
# Atau langsung buat .env baru dengan content di atas

Step 3: Build dan Jalankan Containers

Sekarang moment of truth — jalankan Docker:

# Build dan start semua containers
docker-compose up -d --build

Proses pertama kali akan agak lama karena:

  • Download base images (php, nginx, mysql)
  • Build custom PHP image
  • Install PHP extensions
DOCKER BUILD PROGRESS:
├── Pulling mysql:8.0 ...
├── Pulling nginx:alpine ...
├── Building app (PHP-FPM) ...
│   ├── Installing system dependencies ...
│   ├── Installing PHP extensions ...
│   └── Copying Composer ...
├── Creating network laravel-network ...
├── Creating volume mysql-data ...
├── Starting laravel-mysql ...
├── Starting laravel-app ...
└── Starting laravel-nginx ...

Tunggu sampai selesai, lalu cek status:

# Cek status containers
docker-compose ps

Output yang diharapkan:

NAME              STATUS                   PORTS
laravel-app       Up (healthy)             9000/tcp
laravel-mysql     Up (healthy)             0.0.0.0:3306->3306/tcp
laravel-nginx     Up                       0.0.0.0:8000->80/tcp

Semua harus Up. Kalau ada yang Exit atau Restarting, cek logs:

# Cek logs kalau ada error
docker-compose logs mysql
docker-compose logs app

Step 4: Install Dependencies & Generate Key

# Install Composer dependencies (kalau belum)
docker-compose exec app composer install

# Generate application key
docker-compose exec app php artisan key:generate

# Set permission untuk storage dan cache
docker-compose exec app chmod -R 775 storage bootstrap/cache

Step 5: Test di Browser

Buka browser, akses: http://localhost:8000

EXPECTED RESULT:
├── ✅ Laravel welcome page muncul
├── ✅ Tidak ada error
└── ✅ Page load dengan cepat

Kalau muncul Laravel welcome page, selamat! Docker environment kamu sudah jalan.

CHECKPOINT

CHECKPOINT BAGIAN 3:
├── ✅ Laravel terinstall di folder project
├── ✅ File .env sudah dikonfigurasi
├── ✅ Containers running (app, webserver, mysql)
├── ✅ Laravel accessible di <http://localhost:8000>
└── ✅ App key sudah di-generate

Troubleshooting

Kalau ada masalah, ini beberapa solusi umum:

COMMON ISSUES:

❌ "Connection refused" ke MySQL
├── Cause: MySQL belum fully started
└── Solution: Tunggu 30 detik, atau restart containers
    docker-compose restart

❌ Permission denied di storage/
├── Cause: File ownership mismatch
└── Solution:
    docker-compose exec app chmod -R 775 storage bootstrap/cache
    docker-compose exec app chown -R www:www storage bootstrap/cache

❌ Port 8000 already in use
├── Cause: Ada service lain di port 8000
└── Solution: Ganti port di docker-compose.yml
    ports:
      - "8080:80"  # Ganti 8000 ke 8080

❌ "Class not found" error
├── Cause: Autoload belum di-generate
└── Solution:
    docker-compose exec app composer dump-autoload


Bagian 4: Database Migrations

Sekarang environment sudah running, saatnya kita buat struktur database. Di bagian ini kita akan generate semua migration files untuk simple e-commerce schema.

Database Schema Overview

Sebelum generate migrations, ini overview schema yang akan kita buat:

DATABASE SCHEMA:

users
├── id (bigint, PK)
├── name (string)
├── email (string, unique)
├── email_verified_at (timestamp, nullable)
├── password (string)
├── role (enum: admin, user)          ← Tambahan
├── avatar (string, nullable)          ← Tambahan
├── phone (string, nullable)           ← Tambahan
├── remember_token
└── timestamps

categories
├── id (bigint, PK)
├── name (string)
├── slug (string, unique)
├── icon (string, nullable)
└── timestamps

products
├── id (bigint, PK)
├── category_id (FK → categories)
├── name (string)
├── slug (string, unique)
├── description (text)
├── price (decimal 10,2)
├── stock (integer, default 0)
├── thumbnail (string, nullable)
├── is_active (boolean, default true)
└── timestamps

orders
├── id (bigint, PK)
├── user_id (FK → users)
├── order_number (string, unique)
├── total_amount (decimal 12,2)
├── status (enum: pending, paid, shipped, completed, cancelled)
├── notes (text, nullable)
└── timestamps

order_items
├── id (bigint, PK)
├── order_id (FK → orders, cascade delete)
├── product_id (FK → products)
├── quantity (integer)
├── price (decimal 10,2)
└── timestamps

RELATIONSHIPS:
├── Category hasMany Products
├── Product belongsTo Category
├── User hasMany Orders
├── Order belongsTo User
├── Order hasMany OrderItems
├── OrderItem belongsTo Order
└── OrderItem belongsTo Product

PROMPT 4 — All Migrations

Saya suka generate semua migrations dalam satu prompt besar. Ini lebih efisien dan memastikan konsistensi antar tables:

PROMPT KE ANTIGRAVITY:

"Buatkan semua migration files untuk simple e-commerce Laravel 12.

Tables yang dibutuhkan:

1. Migration untuk UPDATE users table (add columns):
   - role: enum('admin', 'user') dengan default 'user', setelah 'password'
   - avatar: string nullable, setelah 'role'
   - phone: string nullable dengan max 20, setelah 'avatar'

2. Migration untuk CREATE categories table:
   - id: bigIncrements
   - name: string (100)
   - slug: string (120), unique
   - icon: string (50), nullable
   - timestamps

3. Migration untuk CREATE products table:
   - id: bigIncrements
   - category_id: foreignId dengan constrained() dan cascadeOnDelete()
   - name: string (200)
   - slug: string (220), unique
   - description: text
   - price: decimal(10, 2)
   - stock: integer dengan default 0
   - thumbnail: string nullable
   - is_active: boolean dengan default true
   - timestamps
   - Index pada: category_id, is_active, created_at

4. Migration untuk CREATE orders table:
   - id: bigIncrements
   - user_id: foreignId dengan constrained() dan cascadeOnDelete()
   - order_number: string (50), unique
   - total_amount: decimal(12, 2) dengan default 0
   - status: enum('pending', 'paid', 'shipped', 'completed', 'cancelled') default 'pending'
   - notes: text nullable
   - timestamps
   - Index pada: user_id, status, order_number, created_at

5. Migration untuk CREATE order_items table:
   - id: bigIncrements
   - order_id: foreignId dengan constrained() dan cascadeOnDelete()
   - product_id: foreignId dengan constrained() dan cascadeOnDelete()
   - quantity: integer dengan default 1
   - price: decimal(10, 2)
   - timestamps
   - Index pada: order_id, product_id

Gunakan Laravel 12 best practices dan proper naming conventions.

Output: 5 migration files dengan timestamps yang berurutan"

OUTPUT:

File 1: database/migrations/2024_01_01_000001_add_columns_to_users_table.php

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->enum('role', ['admin', 'user'])->default('user')->after('password');
            $table->string('avatar')->nullable()->after('role');
            $table->string('phone', 20)->nullable()->after('avatar');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['role', 'avatar', 'phone']);
        });
    }
};

File 2: database/migrations/2024_01_01_000002_create_categories_table.php

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name', 100);
            $table->string('slug', 120)->unique();
            $table->string('icon', 50)->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

File 3: database/migrations/2024_01_01_000003_create_products_table.php

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')->constrained()->cascadeOnDelete();
            $table->string('name', 200);
            $table->string('slug', 220)->unique();
            $table->text('description');
            $table->decimal('price', 10, 2);
            $table->integer('stock')->default(0);
            $table->string('thumbnail')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            // Indexes for better query performance
            $table->index('category_id');
            $table->index('is_active');
            $table->index('created_at');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

File 4: database/migrations/2024_01_01_000004_create_orders_table.php

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('order_number', 50)->unique();
            $table->decimal('total_amount', 12, 2)->default(0);
            $table->enum('status', ['pending', 'paid', 'shipped', 'completed', 'cancelled'])->default('pending');
            $table->text('notes')->nullable();
            $table->timestamps();

            // Indexes for better query performance
            $table->index('user_id');
            $table->index('status');
            $table->index('order_number');
            $table->index('created_at');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};

File 5: database/migrations/2024_01_01_000005_create_order_items_table.php

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('order_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignId('product_id')->constrained()->cascadeOnDelete();
            $table->integer('quantity')->default(1);
            $table->decimal('price', 10, 2);
            $table->timestamps();

            // Indexes for better query performance
            $table->index('order_id');
            $table->index('product_id');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('order_items');
    }
};

REVIEW:

MIGRATIONS REVIEW:

users (update):
├── ✅ Enum role dengan default 'user'
├── ✅ Avatar dan phone nullable
└── ✅ down() method untuk rollback

categories:
├── ✅ Name dengan max length 100
├── ✅ Slug unique untuk URL-friendly
└── ✅ Icon nullable untuk emoji/icon class

products:
├── ✅ Foreign key ke categories dengan cascade delete
├── ✅ Price decimal(10,2) — max 99,999,999.99
├── ✅ Stock integer dengan default 0
├── ✅ is_active untuk soft toggle
└── ✅ Indexes untuk query optimization

orders:
├── ✅ Foreign key ke users dengan cascade delete
├── ✅ order_number unique untuk tracking
├── ✅ total_amount decimal(12,2) — max 9,999,999,999.99
├── ✅ Status enum dengan 5 options
└── ✅ Indexes untuk filtering dan sorting

order_items:
├── ✅ Foreign keys dengan cascade delete
├── ✅ Quantity dengan default 1
├── ✅ Price snapshot (harga saat order, bukan current price)
└── ✅ Indexes untuk joins

Jalankan Migrations

Save semua files, lalu jalankan migration via Docker:

# Jalankan semua migrations
docker-compose exec app php artisan migrate

Output yang diharapkan:

INFO  Running migrations.

2024_01_01_000001_add_columns_to_users_table ....... 45ms DONE
2024_01_01_000002_create_categories_table .......... 32ms DONE
2024_01_01_000003_create_products_table ............ 48ms DONE
2024_01_01_000004_create_orders_table .............. 41ms DONE
2024_01_01_000005_create_order_items_table ......... 38ms DONE

Verifikasi status:

# Cek migration status
docker-compose exec app php artisan migrate:status

CHECKPOINT

CHECKPOINT BAGIAN 4:
├── ✅ 5 migration files sudah dibuat
├── ✅ Migration berhasil dijalankan
├── ✅ Tables sudah ada di database:
│   ├── users (dengan columns tambahan)
│   ├── categories
│   ├── products
│   ├── orders
│   └── order_items
├── ✅ Foreign key constraints aktif
└── ✅ Indexes sudah dibuat

Tips: Migration Best Practices

MIGRATION BEST PRACTICES:

├── Naming Convention
│   ├── create_xxx_table → untuk table baru
│   ├── add_xxx_to_yyy_table → untuk tambah column
│   ├── remove_xxx_from_yyy_table → untuk hapus column
│   └── update_xxx_in_yyy_table → untuk modify column
│
├── Always Define down()
│   └── Rollback harus bisa jalan tanpa error
│
├── Use Constraints
│   ├── constrained() → auto-detect table name
│   ├── cascadeOnDelete() → hapus child saat parent dihapus
│   └── nullOnDelete() → set null saat parent dihapus
│
├── Add Indexes
│   ├── Foreign keys → biasanya perlu index
│   ├── Columns untuk WHERE → perlu index
│   └── Columns untuk ORDER BY → perlu index
│
└── Decimal untuk Money
    ├── Jangan pakai float untuk uang!
    ├── decimal(10, 2) → 8 digit + 2 decimal
    └── Hindari floating point precision errors


Lanjut ke Bagian 5: Models dengan Relationships →

Bagian 5: Models dengan Relationships

Database structure sudah siap, sekarang kita buat Models. Di Laravel, Model bukan cuma representasi table — tapi juga tempat untuk define relationships, scopes, accessors, dan business logic ringan.

Model Architecture Overview

MODEL ARCHITECTURE:

app/Models/
├── User.php          ← Update existing
├── Category.php      ← New
├── Product.php       ← New
├── Order.php         ← New
└── OrderItem.php     ← New

SETIAP MODEL AKAN PUNYA:
├── $fillable → Mass assignment protection
├── $casts → Type casting
├── Relationships → belongsTo, hasMany
├── Scopes → Reusable query filters
├── Accessors → Computed attributes
└── Boot method → Auto-generate slugs, order numbers

PROMPT 5 — All Models

Sama seperti migrations, saya prefer generate semua models dalam satu prompt untuk konsistensi:

PROMPT KE ANTIGRAVITY:

"Buatkan semua Model files untuk e-commerce Laravel 12 dengan relationships, scopes, dan accessors.

Models yang dibutuhkan:

1. User (UPDATE existing model):
   - Fillable: name, email, password, role, avatar, phone
   - Casts: email_verified_at (datetime), password (hashed)
   - Relationships:
     - hasMany orders
   - Scopes:
     - scopeAdmin() → where role = admin
     - scopeCustomers() → where role = user
   - Accessors:
     - avatar_url → return full URL atau default avatar

2. Category:
   - Fillable: name, slug, icon
   - Relationships:
     - hasMany products
   - Scopes:
     - scopeWithProductCount() → withCount products
   - Boot method:
     - Auto-generate slug dari name saat creating

3. Product:
   - Fillable: category_id, name, slug, description, price, stock, thumbnail, is_active
   - Casts: price (decimal:2), is_active (boolean), stock (integer)
   - Relationships:
     - belongsTo category
     - hasMany orderItems
   - Scopes:
     - scopeActive() → where is_active = true
     - scopeInStock() → where stock > 0
     - scopeByCategory($categoryId) → filter by category
     - scopePriceRange($min, $max) → filter by price range
   - Accessors:
     - formatted_price → format sebagai Rupiah (Rp 150.000)
     - thumbnail_url → return full URL atau default image
   - Boot method:
     - Auto-generate slug dari name saat creating

4. Order:
   - Fillable: user_id, order_number, total_amount, status, notes
   - Casts: total_amount (decimal:2)
   - Relationships:
     - belongsTo user
     - hasMany items (OrderItem)
   - Scopes:
     - scopeByStatus($status) → filter by status
     - scopePending() → where status = pending
     - scopePaid() → where status = paid
     - scopeCompleted() → where status = completed
   - Accessors:
     - formatted_total → format sebagai Rupiah
     - status_label → return label dengan warna (untuk badge)
   - Boot method:
     - Auto-generate order_number format: ORD-YYYYMMDD-XXXXX

5. OrderItem:
   - Fillable: order_id, product_id, quantity, price
   - Casts: price (decimal:2), quantity (integer)
   - Relationships:
     - belongsTo order
     - belongsTo product
   - Accessors:
     - subtotal → quantity * price
     - formatted_subtotal → format sebagai Rupiah

Gunakan Laravel 12 conventions, proper PHPDoc comments, dan return types.

Output: 5 model files"

OUTPUT:

File 1: app/Models/User.php

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Illuminate\\Foundation\\Auth\\User as Authenticatable;
use Illuminate\\Notifications\\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'role',
        'avatar',
        'phone',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    /**
     * Get all orders for the user.
     */
    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    /**
     * Scope to filter admin users.
     */
    public function scopeAdmin($query)
    {
        return $query->where('role', 'admin');
    }

    /**
     * Scope to filter customer users.
     */
    public function scopeCustomers($query)
    {
        return $query->where('role', 'user');
    }

    /*
    |--------------------------------------------------------------------------
    | Accessors
    |--------------------------------------------------------------------------
    */

    /**
     * Get the user's avatar URL.
     */
    public function getAvatarUrlAttribute(): string
    {
        if ($this->avatar) {
            return asset('storage/' . $this->avatar);
        }

        // Default avatar menggunakan UI Avatars
        return '<https://ui-avatars.com/api/?name=>' . urlencode($this->name) . '&background=random';
    }

    /*
    |--------------------------------------------------------------------------
    | Helper Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Check if user is admin.
     */
    public function isAdmin(): bool
    {
        return $this->role === 'admin';
    }
}

File 2: app/Models/Category.php

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Illuminate\\Support\\Str;

class Category extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'slug',
        'icon',
    ];

    /*
    |--------------------------------------------------------------------------
    | Boot Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Boot the model.
     */
    protected static function boot()
    {
        parent::boot();

        // Auto-generate slug when creating
        static::creating(function ($category) {
            if (empty($category->slug)) {
                $category->slug = Str::slug($category->name);
            }
        });

        // Auto-update slug when updating (if name changed)
        static::updating(function ($category) {
            if ($category->isDirty('name') && empty($category->slug)) {
                $category->slug = Str::slug($category->name);
            }
        });
    }

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    /**
     * Get all products for the category.
     */
    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    /**
     * Scope to include product count.
     */
    public function scopeWithProductCount($query)
    {
        return $query->withCount('products');
    }

    /**
     * Scope to get categories with active products only.
     */
    public function scopeHasActiveProducts($query)
    {
        return $query->whereHas('products', function ($q) {
            $q->where('is_active', true);
        });
    }
}

File 3: app/Models/Product.php

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Illuminate\\Support\\Str;

class Product extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'category_id',
        'name',
        'slug',
        'description',
        'price',
        'stock',
        'thumbnail',
        'is_active',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'price' => 'decimal:2',
        'stock' => 'integer',
        'is_active' => 'boolean',
    ];

    /*
    |--------------------------------------------------------------------------
    | Boot Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Boot the model.
     */
    protected static function boot()
    {
        parent::boot();

        // Auto-generate slug when creating
        static::creating(function ($product) {
            if (empty($product->slug)) {
                $product->slug = Str::slug($product->name);

                // Ensure unique slug
                $originalSlug = $product->slug;
                $count = 1;
                while (static::where('slug', $product->slug)->exists()) {
                    $product->slug = $originalSlug . '-' . $count++;
                }
            }
        });
    }

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    /**
     * Get the category that owns the product.
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * Get all order items for the product.
     */
    public function orderItems(): HasMany
    {
        return $this->hasMany(OrderItem::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    /**
     * Scope to filter active products.
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    /**
     * Scope to filter products in stock.
     */
    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }

    /**
     * Scope to filter by category.
     */
    public function scopeByCategory($query, $categoryId)
    {
        return $query->where('category_id', $categoryId);
    }

    /**
     * Scope to filter by price range.
     */
    public function scopePriceRange($query, $min, $max)
    {
        return $query->whereBetween('price', [$min, $max]);
    }

    /**
     * Scope to search products by name or description.
     */
    public function scopeSearch($query, $search)
    {
        return $query->where(function ($q) use ($search) {
            $q->where('name', 'like', "%{$search}%")
              ->orWhere('description', 'like', "%{$search}%");
        });
    }

    /*
    |--------------------------------------------------------------------------
    | Accessors
    |--------------------------------------------------------------------------
    */

    /**
     * Get formatted price in Rupiah.
     */
    public function getFormattedPriceAttribute(): string
    {
        return 'Rp ' . number_format($this->price, 0, ',', '.');
    }

    /**
     * Get thumbnail URL.
     */
    public function getThumbnailUrlAttribute(): string
    {
        if ($this->thumbnail) {
            return asset('storage/' . $this->thumbnail);
        }

        // Default product image
        return asset('images/default-product.png');
    }

    /*
    |--------------------------------------------------------------------------
    | Helper Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Check if product is available for purchase.
     */
    public function isAvailable(): bool
    {
        return $this->is_active && $this->stock > 0;
    }

    /**
     * Decrease stock after purchase.
     */
    public function decreaseStock(int $quantity): void
    {
        $this->decrement('stock', $quantity);
    }
}

File 4: app/Models/Order.php

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Illuminate\\Support\\Str;
use Carbon\\Carbon;

class Order extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'user_id',
        'order_number',
        'total_amount',
        'status',
        'notes',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'total_amount' => 'decimal:2',
    ];

    /**
     * Status constants.
     */
    const STATUS_PENDING = 'pending';
    const STATUS_PAID = 'paid';
    const STATUS_SHIPPED = 'shipped';
    const STATUS_COMPLETED = 'completed';
    const STATUS_CANCELLED = 'cancelled';

    /*
    |--------------------------------------------------------------------------
    | Boot Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Boot the model.
     */
    protected static function boot()
    {
        parent::boot();

        // Auto-generate order number when creating
        static::creating(function ($order) {
            if (empty($order->order_number)) {
                $order->order_number = self::generateOrderNumber();
            }
        });
    }

    /**
     * Generate unique order number.
     * Format: ORD-YYYYMMDD-XXXXX
     */
    public static function generateOrderNumber(): string
    {
        $date = Carbon::now()->format('Ymd');
        $random = strtoupper(Str::random(5));
        $orderNumber = "ORD-{$date}-{$random}";

        // Ensure uniqueness
        while (static::where('order_number', $orderNumber)->exists()) {
            $random = strtoupper(Str::random(5));
            $orderNumber = "ORD-{$date}-{$random}";
        }

        return $orderNumber;
    }

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    /**
     * Get the user that owns the order.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get all items for the order.
     */
    public function items(): HasMany
    {
        return $this->hasMany(OrderItem::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Scopes
    |--------------------------------------------------------------------------
    */

    /**
     * Scope to filter by status.
     */
    public function scopeByStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    /**
     * Scope to filter pending orders.
     */
    public function scopePending($query)
    {
        return $query->where('status', self::STATUS_PENDING);
    }

    /**
     * Scope to filter paid orders.
     */
    public function scopePaid($query)
    {
        return $query->where('status', self::STATUS_PAID);
    }

    /**
     * Scope to filter completed orders.
     */
    public function scopeCompleted($query)
    {
        return $query->where('status', self::STATUS_COMPLETED);
    }

    /**
     * Scope to filter cancelled orders.
     */
    public function scopeCancelled($query)
    {
        return $query->where('status', self::STATUS_CANCELLED);
    }

    /**
     * Scope to filter by date range.
     */
    public function scopeDateRange($query, $startDate, $endDate)
    {
        return $query->whereBetween('created_at', [$startDate, $endDate]);
    }

    /*
    |--------------------------------------------------------------------------
    | Accessors
    |--------------------------------------------------------------------------
    */

    /**
     * Get formatted total in Rupiah.
     */
    public function getFormattedTotalAttribute(): string
    {
        return 'Rp ' . number_format($this->total_amount, 0, ',', '.');
    }

    /**
     * Get status label with color for badge.
     */
    public function getStatusLabelAttribute(): array
    {
        $labels = [
            self::STATUS_PENDING => ['text' => 'Menunggu Pembayaran', 'color' => 'yellow'],
            self::STATUS_PAID => ['text' => 'Dibayar', 'color' => 'blue'],
            self::STATUS_SHIPPED => ['text' => 'Dikirim', 'color' => 'purple'],
            self::STATUS_COMPLETED => ['text' => 'Selesai', 'color' => 'green'],
            self::STATUS_CANCELLED => ['text' => 'Dibatalkan', 'color' => 'red'],
        ];

        return $labels[$this->status] ?? ['text' => $this->status, 'color' => 'gray'];
    }

    /*
    |--------------------------------------------------------------------------
    | Helper Methods
    |--------------------------------------------------------------------------
    */

    /**
     * Calculate and update total amount from items.
     */
    public function calculateTotal(): void
    {
        $this->total_amount = $this->items->sum(function ($item) {
            return $item->quantity * $item->price;
        });
        $this->save();
    }

    /**
     * Check if order can be cancelled.
     */
    public function canBeCancelled(): bool
    {
        return in_array($this->status, [self::STATUS_PENDING, self::STATUS_PAID]);
    }

    /**
     * Mark order as paid.
     */
    public function markAsPaid(): void
    {
        $this->update(['status' => self::STATUS_PAID]);
    }
}

File 5: app/Models/OrderItem.php

<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;

class OrderItem extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'order_id',
        'product_id',
        'quantity',
        'price',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'price' => 'decimal:2',
        'quantity' => 'integer',
    ];

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    */

    /**
     * Get the order that owns the item.
     */
    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * Get the product for this item.
     */
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    /*
    |--------------------------------------------------------------------------
    | Accessors
    |--------------------------------------------------------------------------
    */

    /**
     * Get subtotal (quantity * price).
     */
    public function getSubtotalAttribute(): float
    {
        return $this->quantity * $this->price;
    }

    /**
     * Get formatted subtotal in Rupiah.
     */
    public function getFormattedSubtotalAttribute(): string
    {
        return 'Rp ' . number_format($this->subtotal, 0, ',', '.');
    }

    /**
     * Get formatted price in Rupiah.
     */
    public function getFormattedPriceAttribute(): string
    {
        return 'Rp ' . number_format($this->price, 0, ',', '.');
    }
}

REVIEW:

MODELS REVIEW:

User:
├── ✅ Fillable includes new columns
├── ✅ Password auto-hashed via cast
├── ✅ hasMany orders relationship
├── ✅ Scopes: admin(), customers()
├── ✅ avatar_url accessor dengan fallback
└── ✅ isAdmin() helper method

Category:
├── ✅ Auto-generate slug di boot()
├── ✅ hasMany products relationship
├── ✅ withProductCount scope
└── ✅ hasActiveProducts scope bonus

Product:
├── ✅ Proper casts untuk price, stock, is_active
├── ✅ Auto-generate unique slug
├── ✅ belongsTo category, hasMany orderItems
├── ✅ Scopes: active, inStock, byCategory, priceRange, search
├── ✅ formatted_price accessor (Rupiah format)
├── ✅ isAvailable() dan decreaseStock() helpers
└── ✅ thumbnail_url dengan fallback

Order:
├── ✅ Status constants untuk consistency
├── ✅ Auto-generate order_number (ORD-YYYYMMDD-XXXXX)
├── ✅ belongsTo user, hasMany items
├── ✅ Scopes untuk semua status
├── ✅ status_label accessor dengan warna
├── ✅ calculateTotal() helper
└── ✅ canBeCancelled(), markAsPaid() helpers

OrderItem:
├── ✅ belongsTo order dan product
├── ✅ subtotal accessor (computed)
└── ✅ formatted accessors untuk display

Test Models di Tinker

Sebelum lanjut, kita test models via Tinker:

# Buka Tinker
docker-compose exec app php artisan tinker

// Test Category model
>>> use App\\Models\\Category;
>>> Category::create(['name' => 'Test Category']);
// Harusnya slug auto-generated

// Test Product scopes
>>> use App\\Models\\Product;
>>> Product::active()->inStock()->get();

// Test User scopes
>>> use App\\Models\\User;
>>> User::admin()->get();
>>> User::customers()->get();

// Exit tinker
>>> exit

CHECKPOINT

CHECKPOINT BAGIAN 5:
├── ✅ 5 model files sudah dibuat
├── ✅ Semua relationships defined
├── ✅ Scopes untuk filtering
├── ✅ Accessors untuk formatting
├── ✅ Boot methods untuk auto-generate
└── ✅ Helper methods untuk business logic


Bagian 6: Seeders dengan Data Realistic

Sekarang bagian yang seru — mengisi database dengan data realistic. Data yang bagus bikin development dan testing jauh lebih mudah karena kita bisa lihat hasil yang mirip production.

Seeding Strategy

SEEDING STRATEGY:

├── Factories
│   └── Template untuk generate fake data
│   └── Bisa dipanggil berkali-kali
│
├── Seeders
│   └── Script yang jalankan factories
│   └── Bisa include data statis juga
│
└── Urutan Penting!
    ├── 1. Users (tidak depend ke siapa-siapa)
    ├── 2. Categories (tidak depend ke siapa-siapa)
    ├── 3. Products (depend ke Categories)
    ├── 4. Orders (depend ke Users)
    └── 5. OrderItems (depend ke Orders & Products)

PROMPT 6 — Factories

PROMPT KE ANTIGRAVITY:

"Buatkan Factory files untuk semua models dengan data realistic Indonesia.

Factories yang dibutuhkan:

1. UserFactory (UPDATE existing):
   - name: nama Indonesia realistic (pakai array nama umum)
   - email: unique, format [email protected]
   - password: default 'password'
   - role: 90% 'user', 10% 'admin'
   - avatar: null (untuk simplicity)
   - phone: format Indonesia 08xx-xxxx-xxxx
   - email_verified_at: 80% verified, 20% null

2. CategoryFactory:
   - Categories statis dari array (akan di-handle di seeder)
   - Factory simple untuk testing

3. ProductFactory:
   - name: nama produk Indonesia realistic
   - description: 2-3 kalimat bahasa Indonesia tentang produk
   - price: range 15000 - 2500000 (realistic untuk e-commerce)
   - stock: range 0-100, dengan 10% chance stock 0
   - is_active: 90% true, 10% false
   - thumbnail: null

4. OrderFactory:
   - order_number: akan auto-generate dari model
   - total_amount: 0 (akan di-calculate dari items)
   - status: weighted - 40% pending, 30% paid, 15% completed, 10% shipped, 5% cancelled
   - notes: 30% chance ada catatan bahasa Indonesia

Gunakan fake data yang natural untuk bahasa Indonesia.

Output: 4 factory files"

OUTPUT:

File 1: database/factories/UserFactory.php

<?php

namespace Database\\Factories;

use Illuminate\\Database\\Eloquent\\Factories\\Factory;
use Illuminate\\Support\\Facades\\Hash;
use Illuminate\\Support\\Str;

/**
 * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\App\\Models\\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Indonesian first names.
     */
    protected array $firstNames = [
        'Ahmad', 'Budi', 'Citra', 'Dewi', 'Eko', 'Fitri', 'Galih', 'Hendra',
        'Indra', 'Joko', 'Kartika', 'Lina', 'Muhammad', 'Nina', 'Omar', 'Putri',
        'Qori', 'Rina', 'Sandi', 'Tina', 'Umar', 'Vina', 'Wahyu', 'Yanti', 'Zahra',
        'Agus', 'Bambang', 'Cahya', 'Dian', 'Eka', 'Farhan', 'Gita', 'Hadi',
        'Irfan', 'Jihan', 'Kiki', 'Laras', 'Mega', 'Nanda', 'Okta', 'Pandu',
        'Rizky', 'Sinta', 'Taufik', 'Umi', 'Vero', 'Wulan', 'Yoga', 'Zaki'
    ];

    /**
     * Indonesian last names.
     */
    protected array $lastNames = [
        'Pratama', 'Wijaya', 'Kusuma', 'Saputra', 'Hidayat', 'Santoso', 'Nugraha',
        'Ramadhan', 'Permana', 'Putra', 'Sari', 'Wati', 'Ningrum', 'Lestari',
        'Rahayu', 'Handayani', 'Setiawan', 'Susanto', 'Suryadi', 'Hartono',
        'Wibowo', 'Utomo', 'Firmansyah', 'Kurniawan', 'Hakim', 'Prasetyo'
    ];

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $firstName = fake()->randomElement($this->firstNames);
        $lastName = fake()->randomElement($this->lastNames);
        $fullName = $firstName . ' ' . $lastName;

        return [
            'name' => $fullName,
            'email' => strtolower($firstName) . '.' . strtolower(Str::random(4)) . '@example.com',
            'email_verified_at' => fake()->boolean(80) ? now() : null,
            'password' => static::$password ??= Hash::make('password'),
            'role' => fake()->boolean(10) ? 'admin' : 'user',
            'avatar' => null,
            'phone' => $this->generateIndonesianPhone(),
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * Generate Indonesian phone number.
     */
    protected function generateIndonesianPhone(): string
    {
        $prefixes = ['0812', '0813', '0821', '0822', '0851', '0852', '0853', '0857', '0858'];
        $prefix = fake()->randomElement($prefixes);
        $number = fake()->numerify('########');

        return $prefix . $number;
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }

    /**
     * Indicate that the user is an admin.
     */
    public function admin(): static
    {
        return $this->state(fn (array $attributes) => [
            'role' => 'admin',
        ]);
    }

    /**
     * Indicate that the user is a regular customer.
     */
    public function customer(): static
    {
        return $this->state(fn (array $attributes) => [
            'role' => 'user',
        ]);
    }
}

File 2: database/factories/CategoryFactory.php

<?php

namespace Database\\Factories;

use Illuminate\\Database\\Eloquent\\Factories\\Factory;
use Illuminate\\Support\\Str;

/**
 * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\App\\Models\\Category>
 */
class CategoryFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $name = fake()->unique()->word();

        return [
            'name' => ucfirst($name),
            'slug' => Str::slug($name),
            'icon' => fake()->randomElement(['📱', '👕', '🍔', '💊', '⚽', '📚', '🎮', '🏠']),
        ];
    }
}

File 3: database/factories/ProductFactory.php

<?php

namespace Database\\Factories;

use App\\Models\\Category;
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
use Illuminate\\Support\\Str;

/**
 * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\App\\Models\\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Product name templates by category.
     */
    protected array $productTemplates = [
        'Elektronik' => [
            'Smartphone %s Pro Max', 'Laptop %s Series', 'Tablet %s Lite',
            'Earbuds %s Wireless', 'Smartwatch %s Edition', 'Powerbank %s mAh',
            'Speaker Bluetooth %s', 'Charger Fast %s Watt'
        ],
        'Fashion Pria' => [
            'Kemeja %s Slim Fit', 'Kaos Polos %s', 'Celana Chino %s',
            'Jaket %s Waterproof', 'Sepatu Sneakers %s', 'Topi Baseball %s',
            'Dompet Kulit %s', 'Jam Tangan %s Classic'
        ],
        'Fashion Wanita' => [
            'Dress %s Elegant', 'Blouse %s Modern', 'Rok %s A-Line',
            'Cardigan %s Knit', 'Tas Selempang %s', 'Sepatu Heels %s',
            'Hijab %s Premium', 'Aksesoris %s Set'
        ],
        'Makanan & Minuman' => [
            'Kopi %s Arabica 500g', 'Teh %s Premium', 'Cokelat %s Dark',
            'Snack %s Crispy', 'Madu %s Murni', 'Kurma %s Import',
            'Susu %s UHT 1L', 'Keripik %s Pedas'
        ],
        'Kesehatan' => [
            'Vitamin %s Complex', 'Masker %s 3 Ply', 'Hand Sanitizer %s',
            'Minyak Kayu Putih %s', 'Obat Herbal %s', 'Suplemen %s',
            'Thermometer %s Digital', 'Tensimeter %s'
        ],
        'Hobi & Olahraga' => [
            'Sepeda %s Mountain', 'Raket %s Pro', 'Bola %s Official',
            'Matras Yoga %s', 'Dumbbell %s Set', 'Jersey %s Original',
            'Sepatu Lari %s', 'Tas Olahraga %s'
        ],
    ];

    /**
     * Description templates in Indonesian.
     */
    protected array $descriptions = [
        'Produk berkualitas tinggi dengan bahan premium yang nyaman digunakan sehari-hari. Cocok untuk berbagai kesempatan dan aktivitas.',
        'Dibuat dengan teknologi terbaru untuk memberikan performa maksimal. Desain modern dan elegan yang cocok untuk gaya hidup aktif.',
        'Produk pilihan dengan kualitas terjamin dan harga terjangkau. Tersedia dalam berbagai pilihan warna dan ukuran.',
        'Hadir dengan fitur unggulan yang memudahkan aktivitas kamu. Ringan, praktis, dan tahan lama untuk penggunaan jangka panjang.',
        'Produk original dengan garansi resmi. Kualitas premium dengan harga yang kompetitif di kelasnya.',
    ];

    /**
     * Brand names for products.
     */
    protected array $brands = [
        'Aurora', 'Bintang', 'Cakra', 'Dewa', 'Elang', 'Fortuna', 'Garuda',
        'Harmoni', 'Indah', 'Jaya', 'Kartika', 'Lestari', 'Mandiri', 'Nusantara'
    ];

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $brand = fake()->randomElement($this->brands);
        $name = sprintf(
            fake()->randomElement($this->productTemplates['Elektronik']),
            $brand
        );

        return [
            'category_id' => Category::factory(),
            'name' => $name,
            'slug' => Str::slug($name) . '-' . Str::random(4),
            'description' => fake()->randomElement($this->descriptions),
            'price' => $this->generateRealisticPrice(),
            'stock' => fake()->boolean(90) ? fake()->numberBetween(5, 100) : 0,
            'thumbnail' => null,
            'is_active' => fake()->boolean(90),
        ];
    }

    /**
     * Generate realistic Indonesian price.
     */
    protected function generateRealisticPrice(): int
    {
        $priceRanges = [
            [15000, 50000],      // Low price items
            [50000, 150000],    // Medium-low
            [150000, 500000],   // Medium
            [500000, 1500000],  // Medium-high
            [1500000, 2500000], // High
        ];

        $range = fake()->randomElement($priceRanges);
        $price = fake()->numberBetween($range[0], $range[1]);

        // Round to nearest 1000
        return round($price / 1000) * 1000;
    }

    /**
     * Create product for specific category.
     */
    public function forCategory(Category $category): static
    {
        $templates = $this->productTemplates[$category->name] ?? $this->productTemplates['Elektronik'];
        $brand = fake()->randomElement($this->brands);
        $name = sprintf(fake()->randomElement($templates), $brand);

        return $this->state(fn (array $attributes) => [
            'category_id' => $category->id,
            'name' => $name,
            'slug' => Str::slug($name) . '-' . Str::random(4),
        ]);
    }
}

File 4: database/factories/OrderFactory.php

<?php

namespace Database\\Factories;

use App\\Models\\User;
use App\\Models\\Order;
use Illuminate\\Database\\Eloquent\\Factories\\Factory;

/**
 * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\App\\Models\\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Indonesian order notes.
     */
    protected array $notes = [
        'Tolong packing yang rapi ya',
        'Minta bubble wrap extra',
        'Kirim pagi hari kalau bisa',
        'Jangan dilipat, gulung saja',
        'Sekalian minta nota ya',
        'Kalau bisa kirim hari ini',
        'Packing pakai kardus yang kuat',
        'Minta free gift kalau ada',
    ];

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'order_number' => null, // Will be auto-generated by model
            'total_amount' => 0, // Will be calculated from items
            'status' => $this->getWeightedStatus(),
            'notes' => fake()->boolean(30) ? fake()->randomElement($this->notes) : null,
            'created_at' => fake()->dateTimeBetween('-3 months', 'now'),
        ];
    }

    /**
     * Get weighted random status.
     */
    protected function getWeightedStatus(): string
    {
        $statuses = [
            Order::STATUS_PENDING => 40,
            Order::STATUS_PAID => 30,
            Order::STATUS_COMPLETED => 15,
            Order::STATUS_SHIPPED => 10,
            Order::STATUS_CANCELLED => 5,
        ];

        $rand = fake()->numberBetween(1, 100);
        $cumulative = 0;

        foreach ($statuses as $status => $weight) {
            $cumulative += $weight;
            if ($rand <= $cumulative) {
                return $status;
            }
        }

        return Order::STATUS_PENDING;
    }

    /**
     * Set order as pending.
     */
    public function pending(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => Order::STATUS_PENDING,
        ]);
    }

    /**
     * Set order as paid.
     */
    public function paid(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => Order::STATUS_PAID,
        ]);
    }

    /**
     * Set order as completed.
     */
    public function completed(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => Order::STATUS_COMPLETED,
        ]);
    }
}


PROMPT 7 — Seeders

PROMPT KE ANTIGRAVITY:

"Buatkan Seeder files dengan data realistic Indonesia.

Seeders yang dibutuhkan:

1. UserSeeder:
   - Buat 1 admin dengan data fixed:
     - name: Admin BuildWithAngga
     - email: [email protected]
     - password: password
     - role: admin
   - Buat 15 random customers menggunakan factory

2. CategorySeeder:
   - 6 kategori dengan data statis:
     - Elektronik (icon: 📱)
     - Fashion Pria (icon: 👔)
     - Fashion Wanita (icon: 👗)
     - Makanan & Minuman (icon: 🍔)
     - Kesehatan (icon: 💊)
     - Hobi & Olahraga (icon: ⚽)

3. ProductSeeder:
   - Loop setiap category
   - Buat 6-8 produk per kategori menggunakan factory
   - Total ~40 produk

4. OrderSeeder:
   - Ambil random customers (bukan admin)
   - Buat 25 orders
   - Setiap order punya 1-4 items random
   - Calculate total_amount dari items
   - Gunakan transaksi untuk data integrity

5. DatabaseSeeder:
   - Call semua seeders dengan urutan yang benar
   - Tampilkan output progress

Output: 5 seeder files"

OUTPUT:

File 1: database/seeders/UserSeeder.php

<?php

namespace Database\\Seeders;

use App\\Models\\User;
use Illuminate\\Database\\Seeder;
use Illuminate\\Support\\Facades\\Hash;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Create admin user
        User::create([
            'name' => 'Admin BuildWithAngga',
            'email' => '[email protected]',
            'email_verified_at' => now(),
            'password' => Hash::make('password'),
            'role' => 'admin',
            'phone' => '081234567890',
        ]);

        $this->command->info('✅ Admin user created: [email protected]');

        // Create 15 random customers
        User::factory()
            ->count(15)
            ->customer()
            ->create();

        $this->command->info('✅ 15 customer users created');
    }
}

File 2: database/seeders/CategorySeeder.php

<?php

namespace Database\\Seeders;

use App\\Models\\Category;
use Illuminate\\Database\\Seeder;

class CategorySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $categories = [
            ['name' => 'Elektronik', 'icon' => '📱'],
            ['name' => 'Fashion Pria', 'icon' => '👔'],
            ['name' => 'Fashion Wanita', 'icon' => '👗'],
            ['name' => 'Makanan & Minuman', 'icon' => '🍔'],
            ['name' => 'Kesehatan', 'icon' => '💊'],
            ['name' => 'Hobi & Olahraga', 'icon' => '⚽'],
        ];

        foreach ($categories as $category) {
            Category::create($category);
        }

        $this->command->info('✅ 6 categories created');
    }
}

File 3: database/seeders/ProductSeeder.php

<?php

namespace Database\\Seeders;

use App\\Models\\Category;
use App\\Models\\Product;
use Illuminate\\Database\\Seeder;
use Illuminate\\Support\\Str;

class ProductSeeder extends Seeder
{
    /**
     * Product data by category.
     */
    protected array $products = [
        'Elektronik' => [
            ['name' => 'Smartphone Aurora Pro Max', 'price' => 4500000],
            ['name' => 'Laptop Garuda Series 15', 'price' => 8500000],
            ['name' => 'Earbuds Nusantara Wireless', 'price' => 350000],
            ['name' => 'Smartwatch Bintang Edition', 'price' => 1250000],
            ['name' => 'Powerbank Fortuna 20000mAh', 'price' => 275000],
            ['name' => 'Speaker Bluetooth Harmoni', 'price' => 450000],
            ['name' => 'Charger Fast Elang 65W', 'price' => 185000],
        ],
        'Fashion Pria' => [
            ['name' => 'Kemeja Batik Cakra Premium', 'price' => 285000],
            ['name' => 'Kaos Polos Mandiri Cotton', 'price' => 89000],
            ['name' => 'Celana Chino Jaya Slim Fit', 'price' => 225000],
            ['name' => 'Jaket Denim Lestari Classic', 'price' => 375000],
            ['name' => 'Sepatu Sneakers Kartika', 'price' => 485000],
            ['name' => 'Dompet Kulit Indah Genuine', 'price' => 195000],
        ],
        'Fashion Wanita' => [
            ['name' => 'Dress Aurora Elegant Silk', 'price' => 425000],
            ['name' => 'Blouse Citra Modern Cut', 'price' => 175000],
            ['name' => 'Rok Midi Dewi A-Line', 'price' => 198000],
            ['name' => 'Cardigan Lestari Knit', 'price' => 245000],
            ['name' => 'Tas Selempang Kartika Mini', 'price' => 325000],
            ['name' => 'Hijab Voal Nusantara Premium', 'price' => 125000],
            ['name' => 'Sepatu Heels Fortuna 7cm', 'price' => 295000],
        ],
        'Makanan & Minuman' => [
            ['name' => 'Kopi Toraja Arabica 500g', 'price' => 125000],
            ['name' => 'Teh Pucuk Harum Premium', 'price' => 85000],
            ['name' => 'Cokelat Monggo Dark 70%', 'price' => 65000],
            ['name' => 'Madu Hutan Kalimantan 650ml', 'price' => 175000],
            ['name' => 'Kurma Ajwa Madinah 500g', 'price' => 225000],
            ['name' => 'Keripik Singkong Pedas Manis', 'price' => 35000],
        ],
        'Kesehatan' => [
            ['name' => 'Vitamin C 1000mg Isi 30', 'price' => 125000],
            ['name' => 'Masker Medis 3 Ply Box 50', 'price' => 75000],
            ['name' => 'Hand Sanitizer Antis 500ml', 'price' => 45000],
            ['name' => 'Minyak Kayu Putih Cap Lang', 'price' => 35000],
            ['name' => 'Tolak Angin Cair Isi 12', 'price' => 48000],
            ['name' => 'Thermometer Digital Omron', 'price' => 185000],
        ],
        'Hobi & Olahraga' => [
            ['name' => 'Raket Badminton Yonex Pro', 'price' => 850000],
            ['name' => 'Bola Sepak Nike Official', 'price' => 425000],
            ['name' => 'Matras Yoga TPE 6mm', 'price' => 175000],
            ['name' => 'Dumbbell Set 20kg Kettler', 'price' => 650000],
            ['name' => 'Jersey Timnas Indonesia 2024', 'price' => 385000],
            ['name' => 'Sepatu Lari Adidas Duramo', 'price' => 725000],
            ['name' => 'Tas Gym Under Armour', 'price' => 475000],
        ],
    ];

    /**
     * Product descriptions.
     */
    protected array $descriptions = [
        'Produk berkualitas tinggi dengan bahan premium yang nyaman digunakan sehari-hari. Cocok untuk berbagai kesempatan dan aktivitas. Garansi resmi dan original 100%.',
        'Dibuat dengan teknologi terbaru untuk memberikan performa maksimal. Desain modern dan elegan yang cocok untuk gaya hidup aktif. Tersedia layanan after-sales terbaik.',
        'Produk pilihan dengan kualitas terjamin dan harga terjangkau. Tersedia dalam berbagai pilihan warna dan ukuran. Pengiriman cepat ke seluruh Indonesia.',
        'Hadir dengan fitur unggulan yang memudahkan aktivitas kamu. Ringan, praktis, dan tahan lama untuk penggunaan jangka panjang. Best seller di kategorinya.',
        'Produk original dengan garansi resmi. Kualitas premium dengan harga yang kompetitif di kelasnya. Sudah dipercaya ribuan pelanggan di Indonesia.',
    ];

    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $categories = Category::all();
        $totalProducts = 0;

        foreach ($categories as $category) {
            $products = $this->products[$category->name] ?? [];

            foreach ($products as $productData) {
                Product::create([
                    'category_id' => $category->id,
                    'name' => $productData['name'],
                    'slug' => Str::slug($productData['name']),
                    'description' => fake()->randomElement($this->descriptions),
                    'price' => $productData['price'],
                    'stock' => fake()->numberBetween(10, 100),
                    'is_active' => true,
                ]);
                $totalProducts++;
            }
        }

        $this->command->info("✅ {$totalProducts} products created");
    }
}

File 4: database/seeders/OrderSeeder.php

<?php

namespace Database\\Seeders;

use App\\Models\\Order;
use App\\Models\\OrderItem;
use App\\Models\\Product;
use App\\Models\\User;
use Illuminate\\Database\\Seeder;
use Illuminate\\Support\\Facades\\DB;

class OrderSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Get customers only (not admin)
        $customers = User::where('role', 'user')->get();

        // Get all active products
        $products = Product::where('is_active', true)->get();

        if ($customers->isEmpty() || $products->isEmpty()) {
            $this->command->error('❌ No customers or products found. Run UserSeeder and ProductSeeder first.');
            return;
        }

        $totalOrders = 25;

        DB::transaction(function () use ($customers, $products, $totalOrders) {
            for ($i = 0; $i < $totalOrders; $i++) {
                // Create order for random customer
                $order = Order::create([
                    'user_id' => $customers->random()->id,
                    'status' => $this->getRandomStatus(),
                    'notes' => $this->getRandomNote(),
                    'created_at' => fake()->dateTimeBetween('-3 months', 'now'),
                ]);

                // Add 1-4 random items to order
                $itemCount = fake()->numberBetween(1, 4);
                $orderProducts = $products->random($itemCount);
                $totalAmount = 0;

                foreach ($orderProducts as $product) {
                    $quantity = fake()->numberBetween(1, 3);
                    $price = $product->price;

                    OrderItem::create([
                        'order_id' => $order->id,
                        'product_id' => $product->id,
                        'quantity' => $quantity,
                        'price' => $price,
                    ]);

                    $totalAmount += ($quantity * $price);
                }

                // Update order total
                $order->update(['total_amount' => $totalAmount]);
            }
        });

        $this->command->info("✅ {$totalOrders} orders created with items");
    }

    /**
     * Get random weighted status.
     */
    protected function getRandomStatus(): string
    {
        $statuses = [
            'pending' => 40,
            'paid' => 30,
            'shipped' => 10,
            'completed' => 15,
            'cancelled' => 5,
        ];

        $rand = rand(1, 100);
        $cumulative = 0;

        foreach ($statuses as $status => $weight) {
            $cumulative += $weight;
            if ($rand <= $cumulative) {
                return $status;
            }
        }

        return 'pending';
    }

    /**
     * Get random note (30% chance).
     */
    protected function getRandomNote(): ?string
    {
        if (!fake()->boolean(30)) {
            return null;
        }

        $notes = [
            'Tolong packing yang rapi ya kak',
            'Minta bubble wrap extra dong',
            'Kirim pagi hari kalau bisa',
            'Jangan dilipat ya, digulung aja',
            'Sekalian minta nota/invoice',
            'Kalau bisa kirim hari ini',
            'Packing pakai kardus yang kuat',
            'Warna sesuai foto ya kak',
            'Size pastikan sesuai pesanan',
            'Terima kasih!',
        ];

        return fake()->randomElement($notes);
    }
}

File 5: database/seeders/DatabaseSeeder.php

<?php

namespace Database\\Seeders;

use Illuminate\\Database\\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->command->info('🌱 Starting database seeding...');
        $this->command->newLine();

        // Urutan penting karena foreign key dependencies
        $this->call([
            UserSeeder::class,      // 1. Users first (no dependencies)
            CategorySeeder::class,  // 2. Categories (no dependencies)
            ProductSeeder::class,   // 3. Products (depends on categories)
            OrderSeeder::class,     // 4. Orders & items (depends on users & products)
        ]);

        $this->command->newLine();
        $this->command->info('✅ Database seeding completed!');
        $this->command->newLine();

        // Summary
        $this->command->table(
            ['Table', 'Records'],
            [
                ['users', \\App\\Models\\User::count()],
                ['categories', \\App\\Models\\Category::count()],
                ['products', \\App\\Models\\Product::count()],
                ['orders', \\App\\Models\\Order::count()],
                ['order_items', \\App\\Models\\OrderItem::count()],
            ]
        );
    }
}

REVIEW:

SEEDERS REVIEW:

UserSeeder:
├── ✅ 1 admin dengan credentials fixed
├── ✅ 15 random customers via factory
└── ✅ Progress output

CategorySeeder:
├── ✅ 6 kategori statis dengan emoji icons
└── ✅ Slug auto-generated

ProductSeeder:
├── ✅ Products realistic per kategori
├── ✅ Nama produk Indonesia (brand lokal style)
├── ✅ Harga realistic (ribuan rupiah)
├── ✅ ~40 produk total
└── ✅ Deskripsi bahasa Indonesia

OrderSeeder:
├── ✅ 25 orders dari random customers
├── ✅ 1-4 items per order
├── ✅ Total calculated dari items
├── ✅ Weighted status distribution
├── ✅ Notes bahasa Indonesia (30% chance)
└── ✅ DB transaction untuk data integrity

DatabaseSeeder:
├── ✅ Urutan seeding benar (dependencies)
├── ✅ Progress output di terminal
└── ✅ Summary table di akhir

Jalankan Seeders

# Fresh migration + seed (hapus semua data lama)
docker-compose exec app php artisan migrate:fresh --seed

Output yang diharapkan:

Dropping all tables ...
Migration table created successfully.
Running migrations...
...

🌱 Starting database seeding...

✅ Admin user created: [email protected]
✅ 15 customer users created
✅ 6 categories created
✅ 40 products created
✅ 25 orders created with items

✅ Database seeding completed!

+-------------+---------+
| Table       | Records |
+-------------+---------+
| users       | 16      |
| categories  | 6       |
| products    | 40      |
| orders      | 25      |
| order_items | 65      |
+-------------+---------+

CHECKPOINT

CHECKPOINT BAGIAN 6:
├── ✅ 4 factory files dibuat
├── ✅ 5 seeder files dibuat
├── ✅ Seeding berhasil tanpa error
├── ✅ Data realistic Indonesia
│   ├── Nama orang Indonesia
│   ├── Nomor HP format 08xx
│   ├── Nama produk lokal
│   ├── Harga dalam Rupiah
│   └── Notes bahasa Indonesia
└── ✅ Summary table menunjukkan jumlah records


Lanjut ke Bagian 7: Verifikasi Data di TablePlus →

Bagian 7: Verifikasi Data di TablePlus

Data sudah di-seed, sekarang saatnya verifikasi. Kita akan pakai TablePlus untuk melihat data secara visual dan memastikan semuanya benar — relationships, foreign keys, dan data integrity.

Kenapa TablePlus?

KENAPA TABLEPLUS:

├── Visual Interface
│   └── Lebih mudah lihat data dalam bentuk tabel
│   └── Gak perlu nulis query untuk explore
│
├── Quick Edit
│   └── Bisa edit data langsung di GUI
│   └── Bagus untuk testing
│
├── Query Editor
│   └── Bisa jalankan custom SQL
│   └── Syntax highlighting
│
└── Multiple Connections
    └── Bisa connect ke banyak database
    └── Cocok untuk manage multiple projects

Kalau kamu belum punya TablePlus, download di tableplus.com. Ada versi gratis dengan limitasi (2 tabs, 2 connections) yang cukup untuk development.

Setup Koneksi ke MySQL Docker

Buka TablePlus, klik "Create a new connection", pilih MySQL.

CONNECTION SETTINGS:

┌─────────────────────────────────────────┐
│  Create a new connection                │
├─────────────────────────────────────────┤
│  Name:     Laravel Docker               │
│  Host:     127.0.0.1                    │
│  Port:     3306                         │
│  User:     laravel                      │
│  Password: secret                       │
│  Database: laravel_docker               │
└─────────────────────────────────────────┘

PENTING:
├── Host pakai 127.0.0.1 atau localhost
│   └── BUKAN 'mysql' seperti di .env Laravel
│   └── Karena TablePlus connect dari LUAR Docker
│
├── Port 3306
│   └── Sesuai mapping di docker-compose.yml
│
└── Credentials sesuai .env
    └── DB_USERNAME dan DB_PASSWORD

Klik "Test" untuk memastikan koneksi berhasil, lalu "Connect".

Verifikasi Tables

Setelah connected, kamu akan lihat daftar tables di sidebar kiri:

TABLES LIST:

├── cache
├── cache_locks
├── categories        ← Kita punya
├── failed_jobs
├── job_batches
├── jobs
├── migrations
├── order_items       ← Kita punya
├── orders            ← Kita punya
├── password_reset_tokens
├── products          ← Kita punya
├── sessions
└── users             ← Kita punya (updated)

Cek Data Users

Klik table users, kamu akan lihat:

EXPECTED DATA - USERS:

┌────┬─────────────────────────┬───────────────────────────────┬───────┬────────────────┐
│ id │ name                    │ email                         │ role  │ phone          │
├────┼─────────────────────────┼───────────────────────────────┼───────┼────────────────┤
│ 1  │ Admin BuildWithAngga    │ [email protected]      │ admin │ 081234567890   │
│ 2  │ Ahmad Pratama           │ [email protected]        │ user  │ 081298765432   │
│ 3  │ Dewi Kusuma             │ [email protected]         │ user  │ 085312345678   │
│ 4  │ Budi Santoso            │ [email protected]         │ user  │ 082187654321   │
│ ...│ ...                     │ ...                           │ ...   │ ...            │
└────┴─────────────────────────┴───────────────────────────────┴───────┴────────────────┘

VERIFIKASI:
├── ✅ Total 16 records (1 admin + 15 customers)
├── ✅ Admin dengan email [email protected]
├── ✅ Role column populated (admin/user)
├── ✅ Phone format Indonesia (08xx)
└── ✅ Nama Indonesia realistic

Cek Data Categories

Klik table categories:

EXPECTED DATA - CATEGORIES:

┌────┬───────────────────┬───────────────────┬──────┐
│ id │ name              │ slug              │ icon │
├────┼───────────────────┼───────────────────┼──────┤
│ 1  │ Elektronik        │ elektronik        │ 📱   │
│ 2  │ Fashion Pria      │ fashion-pria      │ 👔   │
│ 3  │ Fashion Wanita    │ fashion-wanita    │ 👗   │
│ 4  │ Makanan & Minuman │ makanan-minuman   │ 🍔   │
│ 5  │ Kesehatan         │ kesehatan         │ 💊   │
│ 6  │ Hobi & Olahraga   │ hobi-olahraga     │ ⚽   │
└────┴───────────────────┴───────────────────┴──────┘

VERIFIKASI:
├── ✅ 6 categories sesuai seeder
├── ✅ Slug auto-generated dengan benar
└── ✅ Icon emoji tersimpan

Cek Data Products

Klik table products:

EXPECTED DATA - PRODUCTS (sample):

┌────┬─────────┬────────────────────────────────┬───────────┬───────┬───────────┐
│ id │ cat_id  │ name                           │ price     │ stock │ is_active │
├────┼─────────┼────────────────────────────────┼───────────┼───────┼───────────┤
│ 1  │ 1       │ Smartphone Aurora Pro Max      │ 4500000   │ 45    │ 1         │
│ 2  │ 1       │ Laptop Garuda Series 15        │ 8500000   │ 23    │ 1         │
│ 3  │ 1       │ Earbuds Nusantara Wireless     │ 350000    │ 78    │ 1         │
│ ...│ ...     │ ...                            │ ...       │ ...   │ ...       │
│ 38 │ 6       │ Sepatu Lari Adidas Duramo      │ 725000    │ 34    │ 1         │
│ 39 │ 6       │ Tas Gym Under Armour           │ 475000    │ 56    │ 1         │
└────┴─────────┴────────────────────────────────┴───────────┴───────┴───────────┘

VERIFIKASI:
├── ✅ ~40 products total
├── ✅ category_id valid (1-6)
├── ✅ Nama produk Indonesia realistic
├── ✅ Harga dalam Rupiah (tanpa decimal)
├── ✅ Stock terisi random
└── ✅ is_active mostly true

Cek Data Orders

Klik table orders:

EXPECTED DATA - ORDERS (sample):

┌────┬─────────┬─────────────────────┬──────────────┬───────────┬─────────────────────────┐
│ id │ user_id │ order_number        │ total_amount │ status    │ notes                   │
├────┼─────────┼─────────────────────┼──────────────┼───────────┼─────────────────────────┤
│ 1  │ 5       │ ORD-20241215-X7K2M  │ 1250000      │ pending   │ Tolong packing rapi ya  │
│ 2  │ 3       │ ORD-20241218-P3N9Q  │ 4850000      │ paid      │ NULL                    │
│ 3  │ 8       │ ORD-20241220-M1K4R  │ 725000       │ completed │ Kirim pagi hari         │
│ ...│ ...     │ ...                 │ ...          │ ...       │ ...                     │
└────┴─────────┴─────────────────────┴──────────────┴───────────┴─────────────────────────┘

VERIFIKASI:
├── ✅ 25 orders total
├── ✅ user_id valid (bukan admin, cuma customers)
├── ✅ order_number format ORD-YYYYMMDD-XXXXX
├── ✅ total_amount calculated dari items
├── ✅ Status varied (pending, paid, completed, dll)
└── ✅ Notes bahasa Indonesia (atau NULL)

Cek Data Order Items

Klik table order_items:

EXPECTED DATA - ORDER_ITEMS (sample):

┌────┬──────────┬────────────┬──────────┬───────────┐
│ id │ order_id │ product_id │ quantity │ price     │
├────┼──────────┼────────────┼──────────┼───────────┤
│ 1  │ 1        │ 15         │ 2        │ 285000    │
│ 2  │ 1        │ 23         │ 1        │ 125000    │
│ 3  │ 2        │ 1          │ 1        │ 4500000   │
│ 4  │ 2        │ 5          │ 1        │ 350000    │
│ ...│ ...      │ ...        │ ...      │ ...       │
└────┴──────────┴────────────┴──────────┴───────────┘

VERIFIKASI:
├── ✅ ~50-80 items total (1-4 per order)
├── ✅ order_id valid
├── ✅ product_id valid
├── ✅ Quantity 1-3
└── ✅ Price = snapshot harga produk saat order

Test Query: Products dengan Category

Di TablePlus, buka tab SQL Query (Cmd+E atau Ctrl+E), jalankan:

SELECT
    p.id,
    p.name AS product_name,
    c.name AS category_name,
    CONCAT('Rp ', FORMAT(p.price, 0)) AS formatted_price,
    p.stock
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.is_active = 1
ORDER BY c.name, p.name
LIMIT 15;

Expected result:

┌────┬────────────────────────────────┬───────────────────┬────────────────┬───────┐
│ id │ product_name                   │ category_name     │ formatted_price│ stock │
├────┼────────────────────────────────┼───────────────────┼────────────────┼───────┤
│ 1  │ Charger Fast Elang 65W         │ Elektronik        │ Rp 185,000     │ 67    │
│ 2  │ Earbuds Nusantara Wireless     │ Elektronik        │ Rp 350,000     │ 78    │
│ 3  │ Laptop Garuda Series 15        │ Elektronik        │ Rp 8,500,000   │ 23    │
│ ...│ ...                            │ ...               │ ...            │ ...   │
└────┴────────────────────────────────┴───────────────────┴────────────────┴───────┘

Test Query: Orders dengan Total Items

SELECT
    o.order_number,
    u.name AS customer_name,
    CONCAT('Rp ', FORMAT(o.total_amount, 0)) AS total,
    COUNT(oi.id) AS total_items,
    o.status,
    o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY o.created_at DESC
LIMIT 10;

Expected result:

┌─────────────────────┬─────────────────┬───────────────┬─────────────┬───────────┬─────────────────────┐
│ order_number        │ customer_name   │ total         │ total_items │ status    │ created_at          │
├─────────────────────┼─────────────────┼───────────────┼─────────────┼───────────┼─────────────────────┤
│ ORD-20241225-M7K2P  │ Dewi Kusuma     │ Rp 1,250,000  │ 3           │ pending   │ 2024-12-25 14:30:00 │
│ ORD-20241224-X3N9Q  │ Ahmad Pratama   │ Rp 4,850,000  │ 2           │ paid      │ 2024-12-24 09:15:00 │
│ ...                 │ ...             │ ...           │ ...         │ ...       │ ...                 │
└─────────────────────┴─────────────────┴───────────────┴─────────────┴───────────┴─────────────────────┘

Test Query: Order Detail

SELECT
    o.order_number,
    p.name AS product_name,
    oi.quantity,
    CONCAT('Rp ', FORMAT(oi.price, 0)) AS unit_price,
    CONCAT('Rp ', FORMAT(oi.quantity * oi.price, 0)) AS subtotal
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
WHERE o.id = 1;

CHECKPOINT

CHECKPOINT BAGIAN 7:
├── ✅ TablePlus connected ke MySQL Docker
├── ✅ Semua tables visible
├── ✅ Data users terverifikasi (16 records)
├── ✅ Data categories terverifikasi (6 records)
├── ✅ Data products terverifikasi (~40 records)
├── ✅ Data orders terverifikasi (25 records)
├── ✅ Data order_items terverifikasi
├── ✅ Foreign key relationships valid
└── ✅ Custom queries berjalan dengan benar


Bagian 8: Useful Docker Commands & Closing

Selamat! Kamu sudah berhasil setup Laravel 12 dengan Docker, lengkap dengan database structure dan realistic data. Di bagian terakhir ini, saya akan kasih cheatsheet commands yang sering dipakai dan tips untuk produktivitas.

Docker Commands Cheatsheet

DAILY COMMANDS (paling sering dipakai):

# Start containers (background mode)
docker-compose up -d

# Stop containers
docker-compose down

# Restart semua containers
docker-compose restart

# Cek status containers
docker-compose ps

# Lihat logs (follow mode)
docker-compose logs -f

# Lihat logs specific service
docker-compose logs -f app
docker-compose logs -f mysql

ARTISAN COMMANDS VIA DOCKER:

# Format: docker-compose exec app php artisan [command]

# Migrations
docker-compose exec app php artisan migrate
docker-compose exec app php artisan migrate:fresh
docker-compose exec app php artisan migrate:fresh --seed
docker-compose exec app php artisan migrate:rollback
docker-compose exec app php artisan migrate:status

# Seeders
docker-compose exec app php artisan db:seed
docker-compose exec app php artisan db:seed --class=UserSeeder

# Cache
docker-compose exec app php artisan cache:clear
docker-compose exec app php artisan config:clear
docker-compose exec app php artisan route:clear
docker-compose exec app php artisan view:clear
docker-compose exec app php artisan optimize:clear

# Generators
docker-compose exec app php artisan make:model Product -mfs
docker-compose exec app php artisan make:controller ProductController -r
docker-compose exec app php artisan make:middleware AdminMiddleware

# Tinker
docker-compose exec app php artisan tinker

# Route list
docker-compose exec app php artisan route:list

COMPOSER COMMANDS VIA DOCKER:

# Install dependencies
docker-compose exec app composer install

# Update dependencies
docker-compose exec app composer update

# Add new package
docker-compose exec app composer require spatie/laravel-permission

# Add dev package
docker-compose exec app composer require --dev barryvdh/laravel-debugbar

# Autoload
docker-compose exec app composer dump-autoload

NPM COMMANDS VIA DOCKER (jika pakai Node):

# Kalau mau jalankan npm di container app
docker-compose exec app npm install
docker-compose exec app npm run dev
docker-compose exec app npm run build

DATABASE COMMANDS:

# Masuk ke MySQL shell
docker-compose exec mysql mysql -u laravel -p laravel_docker
# Password: secret

# Backup database
docker-compose exec mysql mysqldump -u laravel -p laravel_docker > backup.sql

# Restore database
docker-compose exec -T mysql mysql -u laravel -p laravel_docker < backup.sql

Troubleshooting Common Issues

ISSUE: "Connection refused" ke MySQL
─────────────────────────────────────
Cause: MySQL belum fully started
Solution:
1. Tunggu 30-60 detik setelah docker-compose up
2. Cek logs: docker-compose logs mysql
3. Restart: docker-compose restart mysql

ISSUE: Permission denied di storage/
─────────────────────────────────────
Cause: File ownership mismatch
Solution:
docker-compose exec app chmod -R 775 storage bootstrap/cache
docker-compose exec app chown -R www:www storage bootstrap/cache

ISSUE: Port 3306 already in use
─────────────────────────────────────
Cause: Local MySQL running
Solution:
1. Stop local MySQL, atau
2. Ganti port di docker-compose.yml:
   ports:
     - "3307:3306"
   Update .env: DB_PORT=3307

ISSUE: Container terus restart
─────────────────────────────────────
Cause: Error di startup
Solution:
1. Cek logs: docker-compose logs [service]
2. Biasanya: config error, port conflict, atau memory

ISSUE: Changes tidak ke-apply
─────────────────────────────────────
Cause: Cache atau container state
Solution:
docker-compose exec app php artisan optimize:clear
docker-compose restart

ISSUE: "Class not found" error
─────────────────────────────────────
Cause: Autoload outdated
Solution:
docker-compose exec app composer dump-autoload

Tips Produktivitas

TIPS UNTUK WORKFLOW LEBIH CEPAT:

1. Buat Alias di Terminal
─────────────────────────
# Tambahkan di ~/.bashrc atau ~/.zshrc
alias dc="docker-compose"
alias dce="docker-compose exec"
alias art="docker-compose exec app php artisan"

# Sekarang bisa pakai:
art migrate
art make:model Product
art tinker

2. Pakai Docker Desktop Dashboard
─────────────────────────────────
- Visual monitoring containers
- Quick access ke logs
- Easy start/stop

3. Multiple Terminal Tabs
─────────────────────────
- Tab 1: docker-compose logs -f
- Tab 2: Code editing
- Tab 3: artisan commands

4. TablePlus Shortcuts
──────────────────────
- Cmd+E: Open SQL editor
- Cmd+R: Refresh data
- Cmd+S: Save changes

Recap: Apa yang Sudah Dipelajari

LEARNING SUMMARY:

✅ Docker Fundamentals
├── Image, Container, Volume concepts
├── Dockerfile untuk PHP-FPM
├── Nginx configuration
└── docker-compose.yml orchestration

✅ Laravel + Docker Integration
├── Environment setup (.env untuk Docker)
├── Menjalankan artisan via container
├── Database connection antar containers
└── Permission handling

✅ Database Design
├── Migration best practices
├── Foreign key constraints
├── Index optimization
└── Enum dan decimal types

✅ Eloquent Models
├── Relationships (hasMany, belongsTo)
├── Scopes untuk reusable queries
├── Accessors untuk formatting
├── Boot methods untuk auto-generate

✅ Seeding Strategy
├── Factory patterns
├── Realistic Indonesian data
├── Seeding order (dependencies)
└── Database transactions

✅ Data Verification
├── TablePlus setup
├── Visual data inspection
└── Custom SQL queries

Struktur Project Final

laravel-docker/
├── app/
│   └── Models/
│       ├── User.php
│       ├── Category.php
│       ├── Product.php
│       ├── Order.php
│       └── OrderItem.php
├── database/
│   ├── factories/
│   │   ├── UserFactory.php
│   │   ├── CategoryFactory.php
│   │   ├── ProductFactory.php
│   │   └── OrderFactory.php
│   ├── migrations/
│   │   ├── ..._add_columns_to_users_table.php
│   │   ├── ..._create_categories_table.php
│   │   ├── ..._create_products_table.php
│   │   ├── ..._create_orders_table.php
│   │   └── ..._create_order_items_table.php
│   └── seeders/
│       ├── DatabaseSeeder.php
│       ├── UserSeeder.php
│       ├── CategorySeeder.php
│       ├── ProductSeeder.php
│       └── OrderSeeder.php
├── docker/
│   ├── php/
│   │   └── Dockerfile
│   └── nginx/
│       └── default.conf
├── docker-compose.yml
├── .env
└── ...

Next Steps

Setelah tutorial ini, kamu bisa lanjut ke:

NEXT LEARNING PATH:

├── Build Full Application
│   ├── Controllers & Routes
│   ├── Blade views dengan Tailwind
│   ├── Authentication dengan Breeze/Jetstream
│   └── CRUD operations
│
├── Advanced Docker
│   ├── Multi-stage builds
│   ├── Production Dockerfile
│   ├── Docker Swarm / Kubernetes
│   └── CI/CD dengan GitHub Actions
│
├── Add More Services
│   ├── Redis untuk cache & queue
│   ├── Mailhog untuk email testing
│   ├── MinIO untuk file storage
│   └── Elasticsearch untuk search
│
└── Deployment
    ├── Deploy ke DigitalOcean
    ├── Deploy ke AWS ECS
    ├── Deploy ke Railway/Render
    └── SSL dengan Let's Encrypt

Rekomendasi Kelas BuildWithAngga

Kalau kamu mau memperdalam skill Laravel dan Docker, cek kelas-kelas ini di BuildWithAngga:

  • Laravel 12 Essentials — Fundamental Laravel untuk pemula
  • Full Stack Laravel + Vue.js — Build complete web application
  • DevOps untuk Developer — Docker, CI/CD, dan deployment

Closing

Docker mungkin terasa overwhelming di awal, tapi trust me — ini investasi yang sangat worth it. Begitu kamu terbiasa, setup project baru jadi hitungan menit, bukan jam. Environment selalu konsisten, gak ada lagi drama "works on my machine".

Sebagai freelancer, ini skill yang membedakan kamu dari developer lain. Client akan appreciate ketika kamu bisa deliver project dengan setup yang proper dan bisa di-deploy dengan mudah.

Yang penting: jangan cuma dibaca, langsung praktek. Clone repo, jalankan docker-compose up, dan mulai explore. Kalau ada error, itu bagian dari learning process.

Semoga tutorial ini bermanfaat. Happy coding! 🚀


Total waktu development dengan vibe coding: ~2-3 jamTanpa vibe coding: ~8-12 jam

Time saved: 70-80%