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
app/: Contains the core logic of the application, separated into Controllers, Models, and Views.public/: The web root. Only files inside this folder are accessible to the public. It contains the entry point (index.php) and static assets (CSS, JS).config.php: Holds configuration settings, such as database credentials.
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.