⚠️ Warning ⚠️ Deprecated Code! This video tutorial contains outdated code.
πŸ’‘ If you wish to update it, any AI assistant will update the code for you in seconds.

PHP Custom Modular Project Architecture Tutorial Part 2

Published : December 7, 2025   •   Last Edited : December 9, 2025   •   Author : Adam Khoury

Welcome back to Part 2. If you’re tired of slow websites and massive framework overhead, this tutorial is for you. We’re deep-diving into the Custom PHP Template-Based Dynamic Paging architecture that powers my website, delivering elite Lighthouse scores in Performance, Best Practices, and SEO categories, offering lightning-fast loading and development speed.

In this tutorial, you'll learn how to implement the key systems that make this "no-bloat" approach secure and maintainable.

Key Learning Points:

Secure Database Wrapper (Singleton Pattern): See how to build a custom PHP PDO class using the Singleton pattern to ensure one efficient connection and prevent resource waste. We use prepare() and charset=utf8mb4 to achieve high level SQL injection protection. Learn about beginTransaction() for large database transaction, giving you the power to commit() or rollback().

Dynamic Paging: Using the database we set up the dynamic paging system to allow thousands or zillions of pages to render through a single master file.

Templating the Two-Step View: We dissect the initial HTML structure into reusable PHP templates (doctype.php, doctop.php, docbtm.php) to keep presentation logic separate and layout changes a breeze.

This architecture offers the speed of a static site with the power of a dynamic application. You can choose to stop relying on heavy frameworks and start building your own custom, high-performance solution today!

Database.php - goes into the server/objects folder ( PDO Class Object for DB Interaction )
<?php

class Database {
    
    // Singleton pattern properties
    private static $instance = null;
    private ?PDO $pdo = null; // Type hint for internal PDO object

    // Database connection details
    private $host = 'localhost';
    private $db_name = 'your_db_name'; 
    private $user = 'your_db_user';
    private $pass = 'your_db_pass';

    // Private Constructor: Establishes connection only once
    private function __construct() {
        
        $dsn = "mysql:host={$this->host};dbname={$this->db_name};charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];

        try {
            $this->pdo = new PDO($dsn, $this->user, $this->pass, $options);
        } catch (\PDOException $e) {
            error_log("Database Connection Error: " . $e->getMessage());
            // In a production environment, never expose the full error message
            die("Database connection failed. Please check logs.");
        }
    }

    // Prevents cloning and unserialization to maintain the singleton integrity
    private function __clone() { }
    public function __wakeup() { throw new Exception("Cannot unserialize a singleton."); }

    // Static access point (The Singleton)
    public static function getInstance(): Database {
        
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // Optional: Gets the internal PDO connection object for raw PDO access.
    // @return PDO|null The PDO object, or null if the connection is closed.
    public function getPdo(): ?PDO {
        return $this->pdo;
    }
    
    // return string|false The last insert ID, or false on failure.
    public function lastInsertId(): string|false {
        
        // Check if the PDO connection is still active before calling the method
        if ($this->pdo) {
            return $this->pdo->lastInsertId();
        }
        return false;
    }

    // --- QUERY EXECUTION (Prepare/Execute) ---
    public function query(string $sql, array $params = []): \PDOStatement {
        
        if (!$this->pdo) {
             throw new \Exception("Cannot execute query: Database connection is closed.");
        }
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        
        return $stmt;
    }

    // --- TRANSACTION METHODS ---
    public function beginTransaction(): bool {
        
        return $this->pdo->beginTransaction();
    }

    public function commit(): bool {
        
        return $this->pdo->commit();
    }

    public function rollBack(): bool {
        
        return $this->pdo->rollBack();
    }

    // --- UTILITY METHODS ---
    public function numRows(\PDOStatement $stmt): int {
        
        return $stmt->rowCount();
    }

    public function closeConnection(): void {
        
        $this->pdo = null;
        self::$instance = null;
    }
}
config.php - goes into the server folder
<?php
$base_url = "https://www.adamkhoury.com";
$css_core = "css/core.css";
$js_core = "js/core.js";
$doctype = "server/templates/doctype.php";
$doctop = "server/templates/doctop.php";
$docbtm = "server/templates/docbtm.php";
// Version core.css or core.js to cache bust them when needed
// ( ie. core3.css, core1.js )
// Set absolute paths to files if needed for pretty url false directories -
// Or if you have nested directories that you want to add files in
// ( "/css/core.css" and "/js/core.js" )
// Use a .env file instead of .php if you ever want to put secret or -
// sensitive data into this configuration file
?>
article.php - goes in root directory
<?php 
// Error Reporting
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);

// Get all of the configuration strings
require_once 'server/config.php';

// Initialize page output variables
$id = "";
$title = "";
$description = "";
$formatted_date = "";

// Check that the id is present in URL address bar
if(isset($_GET["id"]) && $_GET["id"] != ""){
	$id = preg_replace('#[^0-9]#', '', $_GET['id']);
	if($id == ""){
		echo "ID parameter value is missing after sanitizing it.";
        exit();
	}
} else {
    echo "ID parameter is missing.";
    exit();
}

// Include database class file
require_once 'server/objects/Database.php';
// Connect to database using the Singleton Pattern
$db = Database::getInstance();

// Query the specific article row
$sql = "SELECT title, description, pubdate FROM articles WHERE id = :id LIMIT 1";
$params = [":id" => $id];
$stmt = $db->query($sql, $params);
$numrows = $db->numRows($stmt);

