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:
<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 // 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';
GET /products.php?max=50
> GET /products.php?max=50 <h1>Products</h1> <ul> <li>Desk lamp — 34.50</li> <li>Notebook <A5> — 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 ob_start(); echo "Hello, "; echo "buffer!"; $captured = ob_get_clean(); echo "Captured ", strlen($captured), " characters: [", strtoupper($captured), "]\n";
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:
<div class="card"> <h2><?= $e($name) ?></h2> <p><?= $e($bio) ?></p> </div>
<?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.']);
<div class="card"> <h2>Ada Lovelace</h2> <p>Wrote the first program & more.</p> </div> <div class="card"> <h2>Grace Hopper</h2> <p><b>Admiral</b>, 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:
<!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>© <?= $year ?> My Shop</footer> </body> </html>
<h1>About us</h1> <p>We sell <?= $e($what) ?> since <?= $since ?>.</p>
<?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]);
GET /about.php
> 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 & desks since 2019.</p> </main> <footer>© 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,@extendsand 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_dumporprint_rto get their output as a string (or useprint_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 $html = (function () { ob_start(fn(string $buffer) => preg_replace('/\s+/', ' ', $buffer)); ?> <ul> <li>One</li> <li>Two</li> </ul> <?php ob_end_flush(); })();
<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.
Hi Ana, Thanks for your order #1042: - 3 x Pen - 1 x Notebook We'll let you know when it ships. (101 characters)