Skip to content

Instantly share code, notes, and snippets.

@abdulrahemfaqih
Last active November 9, 2025 02:53
Show Gist options
  • Select an option

  • Save abdulrahemfaqih/f6a8ae92ec4fbddb99bc35644b7f847d to your computer and use it in GitHub Desktop.

Select an option

Save abdulrahemfaqih/f6a8ae92ec4fbddb99bc35644b7f847d to your computer and use it in GitHub Desktop.
PHP MVC

Langkah-Langkah penerapan MVC menggunakan PHP

📁 Root Files

  • composer.json - Konfigurasi autoloading PSR-4 untuk namespace App

📁 public/

  • index.php - Entry point aplikasi, mengarahkan semua request ke router

📁 app/Cores/ (Framework Core)

  • 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 controller
  • Views.php - Template engine untuk rendering view dengan layout system

📁 app/Controllers/ (Business Logic)

  • CatatanController.php - Controller untuk menangani semua operasi catatan (CRUD)

📁 app/Models/ (Data Layer)

  • Catatan.php - Model untuk tabel catatans, extends dari base Model

📁 app/Views/master/ (Layout)

  • app.php - Master layout template dengan HTML structure
  • header.php - Navbar/header component
  • footer.php - Footer component

📁 app/Views/catatan/

  • index.php - Halaman list semua catatatans
  • add.php - Form untuk menambah catatatan baru
  • edit.php - Form untuk edit catatatan yang sudah ada

📁 app/

  • app.php - Konfigurasi routing aplikasi

Langkah 1: Set Up Database

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

Langkah 2: Insert Sample Data

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

Langkah 3: Setup Composer dan PSR-4 Autoloading

Fungsi: Mengatur autoloading PSR-4 untuk namespace App agar dapat menggunakan class tanpa require manual.

Jalankan command di terminal/command prompt:

composer init

Ikuti langkah-langkah berikut:

  1. Package name: php-mvc/catat-app (atau sesuai keinginan)
  2. Description: tekan Enter
  3. Author: tekan Enter
  4. Minimum Stability: tekan Enter
  5. Package Type: tekan Enter
  6. License: tekan Enter
  7. Define dependencies: tekan Enter
  8. Define dev dependencies: tekan Enter
  9. Add PSR-4 autoload mapping: ketik yes
    • Path: app/
  10. 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/"
        }
    }
}

Langkah 4: Buat Struktur Folder

Buat folder-folder berikut:

php-mvc/
├── app/
│   ├── Controllers/
│   ├── Cores/
│   ├── Models/
│   └── Views/
│       ├── master/
│       └── catatan/
└── public/

Langkah 5: Router - app/Cores/Routes.php

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";
        }
    }
}

Langkah 6: Router Configuration - app/app.php

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();

Langkah 7: Entry Point - public/index.php

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";

Langkah 8: Catatan Controller - app/Controllers/CatatanController.php.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;
    }
}

Langkah 9: View Engine - app/Cores/Views.php

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

Langkah 10: Master Layout - app/Views/master/app.php

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>

Langkah 11: Header Component - app/Views/master/header.php

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>

Langkah 12: Footer Component - app/Views/master/footer.php

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>

Langkah 13: Index View - app/Views/catatan/index.php

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("") ?>

Langkah 14: Add Form View - app/Views/catatan/add.php

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("") ?>

Langkah 15: Edit Form View - app/Views/catatan/edit.php

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("") ?>

Langkah 16: Database Connection - app/Cores/Connection.php

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

Langkah 17: Base Model - app/Cores/Model.php

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

Langkah 18: Catatan Model - app/Models/Catatan.php

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"];
}

Langkah 19: Selesai - Testing Aplikasi

Jalankan command untuk update autoload jika belum:

composer dump-autoload

Untuk testing aplikasi, jalankan PHP built-in server dengan command:

php -S localhost:8000 public/index.php

Buka 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_mvc sudah dibuat
  • Jika 404, pastikan mengakses http://localhost:8080 bukan http://localhost:8080/public
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment