Building a Custom PHP MVC Framework from Scratch

Introduction to MVC Architecture

The Model-View-Controller (MVC) pattern is a software architectural design that separates an application into three main logical components: the Model (data and business logic), the View (user interface), and the Controller (handles user input). Originally developed for desktop applications, it has become a standard for web development due to its ability to decouple logic from presentation, thereby improving code organization, reusability, and maintainability.

However, MVC introduces complexity. For small projects, the overhead of creating multiple files and structures might outweigh the benefits. Debugging can also be more challenging as the flow of execution jumps between controllers, models, and views.

Framework Structure and Initialization

A robust directory structure is essential for an MVC application. A typical structure separates the framework core, application logic (controllers, models, views), configuration, and public assets.

  • App: Contains Controllers, Models, and Views (split by Platform/Module).
  • Frame: Core libraries (Database, Template Engine, Base classes).
  • Config: Configuration files (database credentials, settings).
  • Public: Entry point and static assets (CSS, JS, images).

The Entry Point and Request Dispatching

The application uses a single entry point, typically index.php, to handle all requests. This file bootstraps the framework. It defines path constants, loads configuration, and determines which controller and action to execute based on URL parameters (e.g., p for platform, c for controller, a for action).

class Bootstrap {
    public static function run() {
        self::initConstants();
        self::initConfig();
        self::initDispatchParams();
        self::autoload();
        self::dispatch();
    }

    private static function initDispatchParams() {
        $platform = $_GET['p'] ?? 'Home';
        $controller = $_GET['c'] ?? 'Index';
        $action = $_GET['a'] ?? 'index';
        
        define('PLATFORM', $platform);
        define('CONTROLLER', $controller);
        define('ACTION', $action);
    }

    private static function dispatch() {
        $controllerName = CONTROLLER . 'Controller';
        $actionName = ACTION . 'Action';
        
        if (class_exists($controllerName)) {
            $instance = new $controllerName();
            if (method_exists($instance, $actionName)) {
                $instance->$actionName();
            }
        }
    }
}

Class Autoloading

To manage class inclusion efficiently, we register an autoloader using spl_autoload_register. This function attempts to load a class file when it is first instantiated, based on naming conventions (e.g., ArticleModel maps to the Model directory).

public static function loadClass($className) {
    $map = [
        'Controller' => FRAME_PATH . 'Controller.php',
        'Model' => FRAME_PATH . 'Model.php',
        'Database' => FRAME_PATH . 'Database.php',
    ];
    
    if (isset($map[$className])) {
        require $map[$className];
    } elseif (substr($className, -10) === 'Controller') {
        require APP_PATH . PLATFORM . '/Controller/' . $className . '.php';
    } elseif (substr($className, -5) === 'Model') {
        require APP_PATH . PLATFORM . '/Model/' . $className . '.php';
    }
}

Database Abstraction Layer

The framework uses an interface to define database operations, allowing for flexibility in the underlying database driver. The primary implementation uses PDO (PHP Data Objects) for secure and efficient data access.

PDO Implementation

The database class implements a singleton pattern to ensure a single connection instance. It supports common operations like fetching all rows, a single row, or a single column.

class PDODB implements IDatabase {
    private static $instance;
    private $pdo;

    public static function getInstance($config) {
        if (!self::$instance) {
            $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8";
            self::$instance = new self($dsn, $config['user'], $config['pass']);
        }
        return self::$instance;
    }

    public function fetchAll($sql) {
        try {
            $stmt = $this->pdo->query($sql);
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $e) {
            error_log($e->getMessage());
            return false;
        }
    }
}

Model and Factory Pattern

The base Model class provides a connection to the database. Specific models (e.g., ArticleModel) inherit this and encapsulate data logic. A Factory class can be used to instantiate models efficiently, ensuring we reuse instances where possible.

class Model {
    protected $db;
    
    public function __construct() {
        $this->db = PDODB::getInstance(Config::get('database'));
    }
}

class ModelFactory {
    private static $instances = [];

