Templates and Output Buffering

As pages grow, mixing database queries, form handling and HTML in one file becomes a mess. The fix is to separate logic (prepare the data) from presentation (templates that only display it). PHP is itself a template language, and output buffering lets you capture a template's output as a string — the basis of layouts and view rendering in every PHP framework.

Separating logic and templates

A template is a PHP file containing mostly HTML, with small bits of PHP to print variables and loop. The controller script prepares the data, then includes the template:

PHP templates/products.php
<h1><?= htmlspecialchars($title) ?></h1>
<?php if ($products === []): ?>
<p>No products found.</p>
<?php else: ?>
<ul>
<?php foreach ($products as $p): ?>
  <li><?= htmlspecialchars($p['name']) ?> — <?= number_format($p['price'], 2) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
PHP products.php
<?php
// logic: gather and prepare data (in a real app, from a database)
$all = [
    ['name' => 'Desk lamp', 'price' => 34.5],
    ['name' => 'Notebook <A5>', 'price' => 3.5],
    ['name' => 'Chair', 'price' => 129],
];
$max = (float) ($_GET['max'] ?? PHP_INT_MAX);
$products = array_filter($all, fn($p) => $p['price'] <= $max);
$title = 'Products';

// presentation
require __DIR__ . '/templates/products.php';
HTTP
GET /products.php?max=50
Responses
> GET /products.php?max=50
<h1>Products</h1>
<ul>
  <li>Desk lamp — 34.50</li>
  <li>Notebook &lt;A5&gt; — 3.50</li>
</ul>

An included file shares the variables of the code that includes it, so $title and $products are available in the template. Templates use the alternative syntax (foreach … : / endforeach;) and <?= ?>, which keep the HTML readable.

Output buffering

Normally output goes straight to the browser. ob_start() starts collecting it in a buffer instead; ob_get_clean() returns what was collected and stops buffering:

PHP
<?php

ob_start();
echo "Hello, ";
echo "buffer!";
$captured = ob_get_clean();

echo "Captured ", strlen($captured), " characters: [", strtoupper($captured), "]\n";
Output
Captured 14 characters: [HELLO, BUFFER!]

Buffering means headers can still be sent after some output has been generated, and that output can be modified, cached or discarded — for example, replaced by an error page if something fails halfway through.

A render function

Combining buffering with extract (which turns array keys into variables) gives a tiny view renderer. Wrapping the include in a function keeps the template from seeing anything except the variables you pass:

PHP templates/card.php
<div class="card">
  <h2><?= $e($name) ?></h2>
  <p><?= $e($bio) ?></p>
</div>
PHP main.php
<?php

function render(string $template, array $data = []): string
{
    $data['e'] = fn(?string $v) => htmlspecialchars($v ?? '', ENT_QUOTES);
    extract($data);
    ob_start();
    require __DIR__ . "/templates/$template.php";
    return ob_get_clean();
}

echo render('card', ['name' => 'Ada Lovelace', 'bio' => 'Wrote the first program & more.']);
echo render('card', ['name' => 'Grace Hopper', 'bio' => '<b>Admiral</b>, compiler pioneer.']);
Output
<div class="card">
  <h2>Ada Lovelace</h2>
  <p>Wrote the first program &amp; more.</p>
</div>
<div class="card">
  <h2>Grace Hopper</h2>
  <p>&lt;b&gt;Admiral&lt;/b&gt;, compiler pioneer.</p>
</div>

Layouts

Most pages share a header, navigation and footer. Render the page content first, then pass it into a layout template:

PHP templates/layout.php
<!DOCTYPE html>
<html lang="en">
<head><title><?= $e($title) ?> · My Shop</title></head>
<body>
<nav><a href="/">Home</a> <a href="/about.php">About</a></nav>
<main>
<?= $content ?>
</main>
<footer>&copy; <?= $year ?> My Shop</footer>
</body>
</html>
PHP templates/about.php
<h1>About us</h1>
<p>We sell <?= $e($what) ?> since <?= $since ?>.</p>
PHP about.php
<?php

function render(string $template, array $data = []): string
{
    $data['e'] = fn(?string $v) => htmlspecialchars($v ?? '', ENT_QUOTES);
    extract($data);
    ob_start();
    require __DIR__ . "/templates/$template.php";
    return ob_get_clean();
}

$content = render('about', ['what' => 'lamps & desks', 'since' => 2019]);
echo render('layout', ['title' => 'About', 'content' => $content, 'year' => 2026]);
HTTP
GET /about.php
Responses
> GET /about.php
<!DOCTYPE html>
<html lang="en">
<head><title>About · My Shop</title></head>
<body>
<nav><a href="/">Home</a> <a href="/about.php">About</a></nav>
<main>
<h1>About us</h1>
<p>We sell lamps &amp; desks since 2019.</p>
</main>
<footer>&copy; 2026 My Shop</footer>
</body>
</html>

The page content is escaped as it's rendered, and then inserted into the layout without escaping — it's already HTML. Be careful to only output trusted HTML unescaped.

Template engines

Larger projects usually use a dedicated template engine, installed with Composer:

  • Twig (Symfony) — {{ product.name }}, escaping by default, template inheritance with {% extends 'layout.html.twig' %}.
  • Blade (Laravel) — {{ $product->name }}, @foreach, @extends and components.

Both compile to plain PHP behind the scenes. Their main advantage is safety: output is escaped unless you explicitly ask otherwise, so forgetting htmlspecialchars is no longer possible.

Other uses of output buffering

  • Capturing functions that print — wrap var_dump or print_r to get their output as a string (or use print_r($x, true)).
  • Page caching — capture a generated page, save it to a file, and serve the file next time.
  • Post-processing — ob_start($callback) passes the whole output through a function before it's sent, for example to minify HTML.
PHP
<?php

$html = (function () {
    ob_start(fn(string $buffer) => preg_replace('/\s+/', ' ', $buffer));
    ?>
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
    <?php
    ob_end_flush();
})();
Output
 <ul> <li>One</li> <li>Two</li> </ul>

Try it yourself

Write a template for an order confirmation email and render it into a string with output buffering.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
Hi Ana,

Thanks for your order #1042:
  - 3 x Pen
  - 1 x Notebook
We'll let you know when it ships.
(101 characters)