Реализация слоя представления в PHP приложениях

Раздел: Веб-разработка -> Шаблоны и MVC

Обзор создания представлений в PHP

Представление (view) в архитектуре MVC отвечает за вывод данных пользователю. В PHP существует несколько способов организации этого слоя: от простого включения файлов до использования специализированных шаблонизаторов. Каждый вариант имеет свои цели и случаи применения. Ниже рассмотрено основное эффективное решение, а также альтернативные подходы.

Класс View с буферизацией вывода (основной подход)

Наиболее эффективным решением в контексте MVC является создание собственного класса View, который использует буферизацию вывода и отделяет логику от представления. Цель: централизованное управление шаблонами, передача данных без загрязнения глобальной области видимости.

Пример реализации класса View


class View {
    protected string $templatePath;
    protected array $data = [];

    public function __construct(string $templatePath) {
        $this->templatePath = rtrim($templatePath, '/') . '/';
    }

    public function assign(string $key, $value): void {
        $this->data[$key] = $value;
    }

    public function render(string $template, array $data = []): string {
        $data = array_merge($this->data, $data);
        extract($data);
        ob_start();
        include $this->templatePath . $template . '.php';
        return ob_get_clean();
    }
}

Пояснение: конструктор принимает путь к папке с шаблонами. Метод assign добавляет данные, render объединяет данные, извлекает переменные в локальную область, включает файл шаблона с буферизацией и возвращает результат.

Использование в контроллере


$view = new View('/path/to/views');
$view->assign('title', 'Главная страница');
$view->assign('items', ['Пункт 1', 'Пункт 2']);
echo $view->render('index');
<!DOCTYPE html>
<html><head><title>Главная страница</title></head>
<body><ul><li>Пункт 1</li><li>Пункт 2</li></ul></body></html>

Типичные ошибки и способы решения

Ошибка 1: Утечка буфера, если метод ob_get_clean не вызывается. Решение: Использовать try-finally для гарантированного завершения буфера.


public function render(string $template, array $data = []): string {
    extract(array_merge($this->data, $data));
    ob_start();
    try {
        include $this->templatePath . $template . '.php';
    } finally {
        return ob_get_clean();
    }
}

Ошибка 2: Неправильный путь к шаблону. Решение: Использовать абсолютные пути или проверять существование файла.

Вариант 1: Прямое включение файлов через include/require

Как вывести данные в шаблоне без создания отдельного класса?

Самый простой вариант - подключать файлы с PHP-кодом напрямую. Цель: быстрая реализация для небольших проектов без строгого разделения.


// controller.php
$title = 'О сайте';
$content = 'Текст страницы';
include 'view.php';


<!DOCTYPE html>
<html><body>
    <h1><?= $title ?></h1>
    <p><?= $content ?></p>
</body></html>

Проблемы: переменные из контроллера загрязняют глобальное пространство; нет экранирования вывода (XSS-уязвимость).

Типичная ошибка: Использование echo для вывода неэкранированных данных. Решение: Применять htmlspecialchars() в каждом выводе.

Вариант 2: Использование шаблонизатора Twig

Как отделить логику от представления с помощью готового шаблонизатора?

Twig - один из самых популярных шаблонизаторов для PHP. Цель: полное разделение логики, синтаксический сахар, автоматическое экранирование.

Установка через Composer


composer require twig/twig:^3.0

Пример использования


require_once 'vendor/autoload.php';

$loader = new \Twig\Loader\FilesystemLoader('/path/to/templates');
$twig = new \Twig\Environment($loader, [
    'cache' => '/path/to/cache', // отключить в разработке
]);

echo $twig->render('page.html.twig', [
    'title' => 'Twig Example',
    'users' => ['Alice', 'Bob']
]);


<h1>{{ title }}</h1>
<ul>
{% for user in users %}
    <li>{{ user }}</li>
{% endfor %}
</ul>
<h1>Twig Example</h1>
<ul><li>Alice</li><li>Bob</li></ul>

Ошибка: Неверный путь к кешу или его отсутствие. Решение: Создать папку с правами на запись или отключить кеш при разработке.

Вариант 3: Собственный класс View с поддержкой layout и секций

Как реализовать общий макет (layout) для всех страниц?

Решение с наследованием шаблонов аналогично Twig, но без внешних зависимостей. Цель: повторное использование общей структуры.

Пример реализации layout


class View {
    protected $blocks = [];
    protected $extends = null;

    public function extend($template) {
        $this->extends = $template;
    }

    public function section($name, $content = null) {
        if ($content !== null) {
            $this->blocks[$name] = $content;
        } else {
            ob_start();
        }
    }

    public function endSection() {
        $name = array_key_last($this->blocks);
        $this->blocks[$name] = ob_get_clean();
    }

    public function render($template, $data = []) {
        extract($data);
        ob_start();
        include $template . '.php';
        $content = ob_get_clean();

        if ($this->extends) {
            ob_start();
            include $this->extends . '.php';
            return ob_get_clean();
        }
        return $content;
    }
}

Использование:


