How to Structure MVC in Raw PHP

This article provides a practical guide on structuring a Model-View-Controller (MVC) architectural pattern in a raw PHP application. You will learn how to organize your directory structure, define the distinct roles of the Model, View, and Controller, and implement a basic front controller to route incoming requests without relying on heavy external frameworks.

Directory Structure

To keep your code modular and maintainable, organize your project files into a clear folder structure. Separating public-facing files from your core application logic is essential for security and organization.

my-mvc-app/
│
├── app/
│   ├── Controllers/
│   │   └── UserController.php
│   ├── Models/
│   │   └── User.php
│   └── Views/
│       └── user_profile.php
│
├── public/
│   ├── index.php
│   └── .htaccess
│
└── config.php

The Front Controller and Routing

In a raw PHP MVC setup, all requests should go through a single entry point: public/index.php. This file acts as the front controller, handling request routing and bootstrapping the application.

To direct all traffic to index.php, use an Apache .htaccess file (or equivalent Nginx configuration) inside the public/ folder:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

In public/index.php, parse the URL to determine which controller and method to instantiate and call:

<?php
// public/index.php

// Simple autoloader for classes
spl_autoload_register(function ($class) {
    $file = __DIR__ . '/../app/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

// Basic Router
$url = isset($_GET['url']) ? explode('/', filter_var(rtrim($_GET['url'], '/'), FILTER_SANITIZE_URL)) : [];

$controllerName = !empty($url[0]) ? ucfirst($url[0]) . 'Controller' : 'HomeController';
$methodName = isset($url[1]) ? $url[1] : 'index';

$controllerClass = "Controllers\\" . $controllerName;

if (class_exists($controllerClass)) {
    $controller = new $controllerClass();
    if (method_exists($controller, $methodName)) {
        // Pass remaining URL segments as parameters
        call_user_func_array([$controller, $methodName], array_slice($url, 2));
    } else {
        die("Method $methodName not found.");
    }
} else {
    die("Controller $controllerClass not found.");
}

The Model

The Model represents the data and the business logic of your application. It interacts with the database to fetch, insert, update, or delete data. It does not know anything about the View or the Controller.

<?php
// app/Models/User.php

namespace Models;

class User {
    public function getById($id) {
        // Simulation of database retrieval
        return [
            'id' => $id,
            'name' => 'John Doe',
            'email' => 'john.doe@example.com'
        ];
    }
}

The View

The View is responsible for the presentation layer. It takes the data provided by the Controller and formats it into HTML for the user. Views should contain minimal logic, limited mostly to loops and conditional rendering.

<!-- app/Views/user_profile.php -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>User Profile</title>
</head>
<body>
    <h1>User Profile</h1>
    <p><strong>ID:</strong> <?php echo htmlspecialchars($data['id']); ?></p>
    <p><strong>Name:</strong> <?php echo htmlspecialchars($data['name']); ?></p>
    <p><strong>Email:</strong> <?php echo htmlspecialchars($data['email']); ?></p>
</body>
</html>

The Controller

The Controller acts as the intermediary. It receives user input from the front controller, requests data from the Model, and passes that data to the View for rendering.

<?php
// app/Controllers/UserController.php

namespace Controllers;

use Models\User;

class UserController {
    public function profile($id) {
        // Instantiate the Model
        $userModel = new User();
        $userData = $userModel->getById($id);

        // Pass data to the View
        $this->renderView('user_profile', $userData);
    }

    private function renderView($viewName, $data = []) {
        $viewFile = __DIR__ . '/../Views/' . $viewName . '.php';
        if (file_exists($viewFile)) {
            require_once $viewFile;
        } else {
            die("View $viewName not found.");
        }
    }
}

With this setup, visiting example.com/user/profile/1 will trigger UserController->profile(1), which fetches user data from the User model and renders the user_profile view.