Вопрос: Как сделать шаблонизатор на php?

Лучший ответ: Сначала - зачем нужен шаблонизатор. Тут подмена понятий. Скорее вопрос такой: как отделить вывод от заголовков. Если не отделять, то может быть такая ошибка<html>
<body>
<?php
// начать сессию
session_start(); // отправить куку PHPSESSID через заголовки HTTP
// но она не может отправиться, потому что уже начался вывод HTML в строке "<html ..."

// если не авторизован, то отправить на страницу логина
if (empty($_SESSION['username']))
    header('Location: /login.php'); // но заголовок HTTP тоже не может отправиться, как и кука
?>
    <h1>Hello, <?php echo $_SESSION['username'] ?></h1>
</body>
</html>
Проблема решается, если вывод HTML делать после вывода заголовков. Например, использовать буфер<?php ob_start(); // открыть буфер ?>
<html>
<body>
<?php
session_start(); //  кука PHPSESSID отправится, потому что HTML ещё в буфере
if (empty($_SESSION['username']))
    header('Location: /login.php'); // заголовок HTTP отправится, потому что HTML ещё в буфере
?>
    <h1>Hello, <?php echo $_SESSION['username'] ?></h1>
</body>
</html>
<?php ob_end_flush(); // выбросить содержимое буфера наружу и закрыть его ?>
Однако так придётся писать в каждом месте, где формируется HTML. Можно ли сократить?
Простейшее представление через буфер<?php
function render($viewPath)
{
    if (!is_file($viewPath))
        return 'View "'. $viewPath . '" not exists';
    ob_start();
    include($viewPath);
    return ob_get_clean();
}

session_start();
if (empty($_SESSION['username']))
    header('Location: /login.php');

$viewsPath = __DIR__.'/views/'; // где лежат представления
render($viewsPath . 'page.php'); // нарисовать страницу HTML<?php
# page page.php
?>
<html>
<body>
    <h1>Hello, <?php echo $_SESSION['username'] ?></h1>
</body>
</html>
А дополнительно ещё решают проблему отделения логики от формирования интерфейса. Работа с внешними данными в одном месте, а отображение их - в другом. Для этого в представление передают всё, что нужно показать. И точка. Лишних данных там не надо. В некоторых фреймворках стоит Exception если представление начнёт работать с базой данных, читает данные из входного запроса или пытается отправить заголовки.<?php
function render($viewPath, $vars)
{
    if (!is_file($viewPath))
        return 'View "'. $viewPath . '" not exists';
    extract($vars); // extract делает из массива набор переменных в локальной области видимости
    ob_start();
    include($viewPath); // эти переменные будут видны внутри подключаемого файла
    return ob_get_clean();
}

session_start();
if (empty($_SESSION['username']))
    header('Location: /login.php');

$viewsPath = __DIR__.'/views/'; // где лежат представления
render($viewsPath . 'page.php', array( // отображаемые данные передаются массивом
    'username' => $_SESSION['username'],
));# page.php
<?php
/**
 * Подсказки для IDE, чтобы не подсвечивал переменные как неопределённые
 * @var string $username
 */
<html>
<body>
    <h1>Hello, <?php echo $sername ?></h1>
</body>
</html>
Не правда ли, с представлениями код становится значительно изящнее. А если логика для реализации представлений хранится в отдельном файле-библиотеке, то код становится короче и понятнее.
В фреймворках вместо функции render может использоваться объектная реализация<?php
class ViewException extends Exception {}

class View
{
    public $viewsPath = __DIR__.'/views/';
    
    public function __construct($viewsPath = null)
    {
         // настройка представлений
        // например, можно перепределить место хранения представлений
        if (!is_null($viewsPath))
            $this->viewsPath = $viewsPath;
    }

    public function render($viewPath, $vars)
    {
        if (!is_file($this->viewsPath . $viewPath))
            throw new ViewException('View "'. $viewPath . '" in folder "'. $this->viewsPath . '" not exists');
        extract($vars);
        ob_start();
        include($this->viewsPath . $viewPath);
        return ob_get_clean();
    }
}

$view = new View();
$view->render('page.php', array(
    'username' => $_SESSION['username']
));Как таблицы шаблонизаирова если в одной 3 столбца, в другой 5?
Никто в представлениях не запрещает использовать языковые конструкции. Это могут быть конструкции языка PHP или какой-нибудь другой язык, специально написанный для шаблонизатора. Например, в Smarty, Blade, Pug свои языки. Передай в представление количество колонок и сделай цикл :)у первой первый столбец должен быть 70% а у второй последний?
В представлениях можно подключать стили CSS, через которые меняется отображение (колонка 70%).
У каждого представления может быть свои стили.
Если нужно, чтобы были некоторые общие стили и дополнительные, нужные только для этой страницы, используют что-то типа буферизирования вывода блока стилей.<?php
class Assets
{
    public static $cssLinks = array();
    public static $css = array();

    // добавить ссылку на файл стилей
    public function addCssLink($link)
    {
        self::$cssLinks[$link] = $link;
    }

    // добавить блок стилей
    public function addCss($name, $css) // $name - это чтобы не дублировались блоки стилей, на всякий случай
    {
        self::$css[$name] = $css;
    }

    public function renderCss()
    {
        foreach(self::$cssLinks as $url)
            echo '<link href="'.$url.'" rel="stylesheet" type="text/css" />';
        echo '<style type="text/css">';
        foreach(self::$css as $css)
            echo $css;
        echo '</style>';
    }
}# index.php

$assets = new Assets();
// общие стили
$assets->addCssLink('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css');

// ...

$view = new View();
$view->render('page.php', array(
    'username' => $_SESSION['username']
));# page.php
<?php
/**
 * @var string $username
 */
$assets = new Assets();
$assets->addCssLink('/path/to/my/styles.css'); // ещё один файл стилей
$assets->addCss('h1 { color: red; }'); // или даже что-нибудь микроскопическое, только для этой страницы
?>
<html>
<head>
<?php $assets->renderCss() ?>
</head>
<body>
    <h1>Hello, <?php echo $username ?></h1>
</body>
</html>
И аналогично про JS.

Худший ответ: Преждевременная сверхархитектурная выдумка - на данном этапе лишняя и вредная головная боль.
Сейчас нужно написать некий прототип оформления данных. В нём будут самые простые условные операторы и циклы для выведения значений и их типов, причем в перемешку с хтмл. Когда этот прототип заработает как надо, тогда можно будет обдумать "архитектуру". И важно не затыкаться с улучшением какого-то отдельного условия, чтоб красиво - для дальнейших обдумываний будет проще разбирать однообразные конструкции с различающимися значениями, так виднее места для группировки.