Phase 2 · Web FundamentalsModule 14~40 min read

Templates, Layouts & Reusable Views

Compose maintainable server-rendered interfaces from escaped views, layouts, and partials.

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.

A maintainable rendering flow

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/serviceBelongs in a template
Validate input and authorize accessRender already prepared errors and permissions
Query repositories and call domain operationsLoop over prepared records
Choose status codes and redirectsProduce semantic HTML for the chosen response
Log failures and coordinate transactionsEscape values for their output context

Key idea

Templates may contain presentation logic—conditions and loops—but should not own business decisions or fetch their own data.

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.

templates/partials/header.php
<?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

A template name or include path must come from application-controlled routing, not directly from $_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.

src/view.php
<?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

templates/layout.php
<?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.

public/courses.php
<?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.

templates/components.php
<?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

Give a component already-shaped data and one responsibility. If it begins querying, authorizing, redirecting, or reading superglobals, move that work back across the view boundary.

Phase 2 project: a secure course directory

Combine the whole phase into a small server-rendered application with this structure:

Project 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
  1. Route GET /courses to a searchable course list
  2. Route GET /courses/create to an authenticated form
  3. Route POST /courses through CSRF, validation, storage, flash, and a 303 redirect
  4. Escape every user-controlled value and keep storage outside public/
  5. Test unknown routes, invalid fields, replayed CSRF tokens, unauthorized access, and refresh after POST

Phase 2 milestone

You can now reason about a web application without a framework: request, route, validation, state, security, use case, response, and view. Frameworks automate this structure; they do not replace it.

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?