Templating And Views
Every PHP application eventually faces the same problem: HTML and business logic tangled together in the same file. Reading a query, building an array, and then immediately echo-ing raw HTML feels quick at first — but it becomes unmaintainable fast. Templating is the discipline of keeping those two concerns separate. Your PHP code collects and prepares data; your template files render it.
This lesson covers native PHP templates, layout inheritance using includes, escaping output safely, and building a minimal view renderer from scratch. No third-party libraries required.
Why Separate Logic From Presentation
Consider the alternative — logic and markup interleaved:
<?php
// Everything in one place — hard to test, hard to reuse
$user = ['name' => 'Alice', 'role' => 'admin'];
echo "<html><body>";
echo "<h1>Hello, " . $user['name'] . "!</h1>";
if ($user['role'] === 'admin') {
echo "<p>You have admin access.</p>";
}
echo "</body></html>";
This works for toy examples, but grows painful quickly. When your designer edits the HTML or your logic changes, you're editing the same file for entirely different reasons. The goal of templating is to give each concern its own file.
Native PHP Templates
PHP was originally a templating language, and it still excels at it. A .php file used purely for output — with minimal logic — is a perfectly valid template. The alternative syntax for control structures makes templates easier to read:
<?php
$products = [
['name' => 'Keyboard', 'price' => 79.99, 'inStock' => true],
['name' => 'Monitor', 'price' => 349.00, 'inStock' => false],
['name' => 'Mouse', 'price' => 29.99, 'inStock' => true],
];
function e(string $value): string {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<ul>
<?php foreach ($products as $product): ?>
<li>
<strong><?= e($product['name']) ?></strong> —
$<?= e((string) $product['price']) ?>
<?php if (!$product['inStock']): ?>
<em>(out of stock)</em>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
The <?= short tag is shorthand for <?php echo. The e() helper wraps htmlspecialchars() — always escape user-supplied or dynamic data before rendering it in HTML. Never echo raw values into the page.
Building a Simple View Renderer
Rather than manually including template files everywhere, a small view renderer centralises the process. It uses output buffering to capture rendered HTML and return it as a string:
<?php
function renderView(string $template, array $data = []): string {
// Make each key available as a variable inside the template
extract($data, EXTR_SKIP);
ob_start();
// In a real app, this would include a file: include $template;
// Here we simulate the template inline using a closure
$templateFn = function () use ($data) {
$name = htmlspecialchars($data['name'] ?? 'Guest', ENT_QUOTES, 'UTF-8');
$score = (int) ($data['score'] ?? 0);
echo "<h1>Welcome, {$name}!</h1>\n";
echo "<p>Your score is: <strong>{$score}</strong></p>\n";
if ($score >= 90) {
echo "<p>Excellent work!</p>\n";
} elseif ($score >= 60) {
echo "<p>Good job — keep it up.</p>\n";
} else {
echo "<p>Keep practising.</p>\n";
}
};
$templateFn();
return ob_get_clean();
}
$html = renderView('profile', [
'name' => 'Alice',
'score' => 92,
]);
echo $html;
ob_start() begins output buffering — anything echo-ed after this point is captured rather than sent to the browser. ob_get_clean() returns the buffered content and clears the buffer. This pattern lets you treat rendered HTML as a value you can return, cache, or modify before sending.
Layout Inheritance With Includes
Most pages share a common shell: a <head>, navigation, and footer. Rather than duplicating that HTML, you split it into partials and compose them:
<?php
// Simulate a layout + partial system using strings
function layout(string $title, string $content): string {
$safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{$safeTitle}</title>
</head>
<body>
<nav><a href="/">Home</a> | <a href="/about">About</a></nav>
<main>
{$content}
</main>
<footer><p>© 2026 My App</p></footer>
</body>
</html>
HTML;
}
function partial(string $type, array $items): string {
if ($type === 'list') {
$rows = array_map(
fn($item) => '<li>' . htmlspecialchars($item, ENT_QUOTES, 'UTF-8') . '</li>',
$items
);
return '<ul>' . implode("\n", $rows) . '</ul>';
}
return '';
}
$listHtml = partial('list', ['Introduction', 'Core Concepts', 'Exercises']);
$pageHtml = layout('Course Outline', "<h1>Course Outline</h1>\n" . $listHtml);
// Print just the body section for readability
preg_match('/<main>(.*?)<\/main>/s', $pageHtml, $match);
echo trim($match[1] ?? '');
In a real project, layout() and partial() would each correspond to a .php file you include. The key insight is that a layout is just a function (or file) that accepts a content string and wraps it in shared structure.
Escaping Output Correctly
Output escaping is not optional. Failing to escape dynamic content opens you up to cross-site scripting (XSS). Different contexts require different escaping:
<?php
function eHtml(string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function eAttr(string $v): string {
// Same function — htmlspecialchars covers HTML attributes too
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function eUrl(string $v): string {
return urlencode($v);
}
$userInput = '<script>alert("xss")</script>';
$searchTerm = 'hello world & more';
$userId = 'user@example.com';
// HTML context — tags are neutralised
echo "<p>" . eHtml($userInput) . "</p>\n";
// Attribute context — safe inside an attribute value
echo '<input value="' . eAttr($searchTerm) . "\">\n";
// URL context — special characters encoded
echo '<a href="/search?q=' . eUrl($searchTerm) . "\">Search</a>\n";
// Profile link
echo '<a href="/user/' . eUrl($userId) . "\">Profile</a>\n";
The rule: escape at the point of output, not at the point of input. Escaping on input loses information (you might need the raw value later). Escaping on output ensures the correct escaping for each context.
Try It Yourself
Build a small card component renderer. Given an array of blog post data, render a list of HTML cards with title, excerpt, and a read-more link. Escape all output correctly.
<?php
function e(string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function renderCard(array $post): string {
$title = e($post['title'] ?? 'Untitled');
$excerpt = e($post['excerpt'] ?? '');
$slug = e($post['slug'] ?? '#');
return <<<HTML
<div class="card">
<h2>{$title}</h2>
<p>{$excerpt}</p>
<a href="/posts/{$slug}">Read more</a>
</div>
HTML;
}
$posts = [
[
'title' => 'Getting Started With PHP',
'excerpt' => 'PHP is a versatile server-side language used by millions of sites.',
'slug' => 'getting-started-with-php',
],
[
'title' => 'Understanding Arrays',
'excerpt' => 'Arrays in PHP are ordered maps — powerful and flexible.',
'slug' => 'understanding-arrays',
],
[
'title' => 'Form Handling & Validation',
'excerpt' => 'Safely reading and validating user input is critical for security.',
'slug' => 'form-handling-validation',
],
];
$cards = array_map('renderCard', $posts);
echo implode("\n", $cards);
Try extending renderCard() to accept an optional $tag parameter (e.g. "New", "Featured") and render it as a badge in the top corner. Make sure the badge value is escaped too.
Key Takeaways
- Separate data preparation (PHP logic) from rendering (HTML output) — one concern per file
- Use
<?= e($value) ?>as your default output pattern; never echo raw dynamic values into HTML - Output buffering (
ob_start/ob_get_clean) lets you capture rendered output as a string, enabling composable view functions - Split shared page structure into layout and partial files included at render time
- Escape for context:
htmlspecialcharsfor HTML and attributes,urlencodefor URL segments - A minimal view renderer is just a function: receive data, include a template, return captured HTML
Pro Tip: Build the habit of treating every template variable as untrusted until it has been escaped. A simple convention — wrapping all template output in
e()— eliminates an entire class of XSS vulnerabilities. When you eventually adopt a mature templating engine like Twig or Blade, you'll find they auto-escape by default for exactly this reason.
Next Steps
You now know how to separate PHP logic from HTML presentation using layouts, partials, output buffering, and context-aware escaping. Your views are clean, composable, and secure.
Some operations are too slow to run during a web request -- sending emails, processing images, generating reports, syncing with external APIs. If you make users wait for these tasks, page loads become sluggish and timeouts become common. Queues and jobs teaches you to move time-consuming work into the background, keeping your application responsive while heavy tasks execute asynchronously. This is how production PHP applications handle scale.