// page.php (шаблон)
$this->extend('layout');
$this->section('content'); ?>
<h1>Добро пожаловать</h1>
<?php $this->endSection(); ?>


<!DOCTYPE html>
<html><body>
    <?= $this->blocks['content'] ?? '' ?>
</body></html>

Ошибка: Попытка закрыть секцию без начала. Решение: Следить за стеком секций или использовать исключение.

Дополнительные расширенные примеры

Пример 1: Класс View с поддержкой layout, секций и включения других шаблонов

Усовершенствованная версия, которая обрабатывает вложенные секции и позволяет подключать частичные шаблоны (partials).

Код класса

Пример

class AdvancedView {
    protected $path;
    protected $data = [];
    protected $blocks = [];
    protected $blockStack = [];
    protected $extend = null;

    public function __construct($path) {
        $this->path = rtrim($path, '/') . '/';
    }

    public function assign($key, $value) {
        $this->data[$key] = $value;
    }

    public function extend($template) {
        $this->extend = $template;
    }

    public function startBlock($name) {
        array_push($this->blockStack, $name);
        ob_start();
    }

    public function endBlock() {
        $name = array_pop($this->blockStack);
        $this->blocks[$name] = ob_get_clean();
    }

    public function block($name, $default = '') {
        echo $this->blocks[$name] ?? $default;
    }

    public function include($template, $data = []) {
        extract(array_merge($this->data, $data));
        include $this->path . $template . '.php';
    }

    public function render($template, $data = []) {
        $this->data = array_merge($this->data, $data);
        extract($this->data);

        ob_start();
        include $this->path . $template . '.php';
        $content = ob_get_clean();

        if ($this->extend) {
            ob_start();
            include $this->path . $this->extend . '.php';
            return ob_get_clean();
        }
        return $content;
    }
}

Пример использования

Пример

// home.php
$this->extend('layouts/main');
$this->startBlock('title'); ?>Главная<?php $this->endBlock(); ?>
$this->startBlock('content'); ?>
<h1>Добро пожаловать на сайт</h1>
<?php $this->include('partials/menu', ['active' => 'home']); ?>
<?php $this->endBlock(); ?>

// layouts/main.php
<!DOCTYPE html>
<html><head>
    <title><?php $this->block('title', 'Default'); ?></title>
</head><body>
    <?php $this->block('content'); ?>
</body></html>

// partials/menu.php
<ul>
    <li class="<?= $active === 'home' ? 'active' : '' ?>">Главная</li>
    <li>О нас</li>
</ul>

Результат

<!DOCTYPE html>
<html><head>
    <title>Главная</title>
</head><body>
    <h1>Добро пожаловать на сайт</h1>
    <ul>
        <li class="active">Главная</li>
        <li>О нас</li>
    </ul>
</body></html>

Пример 2: Использование Twig с пользовательскими фильтрами и функциями

Расширение шаблонизатора для специфических нужд.

Код

Пример

require_once 'vendor/autoload.php';

$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
$twig = new \Twig\Environment($loader, ['debug' => true]);

// Пользовательский фильтр: сокращение текста
$filter = new \Twig\TwigFilter('excerpt', function ($text, $length = 100) {
    return mb_substr(strip_tags($text), 0, $length) . '...';
});
$twig->addFilter($filter);

// Пользовательская функция: текущая дата
$function = new \Twig\TwigFunction('now', function ($format = 'Y-m-d') {
    return date($format);
});
$twig->addFunction($function);

// Шаблон article.html.twig
$template = $twig->load('article.html.twig');
echo $template->render([
    'title' => 'Длинная статья о PHP',
    'body' => 'Очень длинный текст с <b>HTML</b> тегами...'
]);

Шаблон article.html.twig

Пример

<article>
    <h1>{{ title }}</h1>
    <p>{{ body | excerpt(20) }}</p>
    <small>Опубликовано: {{ now('d.m.Y') }}</small>
</article>

Результат

<article>
    <h1>Длинная статья о PHP</h1>
    <p>Очень длинный текс...</p>
    <small>Опубликовано: 22.02.2025</small>
</article>

Пример 3: Альтернатива с использованием PHP-шаблонизатора Plates (через композер)

Plates - простой шаблонизатор без синтаксиса, только PHP. Цель: лёгкость и совместимость.

Пример

composer require league/plates:^3.0
Пример

require_once 'vendor/autoload.php';

$templates = new League\Plates\Engine('/path/to/templates');

// Добавление данных для всех шаблонов
$templates->addData(['siteName' => 'Мой сайт']);

echo $templates->render('profile', ['username' => 'user123', 'age' => 25]);
Пример


<h1>Профиль пользователя</h1>
<p>Имя: <?= $this->e($username) ?></p>
<p>Возраст: <?= $this->e($age) ?></p>
<p>Сайт: <?= $this->e($siteName) ?></p>
<h1>Профиль пользователя</h1>
<p>Имя: user123</p>
<p>Возраст: 25</p>
<p>Сайт: Мой сайт</p>
- New php view (создание представления (view) в php)

Создание представления (view) в PHP - comments

En
New php view (php)