Template MVC PHP yang kompatibel dengan PHP 5.2, 7, 8 dan versi lebih tinggi. Terinspirasi dari Laravel dan CodeIgniter dengan konfigurasi yang disesuaikan untuk mendukung berbagai versi PHP.
- Fitur Utama
- Struktur Folder
- Instalasi & Setup
- Konfigurasi Environment
- Dokumentasi CRUD
- Query Builder
- Routing
- Controller
- Model
- View
- Authentication
- Middleware
- Helper Functions
- Security
- Kompatibilitas PHP
- Tips & Best Practices
- Troubleshooting
- ✅ Kompatibilitas Multi-Versi: PHP 5.2, 7, 8 dan lebih tinggi
- ✅ Auto-Detection: Otomatis menggunakan PDO atau mysql_* sesuai versi PHP
- ✅ Environment Configuration: Semua config terpusat di file .env
- ✅ CRUD Lengkap: Insert, Select All, Select One, Select Where, Update, Delete
- ✅ Query Builder: Mendukung method chaining untuk query kompleks
- ✅ MVC Pattern: Structure yang clean dan terorganisir
- ✅ Flash Messages: System pesan notifikasi
- ✅ Helper Functions: Berbagai fungsi helper yang berguna
- ✅ Security: Prepared statements dan input sanitization
- ✅ Authentication & Authorization: Login, Register, Logout, Role-based access
- ✅ Middleware System: Request filtering dan validation
MVC-PHP-5-TEMPLATE/
├── .env # Environment configuration
├── .env.example # Environment template
├── .gitignore # Git ignore file
├── README.md # Dokumentasi utama
├── test_implementation.php # File testing implementasi
│
├── _DEV/ # Folder dokumentasi development
│ ├── CHANGELOG_SECURITY.md # Log perubahan security
│ ├── CONTOH_IMPLEMENTASI_AUTH.md # Contoh auth implementation
│ ├── CONTOH_IMPLEMENTASI.md # Contoh CRUD implementation
│ ├── database_secure.sql # Database schema dengan security
│ ├── database.sql # Database schema basic
│ ├── DOKUMENTASI_CRUD.md # Dokumentasi lengkap CRUD
│ ├── DOKUMENTASI_ENV.md # Dokumentasi environment
│ ├── DOKUMENTASI_MIDDLEWARE.md # Dokumentasi middleware
│ ├── DOKUMENTASI_SECURITY.md # Dokumentasi security features
│ ├── ENV_QUICK_START.md # Quick start guide .env
│ ├── FIX_ERROR_LOG.md # Log perbaikan error
│ ├── FIX_HEADERS_ERROR.md # Fix untuk headers error
│ ├── LAPORAN_ANALISIS.md # Laporan analisis sistem
│ ├── RINGKASAN_PERUBAHAN.md # Ringkasan perubahan
│ ├── SECURITY_FEATURES.md # Detail security features
│ ├── UPDATE_LOG_MIDDLEWARE.md # Update log middleware
│ └── test_compatibility.php # Test kompatibilitas PHP
│
├── app/ # Folder aplikasi utama
│ ├── init.php # Inisialisasi aplikasi
│ │
│ ├── controllers/ # Folder controllers
│ │ ├── AuthController.php # Controller untuk authentication
│ │ └── HomeController.php # Controller homepage
│ │
│ ├── core/ # Folder core system
│ │ ├── App.php # Core aplikasi
│ │ ├── Config.php # Konfigurasi (baca dari .env)
│ │ ├── Controller.php # Base controller
│ │ ├── Database.php # Database handler (PHP 5.2 - 8+)
│ │ ├── Env.php # Environment loader
│ │ ├── Helper.php # Helper functions
│ │ ├── Middleware.php # Middleware handler
│ │ ├── Model.php # Base model dengan CRUD
│ │ ├── Router.php # Routing system
│ │ └── Security.php # Security functions
│ │
│ ├── middlewares/ # Folder middlewares
│ │ ├── AuthMiddleware.php # Middleware untuk auth users
│ │ ├── GuestMiddleware.php # Middleware untuk guests
│ │ └── RoleMiddleware.php # Middleware role-based
│ │
│ ├── models/ # Folder models
│ │ └── User.php # Model User
│ │
│ ├── routes/ # Folder routing
│ │ └── routes.php # Definisi routing
│ │
│ └── views/ # Folder views
│ ├── home.php # View homepage
│ └── errors/ # Error views
│ ├── dd.php # Debug & dump view
│ └── error.php # Error view
│
├── public/ # Folder public (document root)
│ └── index.php # Entry point aplikasi
│
├── release/ # Folder untuk release files
│
└── storage/ # Folder storage
├── cache/ # Cache directory
└── logs/ # Log files directory
| Folder/File | Fungsi |
|---|---|
| .env | File konfigurasi environment (database, app settings, dll) |
| _DEV/ | Dokumentasi lengkap dan file development |
| app/controllers/ | Tempat semua controller aplikasi |
| app/core/ | Core system framework (jangan diubah kecuali tahu yang dilakukan) |
| app/middlewares/ | Middleware untuk filtering request |
| app/models/ | Model untuk database operations |
| app/routes/ | Definisi routing URL |
| app/views/ | File tampilan HTML/PHP |
| public/ | Entry point, bisa diakses publik |
| storage/ | Tempat cache dan log files |
# Clone repository (jika dari GitHub)
git clone https://github.com/username/MVC-PHP-5-TEMPLATE.git
# Atau download ZIP dan extract# Copy .env.example ke .env
copy .env.example .env
# Edit .env dan sesuaikan dengan environment Anda
notepad .envEdit file .env dan sesuaikan konfigurasi database:
# Database Configuration
DB_HOST=localhost
DB_NAME=crudtest
DB_USER=root
DB_PASS=
DB_PORT=3306
DB_CHARSET=utf8Buat database baru:
CREATE DATABASE crudtest;
USE crudtest;Import schema (pilih salah satu):
# Option 1: Basic schema (untuk CRUD basic)
mysql -u root -p crudtest < _DEV/database.sql
# Option 2: Secure schema (dengan authentication & security features)
mysql -u root -p crudtest < _DEV/database_secure.sqlAtau import manual via phpMyAdmin:
- Buka phpMyAdmin (http://localhost/phpmyadmin)
- Pilih database
crudtest - Klik tab "Import"
- Pilih file
_DEV/database.sqlatau_DEV/database_secure.sql - Klik "Go"
Menggunakan PHP Built-in Server:
# Jalankan dari root directory project
cd c:\xampp\htdocs\MVC-PHP-5-TEMPLATE
php -S localhost:8000 -t public
# Akses di browser
http://localhost:8000Menggunakan XAMPP/WAMP:
- Letakkan folder di
htdocs(XAMPP) atauwww(WAMP) - Pastikan Apache dan MySQL running
- Edit
.envdan sesuaikanBASE_URL:BASE_URL=http://localhost/MVC-PHP-5-TEMPLATE/
- Akses di browser:
http://localhost/MVC-PHP-5-TEMPLATE/
Buka browser dan akses homepage. Jika berhasil, Anda akan melihat:
- Halaman home dengan data user dari database
- Tidak ada error PHP
- Data tampil dengan benar
Semua konfigurasi aplikasi tersentralisasi di file .env. Ini memudahkan deployment ke berbagai environment (development, staging, production).
# ============================================
# DATABASE CONFIGURATION
# ============================================
DB_HOST=localhost # Host database (localhost atau IP server)
DB_NAME=crudtest # Nama database
DB_USER=root # Username database
DB_PASS= # Password database (kosong untuk XAMPP default)
DB_PORT=3306 # Port MySQL (default 3306)
DB_CHARSET=utf8 # Character set (utf8 atau utf8mb4)
# ============================================
# APPLICATION CONFIGURATION
# ============================================
APP_NAME=MVC-PHP-5-TEMPLATE # Nama aplikasi
APP_ENV=development # Environment: development, staging, production
APP_DEBUG=true # Debug mode (true/false)
APP_URL=http://localhost/MVC-PHP-5-TEMPLATE/
# ============================================
# BASE URL CONFIGURATION
# ============================================
BASE_URL=http://localhost/MVC-PHP-5-TEMPLATE/
# Sesuaikan dengan URL aplikasi Anda
# ============================================
# SESSION CONFIGURATION
# ============================================
SESSION_LIFETIME=120 # Waktu session (menit)
SESSION_COOKIE_NAME=mvc_session # Nama cookie session
# ============================================
# SECURITY CONFIGURATION
# ============================================
SECURITY_SALT=your_random_salt_here_change_this
# Ganti dengan string random untuk security
HASH_ALGO=sha256 # Algorithm hash (md5, sha256, dll)
# ============================================
# ERROR HANDLING
# ============================================
DISPLAY_ERRORS=true # Tampilkan error di browser
LOG_ERRORS=true # Log error ke file
ERROR_LOG_PATH=storage/logs/error.log<?php
// Ambil nilai dari .env
$dbHost = env('DB_HOST', 'localhost'); // Dengan default value
$appName = env('APP_NAME'); // Tanpa default value
// Contoh penggunaan
$debugMode = env('APP_DEBUG', false);
if ($debugMode) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
}
// Cek environment
$env = env('APP_ENV');
if ($env === 'production') {
// Konfigurasi production
} else {
// Konfigurasi development
}- Jangan commit file .env ke repository - Gunakan .env.example sebagai template
- Gunakan nilai yang berbeda untuk setiap environment - Development, staging, production
- Ganti SECURITY_SALT - Gunakan string random yang unik
- Set DEBUG=false di production - Untuk keamanan dan performa
- Backup file .env - Simpan di tempat yang aman
📖 Dokumentasi lengkap: DOKUMENTASI_ENV.md
Model dasar menyediakan method CRUD lengkap yang kompatibel dengan semua versi PHP:
| Method | Fungsi | Parameter | Return |
|---|---|---|---|
insert($data) |
Tambah data baru | Array data | Object |
selectAll() |
Ambil semua data | - | Array |
selectOne($id) |
Ambil satu data berdasarkan ID | Integer ID | Object/null |
selectWhere($col, $val, $op) |
Select dengan kondisi WHERE | Column, Value, Operator | Array |
update($data) |
Update data (butuh WHERE) | Array data | Integer |
updateById($id, $data) |
Update berdasarkan ID | ID, Array data | Integer |
delete($id) |
Hapus data berdasarkan ID | Integer ID | Integer |
deleteById($id) |
Alias dari delete() | Integer ID | Integer |
Syntax:
$model->insert($data);Parameter:
$data(array): Associative array dengan key = nama kolom, value = nilai
Return: Object hasil insert (dengan ID yang baru dibuat)
Contoh:
<?php
// Di Controller
class UserController extends Controller
{
public function store()
{
$user = new User();
// Data yang akan disimpan
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email']),
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT),
'role' => 'user'
);
// Insert data
$result = $user->insert($data);
if ($result) {
// $result->id berisi ID yang baru dibuat
$this->redirectBack('Data berhasil disimpan dengan ID: ' . $result->id, 'success');
} else {
$this->redirectBack('Gagal menyimpan data', 'error');
}
}
}Syntax:
$model->selectAll();Parameter: Tidak ada
Return: Array of objects (semua record dari tabel)
Contoh:
<?php
class UserController extends Controller
{
public function index()
{
$user = new User();
// Ambil semua data
$users = $user->selectAll();
// Kirim ke view
$this->view('users/index', array('users' => $users));
}
}Contoh di View:
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo $user->id; ?></td>
<td><?php echo htmlspecialchars($user->nama); ?></td>
<td><?php echo htmlspecialchars($user->email); ?></td>
</tr>
<?php endforeach; ?>Syntax:
$model->selectOne($id);Parameter:
$id(integer): ID record yang akan diambil
Return: Object (single record) atau null jika tidak ditemukan
Contoh:
<?php
class UserController extends Controller
{
public function show($id)
{
$user = new User();
// Ambil data berdasarkan ID
$userData = $user->selectOne($id);
if ($userData) {
// Data ditemukan
$this->view('users/show', array('user' => $userData));
} else {
// Data tidak ditemukan
$this->redirectTo('users', 'Data tidak ditemukan', 'error');
}
}
}Contoh di View:
<h1>Detail User</h1>
<p><strong>ID:</strong> <?php echo $user->id; ?></p>
<p><strong>Nama:</strong> <?php echo htmlspecialchars($user->nama); ?></p>
<p><strong>Email:</strong> <?php echo htmlspecialchars($user->email); ?></p>Syntax:
$model->selectWhere($column, $value, $operator);Parameter:
$column(string): Nama kolom$value(mixed): Nilai yang dicari$operator(string): Operator (=, <, >, <=, >=, LIKE, dll) - Default: '='
Return: Array of objects (record yang memenuhi kondisi)
Contoh:
<?php
$user = new User();
// Cari berdasarkan nama (LIKE)
$users = $user->selectWhere('nama', '%John%', 'LIKE');
// Cari berdasarkan email (exact match)
$users = $user->selectWhere('email', 'john@example.com', '=');
// Atau tanpa operator (default =)
$users = $user->selectWhere('email', 'john@example.com');
// Cari user aktif
$users = $user->selectWhere('status', 1);
// Cari umur lebih dari 18
$users = $user->selectWhere('age', 18, '>');
// Cari user dengan role admin
$admins = $user->selectWhere('role', 'admin');Syntax:
$model->updateById($id, $data);Parameter:
$id(integer): ID record yang akan diupdate$data(array): Associative array data yang akan diupdate
Return: Integer (jumlah row yang affected)
Contoh:
<?php
class UserController extends Controller
{
public function update($id)
{
$user = new User();
// Data yang akan diupdate
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email'])
);
// Jika password diisi, update juga
if (!empty($_POST['password'])) {
$data['password'] = password_hash($_POST['password'], PASSWORD_DEFAULT);
}
// Update berdasarkan ID
$affected = $user->updateById($id, $data);
if ($affected > 0) {
$this->redirectBack('Data berhasil diupdate', 'success');
} else {
$this->redirectBack('Tidak ada data yang diupdate', 'warning');
}
}
}Syntax:
$model->deleteById($id);
// atau
$model->delete($id);Parameter:
$id(integer): ID record yang akan dihapus
Return: Integer (jumlah row yang affected)
Contoh:
<?php
class UserController extends Controller
{
public function destroy($id)
{
$user = new User();
// Hapus data
$affected = $user->deleteById($id);
if ($affected > 0) {
$this->redirectBack('Data berhasil dihapus', 'success');
} else {
$this->redirectBack('Gagal menghapus data', 'error');
}
}
}Dengan Konfirmasi di View:
<form action="<?php echo base_url('users/' . $user->id . '/delete'); ?>" method="POST" onsubmit="return confirm('Yakin ingin menghapus data ini?')">
<button type="submit" class="btn btn-danger">Hapus</button>
</form>- Selalu gunakan clean_input() untuk sanitasi data input
- Gunakan password_hash() untuk password, jangan simpan plain text
- Cek hasil operasi sebelum redirect atau tampilkan pesan
- Gunakan htmlspecialchars() saat menampilkan data di view (XSS protection)
- Validasi data sebelum insert/update
📖 Dokumentasi lengkap: DOKUMENTASI_CRUD.md
Query Builder memungkinkan Anda membuat query kompleks dengan method chaining yang mudah dibaca dan maintainable.
| Method | Fungsi | Parameter | Return |
|---|---|---|---|
where($col, $val, $op) |
Tambah kondisi WHERE | Column, Value, Operator | Model |
orWhere($col, $val, $op) |
Tambah kondisi OR WHERE | Column, Value, Operator | Model |
orderBy($col, $dir) |
Urutkan hasil | Column, Direction | Model |
limit($limit) |
Batasi jumlah hasil | Integer | Model |
offset($offset) |
Skip n record | Integer | Model |
get() |
Eksekusi query & ambil semua | - | Array |
first() |
Eksekusi query & ambil pertama | - | Object/null |
count() |
Hitung jumlah record | - | Integer |
Syntax:
$model->where($column, $value, $operator)->get();Contoh:
<?php
$user = new User();
// Single WHERE
$users = $user->where('nama', 'John')->get();
// Multiple WHERE (AND condition)
$users = $user->where('nama', 'John')
->where('status', 1)
->get();
// WHERE dengan operator
$users = $user->where('age', 18, '>')->get();
$users = $user->where('nama', '%John%', 'LIKE')->get();
$users = $user->where('created_at', '2024-01-01', '>=')->get();
// Complex WHERE
$users = $user->where('status', 1)
->where('verified', 1)
->where('created_at', '2024-01-01', '>')
->get();Syntax:
$model->where(...)->orWhere(...)->get();Contoh:
<?php
$user = new User();
// WHERE dengan OR
$users = $user->where('nama', 'John')
->orWhere('nama', 'Jane')
->get();
// Kombinasi AND dan OR
$users = $user->where('status', 1)
->where('age', 18, '>')
->orWhere('role', 'admin')
->get();
// SQL: WHERE status = 1 AND age > 18 OR role = 'admin'
// Search di multiple fields
$search = $_GET['search'];
$users = $user->where('nama', "%{$search}%", 'LIKE')
->orWhere('email', "%{$search}%", 'LIKE')
->orWhere('phone', "%{$search}%", 'LIKE')
->get();Syntax:
$model->orderBy($column, $direction)->get();Parameter:
$column(string): Nama kolom$direction(string): 'ASC' atau 'DESC' (default: 'ASC')
Contoh:
<?php
$user = new User();
// Order ASC (ascending)
$users = $user->orderBy('nama', 'ASC')->get();
// Order DESC (descending)
$users = $user->orderBy('created_at', 'DESC')->get();
// Multiple order by
$users = $user->orderBy('status', 'DESC')
->orderBy('nama', 'ASC')
->get();
// Order by dengan WHERE
$users = $user->where('status', 1)
->orderBy('created_at', 'DESC')
->get();Syntax:
$model->limit($number)->get();Contoh:
<?php
$user = new User();
// Ambil 10 data pertama
$users = $user->limit(10)->get();
// Ambil 5 user terbaru
$users = $user->orderBy('created_at', 'DESC')
->limit(5)
->get();
// Ambil 10 user aktif
$users = $user->where('status', 1)
->limit(10)
->get();Syntax:
$model->offset($number)->get();Contoh:
<?php
$user = new User();
// Skip 10 record pertama
$users = $user->offset(10)->get();
// Pagination: Halaman 1 (0-9)
$users = $user->limit(10)->offset(0)->get();
// Pagination: Halaman 2 (10-19)
$users = $user->limit(10)->offset(10)->get();
// Pagination: Halaman 3 (20-29)
$users = $user->limit(10)->offset(20)->get();Syntax:
$model->where(...)->get();Return: Array of objects
Contoh:
<?php
$user = new User();
// Get semua hasil query
$users = $user->where('status', 1)->get();
// Loop hasil
foreach ($users as $user) {
echo $user->nama . '<br>';
}
// Cek apakah ada hasil
if (count($users) > 0) {
echo "Ditemukan " . count($users) . " users";
}Syntax:
$model->where(...)->first();Return: Object atau null
Contoh:
<?php
$user = new User();
// Ambil user berdasarkan email
$user = $user->where('email', 'john@example.com')->first();
if ($user) {
echo "Nama: " . $user->nama;
echo "Email: " . $user->email;
} else {
echo "User tidak ditemukan";
}
// Login check
$user = $user->where('email', $email)
->where('status', 1)
->first();
if ($user && password_verify($password, $user->password)) {
// Login berhasil
}Syntax:
$model->where(...)->count();Return: Integer
Contoh:
<?php
$user = new User();
// Hitung semua user
$total = $user->count();
echo "Total users: " . $total;
// Hitung user aktif
$active = $user->where('status', 1)->count();
echo "Active users: " . $active;
// Hitung admin
$admins = $user->where('role', 'admin')->count();
// Hitung user yang terdaftar hari ini
$today = date('Y-m-d');
$newUsers = $user->where('created_at', $today, 'LIKE')->count();<?php
class UserController extends Controller
{
public function index()
{
$user = new User();
// Pagination settings
$perPage = 10;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $perPage;
// Get data
$users = $user->where('status', 1)
->orderBy('created_at', 'DESC')
->limit($perPage)
->offset($offset)
->get();
// Total records
$total = $user->where('status', 1)->count();
$totalPages = ceil($total / $perPage);
// Send to view
$this->view('users/index', array(
'users' => $users,
'currentPage' => $page,
'totalPages' => $totalPages,
'total' => $total
));
}
}<?php
class UserController extends Controller
{
public function search()
{
$user = new User();
$search = isset($_GET['search']) ? clean_input($_GET['search']) : '';
$role = isset($_GET['role']) ? clean_input($_GET['role']) : '';
// Build query
$query = $user;
// Add search condition
if (!empty($search)) {
$query = $query->where('nama', "%{$search}%", 'LIKE')
->orWhere('email', "%{$search}%", 'LIKE');
}
// Add role filter
if (!empty($role)) {
$query = $query->where('role', $role);
}
// Execute
$users = $query->orderBy('nama', 'ASC')->get();
$this->view('users/search', array('users' => $users));
}
}<?php
$user = new User();
// Query kompleks untuk admin panel
$users = $user->where('status', 1)
->where('email_verified', 1)
->where('created_at', '2024-01-01', '>=')
->where('role', 'admin', '!=')
->orderBy('last_login', 'DESC')
->limit(50)
->get();
// Latest active users
$latestUsers = $user->where('status', 1)
->where('last_login', date('Y-m-d'), 'LIKE')
->orderBy('last_login', 'DESC')
->limit(10)
->get();Routing mengatur URL dan mengarahkannya ke Controller dan Method yang sesuai.
Semua route didefinisikan di: app/routes/routes.php
<?php
return array(
array('METHOD', 'URI', 'Controller@method'),
array('METHOD', 'URI', 'Controller@method', 'middleware'),
);Parameter:
METHOD: HTTP method (GET, POST, PUT, DELETE)URI: Path URL (bisa dengan parameter{id},{slug}, dll)Controller@method: Nama controller dan methodmiddleware: Opsional, nama middleware yang akan dijalankan
<?php
return array(
// Homepage
array('GET', '/', 'HomeController@index'),
// Static pages
array('GET', '/about', 'PageController@about'),
array('GET', '/contact', 'PageController@contact'),
// Users CRUD
array('GET', '/users', 'UserController@index'),
array('GET', '/users/create', 'UserController@create'),
array('POST', '/users/store', 'UserController@store'),
array('GET', '/users/{id}', 'UserController@show'),
array('GET', '/users/{id}/edit', 'UserController@edit'),
array('POST', '/users/{id}/update', 'UserController@update'),
array('POST', '/users/{id}/delete', 'UserController@destroy'),
);<?php
return array(
// Single parameter
array('GET', '/users/{id}', 'UserController@show'),
array('GET', '/posts/{id}', 'PostController@show'),
// Multiple parameters
array('GET', '/category/{cat}/product/{id}', 'ProductController@show'),
array('GET', '/blog/{year}/{month}/{slug}', 'BlogController@show'),
);Di Controller:
<?php
class UserController extends Controller
{
// Parameter otomatis di-pass ke method
public function show($id)
{
$user = new User();
$userData = $user->selectOne($id);
// ...
}
}
class ProductController extends Controller
{
public function show($cat, $id)
{
// $cat dan $id otomatis tersedia
}
}<?php
return array(
// Route tanpa middleware (public)
array('GET', '/', 'HomeController@index'),
// Route dengan AuthMiddleware (harus login)
array('GET', '/dashboard', 'DashboardController@index', 'auth'),
array('GET', '/profile', 'ProfileController@index', 'auth'),
array('POST', '/profile/update', 'ProfileController@update', 'auth'),
// Route dengan GuestMiddleware (belum login)
array('GET', '/login', 'AuthController@loginForm', 'guest'),
array('POST', '/login', 'AuthController@login', 'guest'),
array('GET', '/register', 'AuthController@registerForm', 'guest'),
array('POST', '/register', 'AuthController@register', 'guest'),
// Route dengan RoleMiddleware (hanya admin)
array('GET', '/admin', 'AdminController@index', 'role:admin'),
array('GET', '/admin/users', 'AdminController@users', 'role:admin'),
// Multiple roles (admin atau moderator)
array('GET', '/moderator', 'ModController@index', 'role:admin,moderator'),
);Untuk CRUD lengkap, gunakan pattern ini:
<?php
return array(
// Resource: Users
array('GET', '/users', 'UserController@index'), // List all
array('GET', '/users/create', 'UserController@create'), // Show create form
array('POST', '/users', 'UserController@store'), // Store new
array('GET', '/users/{id}', 'UserController@show'), // Show detail
array('GET', '/users/{id}/edit', 'UserController@edit'), // Show edit form
array('POST', '/users/{id}', 'UserController@update'), // Update
array('POST', '/users/{id}/delete', 'UserController@destroy'), // Delete
);Untuk URL yang lebih clean dan mudah diingat:
<?php
return array(
// Auth routes
array('GET', '/login', 'AuthController@loginForm'),
array('POST', '/login', 'AuthController@login'),
array('GET', '/register', 'AuthController@registerForm'),
array('POST', '/register', 'AuthController@register'),
array('GET', '/logout', 'AuthController@logout'),
// Dashboard
array('GET', '/dashboard', 'DashboardController@index', 'auth'),
array('GET', '/dashboard/stats', 'DashboardController@stats', 'auth'),
// Admin
array('GET', '/admin', 'AdminController@index', 'role:admin'),
array('GET', '/admin/users', 'AdminController@users', 'role:admin'),
array('GET', '/admin/settings', 'AdminController@settings', 'role:admin'),
);- Urutkan dari yang spesifik ke general - Route dengan parameter harus di bawah route static
- Gunakan naming convention - Buat URL yang clean dan meaningful
- Group related routes - Tempatkan route yang related berdekatan
- Protect dengan middleware - Gunakan middleware untuk route yang perlu authentication
- RESTful pattern - Ikuti pattern RESTful untuk API endpoints
Controller menangani logic aplikasi dan menghubungkan Model dengan View.
Semua controller harus extends dari Controller class di app/core/Controller.php.
<?php
// app/controllers/ProductController.php
require_once dirname(__FILE__) . '/../core/Controller.php';
require_once dirname(__FILE__) . '/../models/Product.php';
class ProductController extends Controller
{
// Method untuk list all products
public function index()
{
$product = new Product();
$products = $product->selectAll();
$this->view('products/index', array('products' => $products));
}
// Method untuk show detail product
public function show($id)
{
$product = new Product();
$productData = $product->selectOne($id);
if ($productData) {
$this->view('products/show', array('product' => $productData));
} else {
$this->error('Product not found', 404);
}
}
// Method untuk show create form
public function create()
{
$this->view('products/create');
}
// Method untuk store new product
public function store()
{
$product = new Product();
$data = array(
'name' => clean_input($_POST['name']),
'price' => clean_input($_POST['price']),
'stock' => clean_input($_POST['stock']),
'description' => clean_input($_POST['description'])
);
$result = $product->insert($data);
if ($result) {
$this->redirectTo('products', 'Product berhasil ditambahkan', 'success');
} else {
$this->redirectBack('Gagal menambahkan product', 'error');
}
}
// Method untuk show edit form
public function edit($id)
{
$product = new Product();
$productData = $product->selectOne($id);
if ($productData) {
$this->view('products/edit', array('product' => $productData));
} else {
$this->redirectTo('products', 'Product tidak ditemukan', 'error');
}
}
// Method untuk update product
public function update($id)
{
$product = new Product();
$data = array(
'name' => clean_input($_POST['name']),
'price' => clean_input($_POST['price']),
'stock' => clean_input($_POST['stock']),
'description' => clean_input($_POST['description'])
);
$affected = $product->updateById($id, $data);
if ($affected > 0) {
$this->redirectTo('products', 'Product berhasil diupdate', 'success');
} else {
$this->redirectBack('Tidak ada perubahan data', 'warning');
}
}
// Method untuk delete product
public function destroy($id)
{
$product = new Product();
$affected = $product->deleteById($id);
if ($affected > 0) {
$this->redirectBack('Product berhasil dihapus', 'success');
} else {
$this->redirectBack('Gagal menghapus product', 'error');
}
}
}Load dan tampilkan view dengan data.
<?php
// View tanpa data
$this->view('home');
// View dengan data
$this->view('users/index', array(
'users' => $users,
'title' => 'Daftar User'
));
// Nested view
$this->view('admin/dashboard/index', $data);Redirect ke URL tertentu dengan flash message.
<?php
// Redirect ke users
$this->redirectTo('users');
// Redirect dengan message
$this->redirectTo('users', 'Data berhasil disimpan', 'success');
// Redirect ke homepage
$this->redirectTo('');
// Redirect ke external URL
$this->redirectTo('http://google.com');Redirect kembali ke halaman sebelumnya.
<?php
// Redirect back tanpa message
$this->redirectBack();
// Redirect back dengan message
$this->redirectBack('Data berhasil disimpan', 'success');
$this->redirectBack('Terjadi kesalahan', 'error');
$this->redirectBack('Perhatian!', 'warning');Tampilkan halaman error.
<?php
// Error 404
$this->error('Page not found', 404);
// Error 403
$this->error('Access denied', 403);
// Error 500
$this->error('Internal server error', 500);<?php
class UserController extends Controller
{
public function store()
{
// Validation
$errors = array();
if (empty($_POST['nama'])) {
$errors[] = 'Nama harus diisi';
}
if (empty($_POST['email'])) {
$errors[] = 'Email harus diisi';
} elseif (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Email tidak valid';
}
if (empty($_POST['password'])) {
$errors[] = 'Password harus diisi';
} elseif (strlen($_POST['password']) < 6) {
$errors[] = 'Password minimal 6 karakter';
}
// Jika ada error
if (count($errors) > 0) {
$_SESSION['errors'] = $errors;
$_SESSION['old'] = $_POST;
$this->redirectBack();
return;
}
// Cek email sudah ada atau belum
$user = new User();
$exists = $user->where('email', $_POST['email'])->first();
if ($exists) {
$this->redirectBack('Email sudah digunakan', 'error');
return;
}
// Insert data
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email']),
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT),
'role' => 'user'
);
$result = $user->insert($data);
if ($result) {
$this->redirectTo('users', 'User berhasil ditambahkan', 'success');
} else {
$this->redirectBack('Gagal menambahkan user', 'error');
}
}
}Model menangani database operations dan business logic.
Semua model harus extends dari Model class di app/core/Model.php.
<?php
// app/models/Product.php
require_once dirname(__FILE__) . '/../core/Model.php';
class Product extends Model
{
// Nama tabel (wajib)
protected $table = 'tbl_product';
// Daftar kolom (opsional)
protected $fields = array('id', 'name', 'price', 'stock', 'description', 'created_at');
// Custom method
public function getActiveProducts()
{
return $this->where('status', 1)
->where('stock', 0, '>')
->orderBy('name', 'ASC')
->get();
}
public function getProductsByCategory($categoryId)
{
return $this->where('category_id', $categoryId)
->orderBy('created_at', 'DESC')
->get();
}
public function getTopProducts($limit = 10)
{
return $this->where('featured', 1)
->where('status', 1)
->orderBy('views', 'DESC')
->limit($limit)
->get();
}
public function searchProducts($keyword)
{
return $this->where('name', "%{$keyword}%", 'LIKE')
->orWhere('description', "%{$keyword}%", 'LIKE')
->where('status', 1)
->orderBy('name', 'ASC')
->get();
}
}<?php
protected $table = 'nama_tabel'; // Wajib: nama tabel database
protected $fields = array('col1', ...); // Opsional: daftar kolom<?php
// app/models/User.php
require_once dirname(__FILE__) . '/../core/Model.php';
class User extends Model
{
protected $table = 'tbl_user';
protected $fields = array('id', 'nama', 'email', 'password', 'role', 'status', 'created_at');
/**
* Get all active users
*/
public function getActiveUsers()
{
return $this->where('status', 1)
->orderBy('nama', 'ASC')
->get();
}
/**
* Get user by email
*/
public function getUserByEmail($email)
{
return $this->where('email', $email)->first();
}
/**
* Get users by role
*/
public function getUsersByRole($role)
{
return $this->where('role', $role)
->where('status', 1)
->get();
}
/**
* Search users
*/
public function searchUsers($keyword)
{
return $this->where('nama', "%{$keyword}%", 'LIKE')
->orWhere('email', "%{$keyword}%", 'LIKE')
->orderBy('nama', 'ASC')
->get();
}
/**
* Get users with pagination
*/
public function getUsersPaginated($page = 1, $perPage = 10)
{
$offset = ($page - 1) * $perPage;
return $this->where('status', 1)
->orderBy('created_at', 'DESC')
->limit($perPage)
->offset($offset)
->get();
}
/**
* Count total users
*/
public function getTotalUsers()
{
return $this->count();
}
/**
* Count active users
*/
public function getActiveUsersCount()
{
return $this->where('status', 1)->count();
}
}- Buat method untuk query yang sering dipakai - Jangan tulis query yang sama berulang-ulang
- Gunakan nama method yang descriptive -
getActiveUsers()lebih baik darigetUsers() - Tambahkan comment/docblock - Jelaskan fungsi dari method
- Validasi di model atau controller - Tergantung kompleksitas business logic
- Jangan hardcode value - Gunakan parameter untuk nilai yang dynamic
View menangani tampilan HTML yang ditampilkan ke user.
app/views/
├── home.php # Homepage
├── users/ # User views
│ ├── index.php # List users
│ ├── show.php # User detail
│ ├── create.php # Create form
│ └── edit.php # Edit form
├── products/ # Product views
│ ├── index.php
│ └── show.php
├── auth/ # Auth views
│ ├── login.php
│ └── register.php
└── errors/ # Error views
├── error.php
└── dd.php
<!-- app/views/users/index.php -->
<!DOCTYPE html>
<html>
<head>
<title>Daftar User</title>
<style>
.alert { padding: 10px; margin: 10px 0; }
.alert.success { background: #d4edda; color: #155724; }
.alert.error { background: #f8d7da; color: #721c24; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f2f2f2; }
</style>
</head>
<body>
<h1>Daftar User</h1>
<!-- Flash Message -->
<?php if (isset($_SESSION['flash_message'])): ?>
<div class="alert <?php echo $_SESSION['flash_type']; ?>">
<?php
echo $_SESSION['flash_message'];
unset($_SESSION['flash_message']);
unset($_SESSION['flash_type']);
?>
</div>
<?php endif; ?>
<!-- Action Buttons -->
<div>
<a href="<?php echo base_url('users/create'); ?>">+ Tambah User</a>
</div>
<!-- Data Table -->
<table>
<thead>
<tr>
<th>ID</th>
<th>Nama</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php if (count($users) > 0): ?>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo $user->id; ?></td>
<td><?php echo htmlspecialchars($user->nama); ?></td>
<td><?php echo htmlspecialchars($user->email); ?></td>
<td><?php echo $user->role; ?></td>
<td><?php echo $user->status == 1 ? 'Aktif' : 'Nonaktif'; ?></td>
<td>
<a href="<?php echo base_url('users/' . $user->id); ?>">Detail</a> |
<a href="<?php echo base_url('users/' . $user->id . '/edit'); ?>">Edit</a> |
<form action="<?php echo base_url('users/' . $user->id . '/delete'); ?>" method="POST" style="display:inline;" onsubmit="return confirm('Yakin ingin menghapus?')">
<button type="submit">Hapus</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6">Tidak ada data</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</body>
</html><!-- app/views/users/create.php -->
<!DOCTYPE html>
<html>
<head>
<title>Tambah User</title>
<style>
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input, select { width: 100%; padding: 8px; }
.error { color: red; margin-top: 5px; }
</style>
</head>
<body>
<h1>Tambah User Baru</h1>
<!-- Error Messages -->
<?php if (isset($_SESSION['errors'])): ?>
<div class="alert error">
<ul>
<?php foreach ($_SESSION['errors'] as $error): ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php unset($_SESSION['errors']); ?>
<?php endif; ?>
<!-- Form -->
<form action="<?php echo base_url('users/store'); ?>" method="POST">
<div class="form-group">
<label for="nama">Nama:</label>
<input type="text" name="nama" id="nama" required value="<?php echo isset($_SESSION['old']['nama']) ? $_SESSION['old']['nama'] : ''; ?>">
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" name="email" id="email" required value="<?php echo isset($_SESSION['old']['email']) ? $_SESSION['old']['email'] : ''; ?>">
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" name="password" id="password" required>
</div>
<div class="form-group">
<label for="role">Role:</label>
<select name="role" id="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<button type="submit">Simpan</button>
<a href="<?php echo base_url('users'); ?>">Batal</a>
</div>
</form>
<?php unset($_SESSION['old']); ?>
</body>
</html><?php
// Method 1: View tanpa data
$this->view('home');
// Method 2: View dengan data
$this->view('users/index', array(
'users' => $users,
'title' => 'Daftar User',
'total' => count($users)
));
// Method 3: Nested view path
$this->view('admin/dashboard/index', $data);- Gunakan htmlspecialchars() untuk output user-generated content (XSS protection)
- Pisahkan logic dari view - Logic di controller/model, view hanya untuk display
- Gunakan base_url() untuk semua URL
- Flash message untuk feedback user (success, error, warning)
- Validation error tampilkan dengan user-friendly
- Old input untuk re-populate form saat validation error
<?php
// app/controllers/AuthController.php
class AuthController extends Controller
{
public function loginForm()
{
$this->view('auth/login');
}
public function login()
{
$email = clean_input($_POST['email']);
$password = $_POST['password'];
// Validation
if (empty($email) || empty($password)) {
$this->redirectBack('Email dan password harus diisi', 'error');
return;
}
// Get user by email
$user = new User();
$userData = $user->where('email', $email)->first();
// Check user exists dan password benar
if ($userData && password_verify($password, $userData->password)) {
// Check status aktif
if ($userData->status != 1) {
$this->redirectBack('Akun Anda tidak aktif', 'error');
return;
}
// Set session
$_SESSION['user_id'] = $userData->id;
$_SESSION['user_name'] = $userData->nama;
$_SESSION['user_email'] = $userData->email;
$_SESSION['user_role'] = $userData->role;
$_SESSION['logged_in'] = true;
// Update last login (optional)
$user->updateById($userData->id, array(
'last_login' => date('Y-m-d H:i:s')
));
$this->redirectTo('dashboard', 'Login berhasil', 'success');
} else {
$this->redirectBack('Email atau password salah', 'error');
}
}
}<?php
class AuthController extends Controller
{
public function registerForm()
{
$this->view('auth/register');
}
public function register()
{
// Validation
$errors = array();
if (empty($_POST['nama'])) {
$errors[] = 'Nama harus diisi';
}
if (empty($_POST['email'])) {
$errors[] = 'Email harus diisi';
} elseif (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Format email tidak valid';
}
if (empty($_POST['password'])) {
$errors[] = 'Password harus diisi';
} elseif (strlen($_POST['password']) < 6) {
$errors[] = 'Password minimal 6 karakter';
}
if ($_POST['password'] !== $_POST['password_confirmation']) {
$errors[] = 'Konfirmasi password tidak cocok';
}
// Check errors
if (count($errors) > 0) {
$_SESSION['errors'] = $errors;
$_SESSION['old'] = $_POST;
$this->redirectBack();
return;
}
// Check email already exists
$user = new User();
$exists = $user->where('email', $_POST['email'])->first();
if ($exists) {
$this->redirectBack('Email sudah terdaftar', 'error');
return;
}
// Insert new user
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email']),
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT),
'role' => 'user',
'status' => 1
);
$result = $user->insert($data);
if ($result) {
$this->redirectTo('login', 'Registrasi berhasil, silakan login', 'success');
} else {
$this->redirectBack('Gagal melakukan registrasi', 'error');
}
}
}<?php
class AuthController extends Controller
{
public function logout()
{
// Hapus semua session
session_unset();
session_destroy();
$this->redirectTo('login', 'Logout berhasil', 'success');
}
}<?php if (isset($_SESSION['logged_in']) && $_SESSION['logged_in']): ?>
<!-- User sudah login -->
<p>Welcome, <?php echo $_SESSION['user_name']; ?></p>
<a href="<?php echo base_url('logout'); ?>">Logout</a>
<?php else: ?>
<!-- User belum login -->
<a href="<?php echo base_url('login'); ?>">Login</a>
<a href="<?php echo base_url('register'); ?>">Register</a>
<?php endif; ?><?php
class DashboardController extends Controller
{
public function index()
{
// Check if user logged in
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
$this->redirectTo('login', 'Silakan login terlebih dahulu', 'warning');
return;
}
// User sudah login, tampilkan dashboard
$this->view('dashboard/index');
}
}📖 Dokumentasi lengkap: CONTOH_IMPLEMENTASI_AUTH.md
Middleware adalah filter yang dijalankan sebelum request sampai ke controller. Berguna untuk authentication, authorization, logging, dll.
- AuthMiddleware - Hanya untuk user yang sudah login
- GuestMiddleware - Hanya untuk user yang belum login
- RoleMiddleware - Filter berdasarkan role user (admin, user, dll)
Memastikan user sudah login sebelum mengakses route.
<?php
// app/middlewares/AuthMiddleware.php
class AuthMiddleware extends Middleware
{
public function handle()
{
// Cek apakah user sudah login
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
// Belum login, redirect ke login page
header('Location: ' . base_url('login'));
exit;
}
// Sudah login, lanjutkan ke controller
}
}Penggunaan di route:
<?php
array('GET', '/dashboard', 'DashboardController@index', 'auth'),
array('GET', '/profile', 'ProfileController@index', 'auth'),Hanya untuk user yang belum login (redirect jika sudah login).
<?php
// app/middlewares/GuestMiddleware.php
class GuestMiddleware extends Middleware
{
public function handle()
{
// Cek apakah user sudah login
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in']) {
// Sudah login, redirect ke dashboard
header('Location: ' . base_url('dashboard'));
exit;
}
// Belum login, lanjutkan ke controller (login/register page)
}
}Penggunaan di route:
<?php
array('GET', '/login', 'AuthController@loginForm', 'guest'),
array('GET', '/register', 'AuthController@registerForm', 'guest'),Filter berdasarkan role user (admin, moderator, user, dll).
<?php
// app/middlewares/RoleMiddleware.php
class RoleMiddleware extends Middleware
{
private $allowedRoles = array();
public function setRoles($roles)
{
$this->allowedRoles = explode(',', $roles);
}
public function handle()
{
// Cek apakah user sudah login
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
header('Location: ' . base_url('login'));
exit;
}
// Cek role user
$userRole = isset($_SESSION['user_role']) ? $_SESSION['user_role'] : '';
if (!in_array($userRole, $this->allowedRoles)) {
// Role tidak sesuai, tampilkan error
header('HTTP/1.0 403 Forbidden');
echo 'Access Denied: You do not have permission to access this page.';
exit;
}
// Role sesuai, lanjutkan ke controller
}
}Penggunaan di route:
<?php
// Hanya admin
array('GET', '/admin', 'AdminController@index', 'role:admin'),
// Hanya admin dan moderator
array('GET', '/moderator', 'ModController@index', 'role:admin,moderator'),
// Multiple roles
array('GET', '/special', 'SpecialController@index', 'role:admin,premium,vip'),<?php
// app/middlewares/CustomMiddleware.php
require_once dirname(__FILE__) . '/../core/Middleware.php';
class CustomMiddleware extends Middleware
{
public function handle()
{
// Custom logic
// Contoh: Check IP address
$allowedIPs = array('127.0.0.1', '::1');
$userIP = $_SERVER['REMOTE_ADDR'];
if (!in_array($userIP, $allowedIPs)) {
header('HTTP/1.0 403 Forbidden');
echo 'Access Denied: Your IP is not allowed.';
exit;
}
// Lanjutkan ke controller
}
}Register middleware di Router:
<?php
// app/core/Router.php
// Tambahkan di method handleMiddleware()
case 'custom':
require_once __DIR__ . '/../middlewares/CustomMiddleware.php';
$middleware = new CustomMiddleware();
$middleware->handle();
break;Penggunaan di route:
<?php
array('GET', '/secret', 'SecretController@index', 'custom'),<?php
// Middleware untuk check subscription
class SubscriptionMiddleware extends Middleware
{
public function handle()
{
// Check user login
if (!isset($_SESSION['user_id'])) {
header('Location: ' . base_url('login'));
exit;
}
// Check subscription status
$user = new User();
$userData = $user->selectOne($_SESSION['user_id']);
if (!$userData || $userData->subscription_status != 'active') {
header('Location: ' . base_url('subscribe'));
exit;
}
// Check subscription expiry
if (strtotime($userData->subscription_end) < time()) {
header('Location: ' . base_url('renew-subscription'));
exit;
}
// All checks passed
}
}📖 Dokumentasi lengkap: DOKUMENTASI_MIDDLEWARE.md
Helper functions adalah fungsi-fungsi global yang bisa dipanggil dari mana saja.
Membuat URL lengkap dari base URL aplikasi.
<?php
// Basic usage
echo base_url('users'); // http://localhost/mvc/users
echo base_url('users/create'); // http://localhost/mvc/users/create
echo base_url(); // http://localhost/mvc/
// Di view
<a href="<?php echo base_url('products'); ?>">Products</a>
<a href="<?php echo base_url('users/' . $user->id); ?>">Detail</a>
// Di form action
<form action="<?php echo base_url('users/store'); ?>" method="POST">Mengambil nilai dari file .env.
<?php
// Dengan default value
$dbHost = env('DB_HOST', 'localhost');
$appName = env('APP_NAME', 'My App');
$debug = env('APP_DEBUG', false);
// Tanpa default value
$dbName = env('DB_NAME');
// Usage example
if (env('APP_DEBUG', false)) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
}Membersihkan input dari karakter berbahaya (sanitization).
<?php
// Clean single input
$nama = clean_input($_POST['nama']);
$email = clean_input($_POST['email']);
// Clean in array
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email']),
'address' => clean_input($_POST['address'])
);
// Clean array of inputs
foreach ($_POST as $key => $value) {
$_POST[$key] = clean_input($value);
}Debug and dump data (die and dump). Menampilkan data dan stop execution.
<?php
// Dump array
$users = $user->selectAll();
dd($users);
// Dump object
$user = $user->selectOne(1);
dd($user);
// Dump multiple variables
dd($users, $products, $categories);
// Dump with label
dd('Users Data:', $users);Redirect ke URL tertentu.
<?php
// Redirect ke URL internal
redirect('users');
redirect('dashboard');
redirect('login');
// Redirect ke homepage
redirect('');
// Redirect ke external URL
redirect('http://google.com');Mengambil nilai dari session.
<?php
// Get dengan default value
$userId = session_get('user_id', 0);
$userName = session_get('user_name', 'Guest');
// Get tanpa default
$userId = session_get('user_id');
// Check if session exists
if (session_get('logged_in')) {
echo "User is logged in";
}Menyimpan nilai ke session.
<?php
// Set single value
session_set('user_id', 123);
session_set('user_name', 'John Doe');
// Set multiple values
session_set('user_id', $user->id);
session_set('user_name', $user->nama);
session_set('user_email', $user->email);
session_set('logged_in', true);Set flash message untuk notifikasi one-time.
<?php
// Success message
flash_message('Data berhasil disimpan', 'success');
// Error message
flash_message('Terjadi kesalahan', 'error');
// Warning message
flash_message('Perhatian! Data akan dihapus', 'warning');
// Info message
flash_message('Informasi penting', 'info');Display flash message di view:
<?php if (isset($_SESSION['flash_message'])): ?>
<div class="alert <?php echo $_SESSION['flash_type']; ?>">
<?php
echo $_SESSION['flash_message'];
unset($_SESSION['flash_message']);
unset($_SESSION['flash_type']);
?>
</div>
<?php endif; ?>Mengambil old input value (untuk re-populate form saat validation error).
<!-- Form with old input -->
<input type="text" name="nama" value="<?php echo old('nama'); ?>">
<input type="email" name="email" value="<?php echo old('email'); ?>">
<textarea name="address"><?php echo old('address'); ?></textarea>Check apakah user sudah login.
<?php
// Di controller
if (!is_logged_in()) {
$this->redirectTo('login', 'Please login first', 'warning');
return;
}
// Di view
<?php if (is_logged_in()): ?>
<p>Welcome, <?php echo $_SESSION['user_name']; ?></p>
<?php else: ?>
<a href="<?php echo base_url('login'); ?>">Login</a>
<?php endif; ?>Check apakah user memiliki role tertentu.
<?php
// Di controller
if (!has_role('admin')) {
$this->error('Access Denied', 403);
return;
}
// Di view
<?php if (has_role('admin')): ?>
<a href="<?php echo base_url('admin'); ?>">Admin Panel</a>
<?php endif; ?>
// Multiple roles
if (has_role('admin') || has_role('moderator')) {
// User is admin or moderator
}<?php
class UserController extends Controller
{
public function store()
{
// 1. Clean all inputs
$nama = clean_input($_POST['nama']);
$email = clean_input($_POST['email']);
$password = $_POST['password'];
// 2. Check if user logged in
if (!is_logged_in()) {
redirect('login');
return;
}
// 3. Check role
if (!has_role('admin')) {
flash_message('You are not authorized', 'error');
$this->redirectBack();
return;
}
// 4. Insert data
$user = new User();
$data = array(
'nama' => $nama,
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT)
);
$result = $user->insert($data);
// 5. Redirect with flash message
if ($result) {
flash_message('User created successfully', 'success');
redirect('users');
} else {
flash_message('Failed to create user', 'error');
$this->redirectBack();
}
}
}Template ini dilengkapi dengan berbagai fitur keamanan:
- Prepared Statements - Mencegah SQL Injection
- Input Sanitization - Membersihkan input user
- XSS Protection - Mencegah Cross-Site Scripting
- Password Hashing - Password tidak disimpan plain text
- CSRF Protection - Bisa ditambahkan untuk form
- Environment Variables - Data sensitif di .env (tidak di-commit)
Menggunakan Prepared Statements:
<?php
// ✅ AMAN - Menggunakan prepared statements
$user = $user->where('email', $email)->first();
// ✅ AMAN - Method CRUD otomatis menggunakan prepared statements
$user->insert($data);
$user->updateById($id, $data);
$user->deleteById($id);
// ❌ TIDAK AMAN - Raw query tanpa prepared statements
// JANGAN LAKUKAN INI:
// $query = "SELECT * FROM users WHERE email = '$email'";Menggunakan htmlspecialchars():
<!-- ✅ AMAN - Output di-escape -->
<p><?php echo htmlspecialchars($user->nama); ?></p>
<p><?php echo htmlspecialchars($user->email); ?></p>
<!-- ❌ TIDAK AMAN - Output langsung -->
<!-- <p><?php echo $user->nama; ?></p> -->Menggunakan clean_input():
<?php
// ✅ AMAN - Input di-sanitize
$nama = clean_input($_POST['nama']);
$email = clean_input($_POST['email']);
// ✅ AMAN - Clean semua input
$data = array(
'nama' => clean_input($_POST['nama']),
'email' => clean_input($_POST['email']),
'address' => clean_input($_POST['address'])
);
// ❌ TIDAK AMAN - Langsung dari $_POST
// $nama = $_POST['nama'];Menggunakan password_hash() dan password_verify():
<?php
// ✅ AMAN - Hash password sebelum simpan
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
$user->insert(array(
'password' => $hashedPassword
));
// ✅ AMAN - Verify password saat login
if (password_verify($_POST['password'], $user->password)) {
// Password benar
}
// ❌ TIDAK AMAN - Plain text password
// $user->insert(array('password' => $_POST['password']));
// if ($_POST['password'] === $user->password) { }Menambahkan CSRF token untuk form:
<?php
// Generate CSRF token
function generate_csrf_token() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Verify CSRF token
function verify_csrf_token($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}Di form:
<form method="POST">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<!-- form fields -->
</form>Di controller:
<?php
if (!verify_csrf_token($_POST['csrf_token'])) {
$this->error('Invalid CSRF token', 403);
return;
}Jika menambahkan fitur upload file:
<?php
function secure_upload($file, $allowed_types = array('jpg', 'png', 'pdf')) {
// Check file exists
if (!isset($file) || $file['error'] !== UPLOAD_ERR_OK) {
return false;
}
// Check file size (max 2MB)
if ($file['size'] > 2 * 1024 * 1024) {
return false;
}
// Check file extension
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_types)) {
return false;
}
// Check mime type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowed_mimes = array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
'pdf' => 'application/pdf'
);
if (!isset($allowed_mimes[$ext]) || $mime !== $allowed_mimes[$ext]) {
return false;
}
// Generate random filename
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$destination = 'uploads/' . $filename;
// Move file
if (move_uploaded_file($file['tmp_name'], $destination)) {
return $filename;
}
return false;
}# ✅ JANGAN commit file .env ke repository
# ✅ Tambahkan .env ke .gitignore
# ✅ Gunakan .env.example sebagai template
# .gitignore
.env
storage/logs/*.logTambahkan security headers di public/index.php:
<?php
// Security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
// Untuk production, gunakan HTTPS
if (env('APP_ENV') === 'production') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}- ✅ Selalu gunakan prepared statements untuk query
- ✅ Sanitize semua input dengan clean_input()
- ✅ Escape semua output dengan htmlspecialchars()
- ✅ Hash password dengan password_hash()
- ✅ Validate input di server-side (jangan hanya di client-side)
- ✅ Gunakan HTTPS untuk production
- ✅ Jangan commit .env ke repository
- ✅ Set display_errors=false di production
- ✅ Update PHP dan dependencies secara rutin
- ✅ Limit file upload size dan type
- ✅ Use CSRF protection untuk form
- ✅ Implement rate limiting untuk login
📖 Dokumentasi lengkap: DOKUMENTASI_SECURITY.md
Template ini mendukung berbagai versi PHP dengan auto-detection system.
| PHP Version | Support | Database Driver |
|---|---|---|
| PHP 5.2 | ✅ Full Support | mysql_* functions |
| PHP 5.3 - 5.6 | ✅ Full Support | PDO or mysql_* |
| PHP 7.0 - 7.4 | ✅ Full Support | PDO |
| PHP 8.0+ | ✅ Full Support | PDO |
System otomatis detect versi PHP dan menggunakan driver yang sesuai:
<?php
// app/core/Database.php
if (class_exists('PDO')) {
// PHP 7+ - Gunakan PDO
$this->connection = new PDO($dsn, $user, $pass);
} else {
// PHP 5.2 - Gunakan mysql_*
$this->connection = mysql_connect($host, $user, $pass);
}Fitur yang Didukung:
- ✅ Semua CRUD operations
- ✅ Query Builder
- ✅ Routing
- ✅ MVC pattern
- ✅ Session management
- ✅ Basic security
Database Driver:
- Menggunakan
mysql_*functions mysql_connect(),mysql_query(), dllmysql_real_escape_string()untuk escape
Fallback Functions:
<?php
// Password hashing fallback untuk PHP < 5.5
if (!function_exists('password_hash')) {
function password_hash($password, $algo) {
return md5($password . env('SECURITY_SALT', ''));
}
function password_verify($password, $hash) {
return md5($password . env('SECURITY_SALT', '')) === $hash;
}
}Fitur yang Didukung:
- ✅ Semua fitur PHP 5.2
- ✅ PDO dengan prepared statements
- ✅ Better error handling
- ✅ Native password functions
- ✅ Type declarations
- ✅ Better performance
Database Driver:
- Menggunakan PDO
- Prepared statements untuk security
- Better error handling dengan try-catch
Contoh:
<?php
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(array(':email' => $email));
} catch (PDOException $e) {
error_log($e->getMessage());
}Fitur yang Didukung:
- ✅ Semua fitur PHP 7
- ✅ Named arguments
- ✅ Match expressions
- ✅ Null safe operator
- ✅ Constructor property promotion
- ✅ Union types
Contoh PHP 8 Features:
<?php
// Named arguments
$user->insert(
data: array('nama' => 'John'),
validate: true
);
// Null safe operator
$userName = $user?->profile?->name ?? 'Guest';
// Match expression
$message = match($status) {
'success' => 'Data berhasil disimpan',
'error' => 'Terjadi kesalahan',
default => 'Status tidak diketahui'
};File _DEV/test_compatibility.php untuk test kompatibilitas:
<?php
echo "PHP Version: " . PHP_VERSION . "\n";
echo "PDO Available: " . (class_exists('PDO') ? 'Yes' : 'No') . "\n";
echo "mysql_* Available: " . (function_exists('mysql_connect') ? 'Yes' : 'No') . "\n";- Gunakan features yang compatible - Hindari PHP 8-only features jika support PHP 5.2
- Test di multiple PHP versions - Test di PHP 5.2, 7, dan 8
- Use fallback functions - Untuk features yang tidak ada di PHP lama
- Check function exists - Gunakan
function_exists()sebelum pakai function baru - Document requirements - Jelaskan minimum PHP version jika gunakan feature khusus
<?php
// ✅ BAIK - Terstruktur dan readable
class UserController extends Controller
{
public function index()
{
$user = new User();
$users = $user->where('status', 1)
->orderBy('nama', 'ASC')
->get();
$this->view('users/index', array('users' => $users));
}
}
// ❌ TIDAK BAIK - Logic di view
// Jangan campur logic kompleks di view-- ✅ BAIK - Consistent naming
tbl_user
tbl_product
tbl_category
tbl_order
-- Column names
id, nama, email, created_at, updated_at
-- ❌ TIDAK BAIK - Inconsistent
users, product, Categories, ORDER<?php
// ✅ SELALU clean input
$nama = clean_input($_POST['nama']);
// ✅ SELALU escape output
echo htmlspecialchars($user->nama);
// ✅ SELALU hash password
password_hash($password, PASSWORD_DEFAULT);
// ✅ SELALU validate di server-side
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Invalid
}<?php
// ✅ BAIK - Check result dan handle error
$result = $user->insert($data);
if ($result) {
flash_message('Success', 'success');
} else {
flash_message('Failed', 'error');
}
// ✅ BAIK - Check data exists
$userData = $user->selectOne($id);
if (!$userData) {
$this->error('Not found', 404);
return;
}<?php
// ✅ BAIK - Descriptive names
public function getActiveUsers() { }
public function getUsersByRole($role) { }
// ❌ TIDAK BAIK - Unclear names
public function get() { }
public function doSomething() { }
// ✅ BAIK - Comments untuk complex logic
/**
* Get users with pagination and filters
* @param int $page Current page
* @param int $perPage Items per page
* @param array $filters Filter conditions
* @return array Users data
*/
public function getUsersPaginated($page, $perPage, $filters = array()) { }<?php
// ✅ BAIK - Limit query results
$users = $user->limit(100)->get();
// ✅ BAIK - Use specific columns
$users = $user->select('id, nama, email')->get();
// ❌ TIDAK BAIK - Query all tanpa limit
// $users = $user->get(); // Bisa return ribuan rows<?php
// ✅ BAIK - Create reusable method
class User extends Model
{
public function getActiveUsers()
{
return $this->where('status', 1)
->orderBy('nama', 'ASC')
->get();
}
}
// Di berbagai controller
$activeUsers = $user->getActiveUsers();
// ❌ TIDAK BAIK - Repeat same query
// $users = $user->where('status', 1)->orderBy('nama', 'ASC')->get();
// Tulis query yang sama di banyak tempatPenyebab: Output sebelum redirect atau header()
Solusi:
<?php
// Pastikan tidak ada output (echo, HTML, whitespace) sebelum redirect
// ❌ SALAH
echo "Test";
$this->redirectTo('users');
// ✅ BENAR
$this->redirectTo('users');Penyebab: Konfigurasi database salah atau MySQL tidak running
Solusi:
- Check file
.env- pastikan DB_HOST, DB_NAME, DB_USER, DB_PASS benar - Pastikan MySQL service running (XAMPP: Start MySQL)
- Test connection manual via phpMyAdmin
Penyebab: File tidak di-require atau path salah
Solusi:
<?php
// Pastikan require_once path benar
require_once dirname(__FILE__) . '/../core/Model.php';
require_once dirname(__FILE__) . '/../models/User.php';Penyebab: Method tidak ada di class atau salah ketik
Solusi:
- Check nama method sudah benar
- Pastikan class extends dari base class yang benar
- Check dokumentasi method yang available
Penyebab: session_start() belum dipanggil
Solusi:
- Pastikan
session_start()ada dipublic/index.php - Check tidak ada output sebelum session_start()
Penyebab: File .env tidak ada atau path salah
Solusi:
- Copy
.env.exampleke.env - Edit
.envdan isi konfigurasi - Pastikan Env::load() dipanggil di init.php
Penyebab: BASE_URL di .env tidak sesuai
Solusi:
# Untuk XAMPP
BASE_URL=http://localhost/MVC-PHP-5-TEMPLATE/
# Untuk PHP built-in server
BASE_URL=http://localhost:8000/Penyebab: Routing tidak disetup dengan benar
Solusi:
- Check file
app/routes/routes.phpada dan benar - Pastikan .htaccess ada di folder public (jika pakai Apache)
- Untuk PHP built-in server, jalankan dari public folder:
php -S localhost:8000 -t public
- ✅ Dokumentasi lengkap dan terstruktur
- ✅ Authentication & Authorization system
- ✅ Middleware system (Auth, Guest, Role)
- ✅ Security features lengkap
- ✅ Helper functions tambahan
- ✅ Error handling improvement
- ✅ Flash message system
- ✅ Session management
- ✅ CSRF protection ready
- ✅ Environment configuration system (.env)
- ✅ Env class untuk load environment variables
- ✅ Semua config terpusat di .env
- ✅ Storage directory untuk logs & cache
- ✅ .gitignore untuk keamanan
- ✅ Dokumentasi lengkap environment config
- ✅ Kompatibilitas PHP 5.2 - 8+
- ✅ Auto-detect PDO/mysql_*
- ✅ Method CRUD lengkap dengan alias
- ✅ Query builder support
- ✅ Dokumentasi lengkap
- ✅ Contoh implementasi
- Basic MVC structure
- PDO only support
- Simple CRUD
Contributions are welcome! Jika Anda ingin berkontribusi:
- Fork repository ini
- Buat branch baru (
git checkout -b feature/AmazingFeature) - Commit changes (
git commit -m 'Add some AmazingFeature') - Push ke branch (
git push origin feature/AmazingFeature) - Buat Pull Request
MIT License - Feel free to use this template for your projects.
Created with ❤️ for maximum PHP compatibility
Untuk dokumentasi lebih detail, lihat folder _DEV/:
- DOKUMENTASI_CRUD.md - Dokumentasi lengkap CRUD operations
- DOKUMENTASI_ENV.md - Dokumentasi environment configuration
- DOKUMENTASI_MIDDLEWARE.md - Dokumentasi middleware system
- DOKUMENTASI_SECURITY.md - Dokumentasi security features
- CONTOH_IMPLEMENTASI.md - Contoh implementasi lengkap
- CONTOH_IMPLEMENTASI_AUTH.md - Contoh authentication
Jika ada pertanyaan atau issues, silakan buat issue di GitHub repository.
Happy Coding! 🚀
Last Updated: 2024 Version: 2.2.0 Compatibility: PHP 5.2, 7.x, 8.x+