    public static function create($className) {
        if (!isset(self::$instances[$className])) {
            self::$instances[$className] = new $className();
        }
        return self::$instances[$className];
    }
}

Controller and View Integration

Controllers handle user requests and return responses. A base Controller class initializes the template engine (e.g., Smarty) and provides helper methods for redirects and input filtering.

Template Engine Setup

class Controller {
    protected $template;

    public function __construct() {
        $this->template = new Smarty();
        $this->template->setTemplateDir(APP_PATH . PLATFORM . '/View/');
        $this->template->setCompileDir(APP_PATH . 'Runtime/Compile/');
    }

    protected function redirect($url, $message = '', $delay = 3) {
        if (empty($message)) {
            header("Location: $url");
        } else {
            // Render a temporary redirect page with a message
            echo "<script>setTimeout(function(){{window.location='$url';}}, $delay * 1000);</script>";
            echo $message;
        }
        exit;
    }
}

Input Filtering and Security

To prevent SQL injection and XSS, all user input should be sanitized. While prepared statements (PDO) handle SQL injection effectively, filtering input data is a good practice.

protected function sanitize($data) {
    return addslashes(strip_tags(trim($data)));
}

Essential Utility Classes

Pagination

A pagination class calculates the offset, limit, and page links based on the total number of records and the current page number.

class Paginator {
    public function __construct($total, $perPage, $currentPage, $url) {
        $this->totalPages = ceil($total / $perPage);
        $this->currentPage = max(1, min($this->totalPages, $currentPage));
        // Logic to generate HTML links...
    }
}

File Uploads

An upload class manages file validation (type, size), creates random filenames to prevent collisions, and moves files to the target directory.

class Upload {
    public function save($file, $targetDir) {
        $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
        $filename = uniqid() . '.' . $ext;
        $destination = $targetDir . '/' . $filename;
        if (move_uploaded_file($file['tmp_name'], $destination)) {
            return $filename;
        }
        return false;
    }
}

Image Processing

The image class handles thumbnail generation. It creates a canvas with a specific background color, resizes the source image proportionally, and centers it on the canvas.

Advanced Features

Infinite Classification (Recursive Categories)

For nested categories (e.g., forums, product categories), a recursive function is used to generate a tree structure. The database typically uses an ID and Parent ID structure.

class CategoryModel extends Model {
    public function getTree($pid = 0, $level = 0) {
        static $tree = [];
        $sql = "SELECT * FROM categories WHERE parent_id = $pid";
        $rows = $this->db->fetchAll($sql);
        
        foreach ($rows as $row) {
            $row['level'] = $level;
            $tree[] = $row;
            $this->getTree($row['id'], $level + 1);
        }
        return $tree;
    }
}

Login and Permissions

A session-based authentication system checks user credentials against the database. A PlatformController for the admin area can enforce login checks before executing any actions.

class AdminController extends Controller {
    public function __construct() {
        parent::__construct();
        session_start();
        if (!isset($_SESSION['user_id'])) {
            $this->redirect('/login.php');
        }
    }
}

CAPTCHA Generation

Generating a CAPTCHA involves creating a blank image, adding random noise (lines and dots), and drawing random characters. The generated string is stored in the session for verification.

class Captcha {
    public function render() {
        $image = imagecreatetruecolor(100, 30);
        $bgColor = imagecolorallocate($image, 255, 255, 255);
        imagefill($image, 0, 0, $bgColor);
        
        $code = substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZ23456789'), 0, 4);
        $_SESSION['captcha'] = $code;
        
        $textColor = imagecolorallocate($image, 0, 0, 0);
        imagestring($image, 5, 20, 5, $code, $textColor);
        
        header('Content-type: image/png');
        imagepng($image);
        imagedestroy($image);
    }
}

This custom framework approach provides a solid foundation for web applications like blogs or forums, allowing developers to have full control over the architecture while practicing key software engineering patterns.

Tags: PHP mvc web development Framework Software Architecture

Posted on Mon, 18 May 2026 18:45:35 +0000 by stenk