composer.json- Konfigurasi autoloading PSR-4 untuk namespace App
index.php- Entry point aplikasi, mengarahkan semua request ke router
Connection.php- Mengelola koneksi database PDO (singleton pattern)Model.php- Base model dengan CRUD operations (Create, Read, Update, Delete)Routes.php- Router untuk menangani HTTP request dan routing ke controllerViews.php- Template engine untuk rendering view dengan layout system
CatatanController.php- Controller untuk menangani semua operasi catatan (CRUD)
Catatan.php- Model untuk tabel catatans, extends dari base Model
app.php- Master layout template dengan HTML structureheader.php- Navbar/header componentfooter.php- Footer component
index.php- Halaman list semua catatatansadd.php- Form untuk menambah catatatan baruedit.php- Form untuk edit catatatan yang sudah ada
app.php- Konfigurasi routing aplikasi
Buat database php_mvc dan tabel catatans:
CREATE DATABASE php_mvc;
USE php_mvc;
CREATE TABLE catatans (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT
);Jalankan SQL berikut untuk insert 1 data sample:
Atau jika menggunakan tabel catatans (sesuai dengan database setup di langkah 1):
INSERT INTO catatans (title, description) VALUES
('Belajar PHP MVC', 'Membuat aplikasi acatatan menggunakan PHP MVC framework dari awal');Fungsi: Mengatur autoloading PSR-4 untuk namespace App agar dapat menggunakan class tanpa require manual.
Jalankan command di terminal/command prompt:
composer initIkuti langkah-langkah berikut:
- Package name:
php-mvc/catat-app(atau sesuai keinginan) - Description: tekan Enter
- Author: tekan Enter
- Minimum Stability: tekan Enter
- Package Type: tekan Enter
- License: tekan Enter
- Define dependencies: tekan Enter
- Define dev dependencies: tekan Enter
- Add PSR-4 autoload mapping: ketik yes
- Path:
app/
- Path:
- Confirm generation: tekan Enter
Edit file composer.json yang terbuat, ganti:
"PhpMvc\\CatatApp\\": "app/"menjadi:
"App\\": "app/"Sehingga bagian autoload menjadi:
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}Buat folder-folder berikut:
php-mvc/
├── app/
│ ├── Controllers/
│ ├── Cores/
│ ├── Models/
│ └── Views/
│ ├── master/
│ └── catatan/
└── public/
Fungsi: Menangani routing HTTP request ke controller yang sesuai, mendukung parameter dinamis seperti {id}.
Buat file app/Cores/Routes.php:
<?php
namespace App\Cores;
class Routes
{
public $routes = [];
// methode GET / POST
public function get($route, $action)
{ // $route = /, /home, /siswa/{id} dll ... $action = HomeController@index
$newRoute = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[a-zA-Z0-9_/-]+)', $route);
$this->routes['GET'][$newRoute] = $action;
}
public function post($route, $action)
{ // $route = /, /home, /siswa/{id} dll ... $action = HomeController@index
$newRoute = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[a-zA-Z0-9_/-]+)', $route);
$this->routes['POST'][$newRoute] = $action;
}
public function run()
{
$url = $_SERVER['REQUEST_URI'];
$method = strtoupper($_SERVER['REQUEST_METHOD']);
foreach ($this->routes[$method] as $route => $action) {
$regex = "#^{$route}$#";
if (preg_match($regex, $url, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
$this->action($action, $params);
}
}
}
public function action($action, $params)
{
[$controller, $method] = explode("@", $action);
$controller = "App\\Controllers\\" . $controller;
if (class_exists($controller)) {
$instace = new $controller();
if (method_exists($instace, $method)) {
call_user_func_array([$instace, $method], $params);
} else {
echo "$method tidak ditemukan";
}
} else {
echo "$controller tidak ditemukan";
}
}
}Fungsi: File konfigurasi routing yang mendefinisikan semua route aplikasi dan menjalankan router.
Buat file app/app.php:
<?php
namespace App;
use App\Cores\Routes;
$routes = new Routes();
$routes->get("/", "CatatanController@index");
$routes->get("/add", "CatatanController@add");
$routes->post("/add", "CatatanController@store");
$routes->get("/edit/{id}", "CatatanController@edit");
$routes->post("/update/{id}", "CatatanController@update");
$routes->post("/delete/{id}", "CatatanController@delete");
$routes->run();Fungsi: Entry point aplikasi yang menerima semua HTTP request dan mengarahkan ke router.
Buat file public/index.php:
<?php
require __DIR__ . "/../vendor/autoload.php";
$app = require_once __DIR__ . "/../app/app.php";Fungsi: Controller yang menangani semua operasi CRUD untuk Catatan (index, add, store, edit, update, delete).
Buat file app/Controllers/CatatanController.php:
<?php
namespace App\Controllers;
use App\Cores\Views;
use App\Models\Catatan;
class CatatanController
{
public function index()
{
$catatan = new Catatan();
$data = [
"catatans" => $catatan->all()
];
echo Views::render("catatan.index", $data);
}
public function add()
{
echo Views::render("catatan.add");
}
public function store()
{
$catatan = new Catatan();
$data = $_POST;
$simpan = $catatan->create([
"title" => $data["title"],
"description" => $data["description"]
]);
if ($simpan) {
header("Location: /");
exit;
}
echo "gagal disimpan";
exit;
}
public function edit($id)
{
$catatan = new Catatan();
$data = [
"catatan" => $catatan->find($id)
];
echo Views::render("catatan.edit", $data);
}
public function update($id)
{
$catatan = new Catatan();
$data = $_POST;
$update = $catatan->update($id, [
"title" => $data["title"],
"description" => $data["description"]
]);
if ($update) {
header("Location: /");
exit;
}
echo "gagal diupdate";
exit;
}
public function delete($id)
{
$catatan = new Catatan();
$hapus = $catatan->delete($id);
if ($hapus) {
header("Location: /");
exit;
}
echo "gagal dihapus";
exit;
}
}Fungsi: Template engine untuk rendering view dengan layout system, mendukung sections dan extending master layout.
Buat file app/Cores/Views.php:
<?php
namespace App\Cores;
class Views
{
protected static $veiwPath = __DIR__ . '/../Views/';
protected static $layout;
protected static $section = [];
protected static $stackSection = [];
public static function render($view, $data = [])
{
$path = self::$veiwPath . str_replace('.', '/', $view) . '.php';
if (file_exists($path)) {
extract($data);
ob_start();
include($path);
$content = ob_get_clean();
if (self::$layout) {
ob_start();
include(self::$layout);
return ob_get_clean();
}
return $content;
} else {
throw new \Exception("View file not found : {$path}");
}
}
public static function extend($view)
{
$path = self::$veiwPath . str_replace('.', '/', $view) . '.php';
if (file_exists($path)) {
self::$layout = $path;
} else {
throw new \Exception("View file not found : {$path}");
}
}
public static function startSection($name)
{
array_push(self::$stackSection, $name);
ob_start();
}
public static function endSection()
{
$name = array_pop(self::$stackSection);
self::$section[$name] = ob_get_clean();
}
public static function yieldSection($name)
{
echo self::$section[$name] ?? '';
}
public static function include($view, $data = [])
{
$path = self::$veiwPath . str_replace('.', '/', $view) . '.php';
if (file_exists($path)) {
extract($data);
include($path);
} else {
throw new \Exception("View file not found : {$path}");
}
}
}Fungsi: Template dasar HTML yang digunakan semua halaman, dengan sections untuk content, CSS, dan JavaScript.
Buat file app/Views/master/app.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catatan App</title>
<?php App\Cores\Views::yieldSection("css") ?>
</head>
<body>
<?php App\Cores\Views::include("master.header") ?>
<main>
<?php App\Cores\Views::yieldSection("content") ?>
</main>
<?php App\Cores\Views::include("master.footer") ?>
<?php App\Cores\Views::yieldSection("js") ?>
</body>
</html>Fungsi: Komponen navbar/header yang berisi navigasi utama aplikasi.
Buat file app/Views/master/header.php:
<nav style="background-color: #f8f9fa; padding: 15px; border-bottom: 1px solid #ddd;">
<div style="max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">Catatan App</h3>
<ul style="list-style: none; margin: 0; padding: 0; display: flex; gap: 20px;">
<li><a href="/" style="text-decoration: none; color: #333;">Home</a></li>
<li><a href="/add" style="text-decoration: none; color: #333;">Tambah Catatan</a></li>
</ul>
</div>
</nav>Fungsi: Komponen footer yang berisi informasi copyright dan footer aplikasi.
Buat file app/Views/master/footer.php:
<footer style="background-color: #f8f9fa; padding: 20px; border-top: 1px solid #ddd; margin-top: 50px; text-align: center;">
<div style="max-width: 1200px; margin: 0 auto;">
<p style="margin: 0; color: #666; font-size: 14px;">© 2025 Catatan App.</p>
</div>
</footer>Fungsi: Halaman utama yang menampilkan daftar semua catatan dengan aksi edit dan hapus.
Buat file app/Views/catatan/index.php:
<?php App\Cores\Views::extend("master.app") ?>
<?php App\Cores\Views::startSection("content") ?>
<div class="catatan-container">
<h1>Daftar Catatan</h1>
<table border="1" class="catatan-table">
<thead>
<tr>
<th>No</th>
<th>Judul</th>
<th>Deskripsi</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($catatans as $index => $catatan) : ?>
<tr>
<td><?= $index + 1 ?></td>
<td><?= $catatan->title ?></td>
<td><?= $catatan->description ?></td>
<td>
<a href="/edit/<?= $catatan->id ?>">Edit</a>
<form action="/delete/<?= $catatan->id ?>" method="POST" style="display: inline;">
<button type="submit" onclick="return confirm('Yakin ingin hapus?')">Hapus</button>
</form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("css") ?>
<style>
.catatan-container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
}
.catatan-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.catatan-table th,
.catatan-table td {
padding: 10px;
text-align: left;
}
a, button {
margin-right: 10px;
text-decoration: none;
padding: 5px 10px;
cursor: pointer;
}
form {
margin: 0;
}
</style>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("js") ?>
<script>
console.log("halaman catatan loaded");
</script>
<?php App\Cores\Views::endSection("") ?>Fungsi: Form untuk menambah catatan baru dengan input title dan description.
Buat file app/Views/catatan/add.php:
<?php App\Cores\Views::extend("master.app") ?>
<?php App\Cores\Views::startSection("content") ?>
<div class="catatan-container">
<h1>Tambah Catatan Baru</h1>
<form action="/add" method="POST">
<table>
<tr>
<td>Judul:</td>
<td><input type="text" name="title" required></td>
</tr>
<tr>
<td>Deskripsi:</td>
<td><textarea name="description" rows="4" cols="50" required></textarea></td>
</tr>
<tr>
<td></td>
<td>
<button type="submit">Simpan</button>
<a href="/">Batal</a>
</td>
</tr>
</table>
</form>
</div>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("css") ?>
<style>
.catatan-container {
max-width: 500px;
margin: 20px auto;
padding: 20px;
}
table {
width: 100%;
}
td {
padding: 10px;
}
input[type="text"], textarea {
width: 100%;
padding: 5px;
}
button, a {
padding: 8px 15px;
margin-right: 10px;
text-decoration: none;
cursor: pointer;
}
</style>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("js") ?>
<script>
console.log("halaman tambah catatan loaded");
</script>
<?php App\Cores\Views::endSection("") ?>Fungsi: Form untuk mengedit catatan yang sudah ada dengan data yang sudah terisi.
Buat file app/Views/catatan/edit.php:
<?php App\Cores\Views::extend("master.app") ?>
<?php App\Cores\Views::startSection("content") ?>
<div class="catatan-container">
<h1>Edit Catatan</h1>
<form action="/update/<?= $catatan->id ?>" method="POST">
<table>
<tr>
<td>Judul:</td>
<td><input type="text" name="title" value="<?= $catatan->title ?>" required></td>
</tr>
<tr>
<td>Deskripsi:</td>
<td><textarea name="description" rows="4" cols="50" required><?= $catatan->description ?></textarea></td>
</tr>
<tr>
<td></td>
<td>
<button type="submit">Update</button>
<a href="/">Batal</a>
</td>
</tr>
</table>
</form>
</div>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("css") ?>
<style>
.catatan-container {
max-width: 500px;
margin: 20px auto;
padding: 20px;
}
table {
width: 100%;
}
td {
padding: 10px;
}
input[type="text"], textarea {
width: 100%;
padding: 5px;
}
button, a {
padding: 8px 15px;
margin-right: 10px;
text-decoration: none;
cursor: pointer;
}
</style>
<?php App\Cores\Views::endSection("") ?>
<?php App\Cores\Views::startSection("js") ?>
<script>
console.log("halaman edit catatan loaded");
</script>
<?php App\Cores\Views::endSection("") ?>Fungsi: Mengelola koneksi database menggunakan PDO dengan singleton pattern untuk efisiensi.
Buat file app/Cores/Connection.php:
<?php
namespace App\Cores;
class Connection
{
protected $host = '127.0.0.1';
protected $db = 'php_mvc';
protected $username = 'root';
protected $password = '';
protected $connect;
public function __construct()
{
try {
$rule = "mysql:host={$this->host};dbname={$this->db}";
$pdo = new \PDO($rule, $this->username, $this->password, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ,
\PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (\PDOException $e) {
throw "Connection Error: {$e->getMessage()}";
}
$this->connect = $pdo;
}
}Fungsi: Base class untuk semua model, menyediakan operasi CRUD dasar yang bisa digunakan semua model turunan.
Buat file app/Cores/Model.php:
<?php
namespace App\Cores;
use App\Cores\Connection;
class Model extends Connection
{
protected $table;
protected $primaryKey;
protected $fillable = [];
public function all()
{
$sql = "SELECT * FROM {$this->table}";
$stmt = $this->connect->prepare($sql);
$stmt->execute();
return $stmt->fetchAll();
}
public function find($id)
{
$sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = :id LIMIT 1";
$stmt = $this->connect->prepare($sql);
$stmt->bindParam(':id', $id);
$stmt->execute();
return $stmt->fetch();
}
public function create($data = [])
{
$attributes = [];
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable)) {
$attributes[$key] = $value;
}
}
$columns = array_keys($attributes);
$values = array_values($attributes);
$placeholder = array_fill(0, count($columns), '?'); // output ['?','?','?']
$sql = "INSERT INTO {$this->table} (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholder) . ")";
$stmt = $this->connect->prepare($sql);
return $stmt->execute($values);
}
public function update($id, $data = [])
{
//buat placeholders
$columns = [];
foreach ($data as $key => $value) {
$columns[] = "{$key} = :{$key}";
} // ['name = :name','email = :email', 'password = :password'];
$sql = "UPDATE {$this->table} SET " . implode(', ', $columns) . " WHERE {$this->primaryKey} = :id";
$stmt = $this->connect->prepare($sql);
$stmt->bindParam(':id', $id);
foreach ($data as $key => &$value) {
$stmt->bindParam(":{$key}", $value);
}
return $stmt->execute();
}
public function delete($id)
{
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = :id";
$stmt = $this->connect->prepare($sql);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
}Fungsi: Model spesifik untuk tabel catatans, mewarisi semua method CRUD dari base Model.
Buat file app/Models/Catatan.php:
<?php
namespace App\Models;
use App\Cores\Model;
class Catatan extends Model
{
protected $table = "catatans";
protected $primaryKey = "id";
protected $fillable = ["title", "description"];
}Jalankan command untuk update autoload jika belum:
composer dump-autoloadUntuk testing aplikasi, jalankan PHP built-in server dengan command:
php -S localhost:8000 public/index.phpBuka browser dan akses:
http://localhost:8080
Struktur final folder:
php-mvc/
├── app/
│ ├── Controllers/
│ │ └── CatatanController.php
│ ├── Cores/
│ │ ├── Connection.php
│ │ ├── Model.php
│ │ ├── Routes.php
│ │ └── Views.php
│ ├── Models/
│ │ └── Catatan.php
│ ├── Views/
│ │ ├── master/
│ │ │ ├── app.php
│ │ │ ├── header.php
│ │ │ └── footer.php
│ │ └── catatan/
│ │ ├── index.php
│ │ ├── add.php
│ │ └── edit.php
│ └── app.php
├── public/
│ └── index.php
├── vendor/
└── composer.json
Troubleshooting:
- Jika error "Class not found", jalankan
composer dump-autoload - Jika error database, pastikan MySQL running dan database
php_mvcsudah dibuat - Jika 404, pastikan mengakses
http://localhost:8080bukanhttp://localhost:8080/public