Chapter 15: Web Programming Basics
Modern PHP can serve as both a lightweight scripting layer and the foundation of full web applications. Even without frameworks, you can build modular structures using routing, templates, simple controllers, and consistent state management. This chapter covers the essential web architecture concepts that lead toward more scalable patterns.
Routing Strategies
Routing connects URLs to code. In plain PHP, you can dispatch requests by inspecting $_SERVER['REQUEST_URI'] and calling handlers for each path. For larger applications, a routing table or pattern matcher keeps code organized.
Using a front controller with path routing
With a single entry point such as index.php, interpret the requested path and include the appropriate handler. This keeps routing centralized.
<?php
// public/index.php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
switch ($path) {
case '/':
require '../src/home.php';
break;
case '/about':
require '../src/about.php';
break;
case '/contact':
require '../src/contact.php';
break;
default:
http_response_code(404);
echo 'Page not found';
}
Using pattern based dispatching
For dynamic URLs, match with regular expressions and extract parameters. This allows you to map routes like /posts/42 to a handler automatically.
<?php
$routes = [
'#^/posts/(?P<id>\d+)$#' => 'show_post',
'#^/users/(?P<name>[a-z]+)$#i' => 'show_user',
];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($routes as $pattern => $handler) {
if (preg_match($pattern, $path, $m)) {
require "../src/$handler.php";
exit;
}
}
http_response_code(404);
echo 'Not found';
routes.php file for clarity. Frameworks later replace this with routers and middlewares.
Templating Approaches
Templates separate presentation from logic. You can use plain PHP as a templating language, or bring in engines such as Twig or Plates for stricter separation. Each template receives data variables and produces HTML.
Embedding PHP directly within HTML
The simplest template is an HTML file with PHP echo blocks for data output. Escape variables to prevent injection.
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title><?= htmlspecialchars($title) ?></title></head>
<body>
<h1><?= htmlspecialchars($heading) ?></h1>
<p><?= htmlspecialchars($content) ?></p>
</body>
</html>
Rendering partials and layout wrappers
To reuse headers and footers, define layout templates and include content blocks. Capture output with ob_start and ob_get_clean.
<?php
// render.php
function render(string $view, array $data = []): string {
extract($data, EXTR_SKIP);
ob_start();
require __DIR__ . "/views/$view.php";
return ob_get_clean();
}
// usage
echo render('page', ['title' => 'Home', 'heading' => 'Welcome', 'content' => 'Hello world']);
Simple MVC in Plain PHP
The Model View Controller pattern organizes code into three roles: Models manage data, Views present it, and Controllers coordinate between them. You can implement this manually with small files and clear conventions.
Defining models as lightweight data classes
Models can simply be classes that query or represent data. This example uses PDO to fetch a record safely.
<?php
// src/models/Post.php
class Post {
public static function find(PDO $db, int $id): ?array {
$stmt = $db->prepare('SELECT * FROM posts WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
}
Connecting controller actions to models and views
A controller loads a model, decides what to show, and calls a template renderer. Keep business logic minimal inside controllers.
<?php
// src/controllers/show_post.php
require_once __DIR__ . '/../models/Post.php';
require_once __DIR__ . '/../render.php';
$db = new PDO('sqlite:' . __DIR__ . '/../data/blog.db');
$id = (int) ($_GET['id'] ?? 0);
$post = Post::find($db, $id);
if (!$post) {
http_response_code(404);
echo 'Not found';
exit;
}
echo render('post', ['title' => $post['title'], 'content' => $post['body']]);
models/, views/, and controllers/ directories.
State and CSRF Protection
Maintaining user state safely is critical. Always regenerate session identifiers after login and include anti CSRF tokens in forms that change data.
Embedding a CSRF token in forms
Generate a token per session, include it as a hidden field, and verify it on submission. Use random bytes for cryptographic safety.
<?php
session_start();
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf'];
?>
<form method="post">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($token) ?>">
<button>Submit</button>
</form>
Verifying and rotating CSRF tokens
Before processing any modifying request, compare the provided token with the stored value. On logout, destroy the session completely.
<?php
session_start();
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('CSRF validation failed');
}
Pagination and Sorting
When showing large data sets, limit results per page and let users navigate through pages. Combine this with ordering for a user friendly list view.
Limiting results and calculating offsets
Use LIMIT and OFFSET in SQL with parameters. Derive current page and page size safely from query input.
<?php
$page = max(1, filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['default' => 1]]));
$per = 10;
$offset = ($page - 1) * $per;
$stmt = $db->prepare('SELECT * FROM posts ORDER BY created DESC LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $per, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
Creating navigation links
Build next and previous links dynamically. Use htmlspecialchars for URLs and mark the active page clearly.
<nav>
<a href="?page=<?= $page - 1 ?>">Previous</a>
<span>Page <?= $page ?></span>
<a href="?page=<?= $page + 1 ?>">Next</a>
</nav>
id in your ORDER BY clause. This avoids duplicate or missing rows between pages.
Building a Small CRUD App
CRUD stands for Create, Read, Update, and Delete—the four basic operations for persistent data. Using the building blocks from earlier sections, you can assemble a simple but functional mini application.
Structuring directories and dependencies
Keep files grouped logically and include a bootstrap that sets up configuration and database access.
app/
public/
index.php
src/
models/
controllers/
views/
data/
blog.db
Implementing create and read actions
Use prepared statements for inserts, and display records with proper escaping.
<?php
// create_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title'] ?? '');
$body = trim($_POST['body'] ?? '');
$stmt = $db->prepare('INSERT INTO posts (title, body) VALUES (?, ?)');
$stmt->execute([$title, $body]);
header('Location: /');
exit;
}
<?php
// list_posts.php
$stmt = $db->query('SELECT id, title FROM posts ORDER BY id DESC');
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<ul>
<?php foreach ($posts as $p): ?>
<li><a href="/post.php?id=<?= $p['id'] ?>"><?= htmlspecialchars($p['title']) ?></a></li>
<?php endforeach ?>
</ul>
Updating and deleting safely
Include CSRF protection and use parameterized statements for updates and deletes. Confirm destructive actions before executing them.
<?php
// update_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)$_POST['id'];
$title = trim($_POST['title']);
$body = trim($_POST['body']);
$stmt = $db->prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?');
$stmt->execute([$title, $body, $id]);
header('Location: /post.php?id=' . $id);
exit;
}
<?php
// delete_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
$id = (int) $_POST['id'];
$stmt = $db->prepare('DELETE FROM posts WHERE id = ?');
$stmt->execute([$id]);
header('Location: /');
exit;
}