What you'll learn
Templates keep presentation expressive without letting request handling, persistence, and business rules leak into every page. You'll build a small view system from first principles.
- Shape page-specific view data
- Escape values by default while allowing deliberate trusted fragments
- Extract headers, footers, and repeated fragments into partials
- Capture templates with output buffering
- Wrap pages in a shared layout
- Complete a Phase 2 server-rendered mini application structure
Keep the view boundary narrow
A controller should hand the template ready-to-display values, not an open database connection or the whole request. The template makes small presentation choices and produces HTML.
LAYER 1
Controller
Handles the request and invokes one use case.
LAYER 2
View model
Shapes the small set of values the page needs.
LAYER 3
Template
Escapes data and turns it into semantic HTML.
LAYER 4
Layout
Wraps page content in shared document structure.
| Belongs in a controller/service | Belongs in a template |
|---|---|
| Validate input and authorize access | Render already prepared errors and permissions |
| Query repositories and call domain operations | Loop over prepared records |
| Choose status codes and redirects | Produce semantic HTML for the chosen response |
| Log failures and coordinate transactions | Escape values for their output context |
Key idea
Extract shared fragments
Use require for files the page cannot render without. Anchor paths to __DIR__, and keep each partial's expected variables small and documented.
<?php
/** @var array{id:int, name:string}|null $currentUser */
?>
<header class="site-header">
<a href="/" class="brand">MasterCoding</a>
<nav aria-label="Primary navigation">
<a href="/courses">Courses</a>
<?php if ($currentUser !== null): ?>
<a href="/profile"><?= e($currentUser['name']) ?></a>
<?php else: ?>
<a href="/login">Sign in</a>
<?php endif; ?>
</nav>
</header>Do not include a request path
$_GET. User-controlled includes can expose local files or execute unintended PHP.Capture a template into a string
Output buffering lets a template use natural PHP/HTML syntax while the renderer returns its output as a string. That string can be tested, wrapped in a layout, or placed in a response object.
<?php
declare(strict_types=1);
function render(string $template, array $data = []): string
{
$base = dirname(__DIR__) . '/templates';
$path = $base . '/' . ltrim($template, '/');
if (!is_file($path)) {
throw new RuntimeException("View not found: {$template}");
}
extract($data, EXTR_SKIP);
ob_start();
try {
require $path;
return (string) ob_get_clean();
} catch (Throwable $error) {
ob_end_clean();
throw $error;
}
}About extract()
extract() is convenient only with application-owned keys and EXTR_SKIP. Never extract raw request data. Larger systems often use explicit view objects or template engines for stronger contracts.Wrap page content in one document shell
<?php
// templates/layout.php
/** @var string $title */
/** @var string $content */
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($title) ?></title>
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<?php require __DIR__ . '/partials/header.php'; ?>
<main id="main-content"><?= $content ?></main>
<?php require __DIR__ . '/partials/footer.php'; ?>
</body>
</html>The page renderer captures a page template first, then passes that trusted HTML fragment into the layout. User values inside the page template must already be escaped.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/src/view.php';
function e(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$courses = listPublishedCourses();
$content = render('courses/index.php', [
'courses' => $courses,
]);
echo render('layout.php', [
'title' => 'PHP Courses',
'content' => $content,
'currentUser' => currentUser(),
]);Create reusable view components carefully
A partial works well for structural fragments. A focused rendering function can be useful for a small repeated component with an explicit contract.
<?php
function courseCard(array $course): string
{
$title = e((string) $course['title']);
$summary = e((string) $course['summary']);
$url = '/courses/' . rawurlencode((string) $course['slug']);
return <<<HTML
<article class="course-card">
<h2><a href="{$url}">{$title}</a></h2>
<p>{$summary}</p>
</article>
HTML;
}Tip
Phase 2 project: a secure course directory
Combine the whole phase into a small server-rendered application with this structure:
course-directory/
├── public/
│ ├── index.php # front controller
│ └── assets/app.css
├── src/
│ ├── http.php # response helpers
│ ├── validation.php # field contracts
│ ├── auth.php # identity + CSRF helpers
│ └── view.php # e() + render()
├── templates/
│ ├── layout.php
│ ├── courses/index.php
│ ├── courses/create.php
│ └── partials/header.php
└── storage/courses.json- Route
GET /coursesto a searchable course list - Route
GET /courses/createto an authenticated form - Route
POST /coursesthrough CSRF, validation, storage, flash, and a 303 redirect - Escape every user-controlled value and keep storage outside
public/ - Test unknown routes, invalid fields, replayed CSRF tokens, unauthorized access, and refresh after POST
Phase 2 milestone
Recap & Phase 2 check
Key takeaways
- Controllers coordinate requests and use cases; templates turn prepared data into HTML.
- Partials share structural fragments and must use application-controlled paths.
- A renderer can capture PHP template output and return it as a string.
- Escape user values by default, but do not escape a deliberately captured trusted HTML fragment twice.
- A layout owns the shared document shell while page templates own page-specific markup.
Quick check
1. Which task does not belong in a template?
2. Why capture page output before rendering a layout?
3. Where should a partial path come from?
4. What is the Phase 2 request flow?