// Check to make sure that article exists
if($numrows == 0){
    echo "That article does not exist.";
    exit();
}

// Get all of the article data into local variables for echo into the page
while ($row = $stmt->fetch()) {
    
    $title = $row["title"];
    $description = $row["description"];
    $pubdate = $row["pubdate"];
    $formatted_date = date("F j, Y", strtotime($pubdate));

}

// Close database connection
$db->closeConnection();

// Set any variables that need to go into your <head> element
$doc_title = $title;

// Install the <doctype>, <html>, <head>, <meta>, <link>, etc. tags
require_once $doctype;

?>

<style>
    #artlinks { line-height: 2em; }
</style>
</head>
<body>
    
    <?php require_once $doctop; ?>
    
    <h2><?php echo $title; ?></h2>
    <p><?php echo $description; ?></p>
    <p><?php echo $formatted_date; ?></p>

    <?php require_once $docbtm; ?>
    
</body>
</html>
index.php - goes in the root directory
<?php 
// Error Reporting
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);

// Get all of the configuration strings
require_once 'server/config.php';

// Include database class file
require_once 'server/objects/Database.php';

// Connect to database using the Singleton Pattern
$db = Database::getInstance();

// Get the newest 4 articles from the database
$html = "";
$sql = "SELECT id, title FROM articles ORDER BY pubdate DESC LIMIT 4";
$stmt = $db->query($sql, []);
while ($row = $stmt->fetch()) {
    $id = $row["id"];
    $title = $row["title"];
    $html .= '<a href="article.php?id='.$id.'">'.$title.'</a> <br>';
}

// Close database connection
$db->closeConnection();

// Set any variables that need to go into your <head> element
$doc_title = "AK Dev Tutorials | Home";
// Install the <doctype>, <html>, <head>, <meta>, <link>, etc. tags
require_once $doctype;

?>

<style>
    #artlinks { line-height: 2em; }
</style>
</head>
<body>
    
    <?php require_once $doctop; ?>
    
            <h2>Welcome to the Developer's Corner</h2>
            <p>Your resource for full-stack programming and core focused web development tutorials since 2008.</p>
            <h3>Latest Articles</h3>
            <div id="artlinks"><?php echo $html; ?></div>
        
    <?php require_once $docbtm; ?>
    
</body>
</html>
doctype.php - goes in server/templates folder
<!DOCTYPE html>
<html lang="en">
<head>
    
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $doc_title; ?></title>
    <link rel="stylesheet" href="<?php echo $css_core; ?>">
    <script src="<?php echo $js_core; ?>" defer></script>
doctop.php - goes in server/templates folder
<header class="masthead">
    <div class="container">
        <h1 class="logo">Adam Khoury Dev</h1>
        <nav class="main-nav">
            <a href="#videos">Videos</a>
            <a href="#articles">Articles</a>
            <a href="contact.php">Contact</a>
        </nav>
    </div>
</header>

<main class="page-content">
    <div class="container">
docbtm.php - goes in server/templates folder
</div>
</main>

<footer class="site-footer">
<div class="container">
    <p>&copy; 2025 Adam Khoury Dev. All rights reserved.</p>
</div>
</footer>
core.js - goes into the js folder
// js/core.js

document.addEventListener('DOMContentLoaded', () => {
    // Basic shared utility function, e.g., for logging a message
    console.log("AK Dev Core JavaScript initialized.");

    // Demo: Highlighting the current menu item based on the URL (simple version)
    const navLinks = document.querySelectorAll('.main-nav a');
    const currentPath = window.location.pathname;

    navLinks.forEach(link => {
        // A more robust solution is needed for a true active state, 
        // but this demonstrates a shared client-side script.
        if (link.href.includes(currentPath) && currentPath !== '/') {
            // link.classList.add('active'); 
        }
    });

    // You would add common utilities here, like simple form validation helpers or modal logic.
});
core.css - goes into the css folder
/* --- CSS Reset and Base Styles --- */
*, *::before, *::after {
    box-sizing: border-box; /* Crucial for responsive design */
    margin: 0;
    padding: 0;
}

body {
    font-family: sans-serif;
    line-height: 1.6;
    color: #333;
    background-color: #f4f7f6;
    min-height: 100vh;
    display: flex;
    flex-direction: column; /* Allows footer to stick to the bottom */
}

.container {
    width: 90%;
    max-width: 1200px; /* Limits width on large screens */
    margin: 0 auto;
    padding: 10px 0;
}

/* --- Header/Masthead Styles --- */
.masthead {
    background-color: #0056b3; /* A deep blue for your branding */
    color: #fff;
    padding: 10px 0;
    border-bottom: 3px solid #003d80;
}

.masthead .container {
    display: flex;
    justify-content: space-between; /* Logo on left, Nav on right */
    align-items: center;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
}

.main-nav a {
    color: #fff;
    text-decoration: none;
    margin-left: 20px;
    padding: 5px;
    transition: color 0.2s;
}

.main-nav a:hover {
    color: #ffcc00; /* Highlight on hover */
}

/* --- Main Content Styles --- */
.page-content {
    flex-grow: 1; /* Pushes the footer down */
    padding: 40px 0;
}

/* --- Footer Styles --- */
.site-footer {
    background-color: #0056b3;
    color: #ccc;
    text-align: center;
    padding: 15px 0;
    margin-top: auto; /* Ensures it sits at the bottom */
}