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% ⚡