Let’s compare plain PHP templates and Twig for keeping HTML out of your business logic. Mixed PHP and HTML in controllers gets messy fast.
Plain PHP templates
A template is a PHP file that mostly HTML, with short echo blocks. Pass data in as variables:
// Controller:
return view('notes/index', ['notes' => $notes, 'title' => 'My notes']);
// helpers/view.php:
function view(string $name, array $data = []): string
{
extract($data, EXTR_SKIP);
ob_start();
require dirname(__DIR__) . '/templates/' . $name . '.php';
return ob_get_clean();
}
<!-- templates/notes/index.php -->
<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></h1>
<ul>
<?php foreach ($notes as $note): ?>
<li><?= htmlspecialchars($note['title'], ENT_QUOTES, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
Layouts
Avoid copying header and footer on every page. Use a layout wrapper:
// templates/layout.php
<!DOCTYPE html>
<html lang="en-GB">
<head><title><?= htmlspecialchars($title ?? 'App') ?></title></head>
<body>
<?php include __DIR__ . '/partials/nav.php'; ?>
<main><?php include $contentTemplate; ?></main>
</body>
</html>
Twig option
Twig auto-escapes output by default and has inheritance (extends, block). Install with Composer:
composer require twig/twig
$loader = new TwigLoaderFilesystemLoader(dirname(__DIR__) . '/templates');
$twig = new TwigEnvironment($loader, ['cache' => false]);
echo $twig->render('notes/index.twig', ['notes' => $notes]);
{# templates/notes/index.twig #}
{% extends 'layout.twig' %}
{% block title %}My notes{% endblock %}
{% block content %}
<ul>
{% for note in notes %}
<li>{{ note.title }}</li>
{% endfor %}
</ul>
{% endblock %}
Which to pick
- Plain PHP – zero dependencies, full control, you handle escaping
- Twig – safer defaults, nicer inheritance, small learning curve
Either beats echoing HTML from a controller. Pick one, stay consistent, and always escape user data on output.

