π‘ 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
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>© 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 */
}