Класс View в PHP: отображение данных в MVC

Раздел: Архитектура приложений -> MVC

Класс представления в PHP: организация отображения в MVC

Класс представления (View) в архитектуре MVC отвечает за вывод данных пользователю. В PHP он обычно реализуется как объект, который принимает данные от контроллера и рендерит HTML-шаблон. Основная цель - отделить логику отображения от бизнес-логики. Ниже рассмотрены различные подходы к реализации такого класса, от простейшего до продвинутого.

Как реализовать минимальный класс View с передачей переменных в шаблон?

Самый распространенный способ - класс, который хранит данные и подключает файл шаблона, делая переменные доступными через extract().


class View {
    protected $data = [];

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

    public function render($template) {
        extract($this->data);
        ob_start();
        require $template;
        return ob_get_clean();
    }
}

Class view php (класс представления в php)

Контроллер заполняет view:


$view = new View();
$view->assign('title', 'Главная');
$view->assign('items', ['a', 'b']);
echo $view->render('templates/main.php');

Файл main.php:


<h1><?= $title ?></h1>
<ul>
<?php foreach ($items as $item): ?>
    <li><?= $item ?></li>
<?php endforeach; ?>
</ul>

Типичные ошибки: переменные не определены в шаблоне, если забыли вызвать assign. Использование extract() может перезаписать существующие переменные (например, $this). Рекомендуется экранировать вывод через htmlspecialchars вручную или автоматически.

Как сделать так, чтобы данные были доступны как свойства объекта в шаблоне?

Вместо assign() можно использовать магические методы __set и __get.


class View {
    protected $data = [];

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

    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    public function render($template) {
        $view = $this; // передаем сам объект
        ob_start();
        require $template;
        return ob_get_clean();
    }
}

Шаблон использует $view->title:


<h1><?= $view->title ?></h1>

Проблемы: в шаблоне нужно явно использовать переменную $view, что увеличивает связность. Кроме того, невозможно использовать встроенные функции вроде compact.

Как применить шаблонизатор (например, Twig) через класс View?

Класс View может быть адаптером для внешнего шаблонизатора.


require 'vendor/autoload.php';

class View {
    protected $twig;

    public function __construct() {
        $loader = new \Twig\Loader\FilesystemLoader('templates');
        $this->twig = new \Twig\Environment($loader, ['cache' => 'cache']);
    }

    public function render($template, $data = []) {
        return $this->twig->render($template, $data);
    }
}

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


$view = new View();
echo $view->render('main.twig', ['title' => 'Главная', 'items' => ['a','b']]);

Шаблон main.twig:


<h1>{{ title }}</h1>
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

Ошибки: забыть установить Twig через Composer, не настроить кэш (приведёт к медленной работе), неправильный путь к шаблонам.

Как реализовать наследование шаблонов (layout) внутри класса View?

Можно ввести понятие макета и секций.


class View {
    protected $data = [];
    protected $layout = null;
    protected $sections = [];

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

    public function section($name) {
        ob_start();
        $this->sections[$name] = '';
        return function() use ($name) {
            $this->sections[$name] = ob_get_clean();
        };
    }

    public function render($template, $data = []) {
        $this->data = array_merge($this->data, $data);
        extract($this->data);
        ob_start();
        include $template;
        $content = ob_get_clean();
        if ($this->layout) {
            ob_start();
            include $this->layout;
            return ob_get_clean();
        }
        return $content;
    }
}

Шаблон page.php использует секции:


<?php $view->extend('layout.php') ?>
<?php $section = $view->section('content') ?>
<h1>Контент страницы</h1>
<?php $section() ?>

Макет layout.php выводит секцию:


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

Проблемы: реализация секций через замыкания может сбить с толку новичков. Альтернатива - передавать секции в виде callback-функций.

Как организовать автоматическое экранирование вывода в классе View?

Можно обернуть строки в объект-эскейпер или использовать метод e().


class View {
    public function e($value) {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    }

    public function render($template, $data = []) {
        extract($data);
        ob_start();
        include $template;
        return ob_get_clean();
    }
}

В шаблоне:


<p><?= $view->e($userInput) ?></p>

Для автоматического экранирования можно использовать __toString или специализированные классы-обёртки.

Ошибки: забыть экранировать - уязвимость XSS. Неправильная кодировка.

Как передавать View в контроллер как объект и вызывать его методы?

Объект View можно сделать вызываемым с помощью __invoke.


class View {
    protected $template;
    protected $data;

    public function __invoke($template, $data = []) {
        $this->template = $template;
        $this->data = $data;
        return $this->render();
    }

    public function render() {
        extract($this->data);
        ob_start();
        include $this->template;
        return ob_get_clean();
    }
}

Тогда контроллер:


$view = new View();
$response = $view('home.php', ['title' => 'Home']);
echo $response;

Проблема: такой подход менее читаем, чем явный вызов render.

Выбор конкретной реализации зависит от требований проекта. Для небольших приложений подойдет простой класс с assign, для сложных - использование Twig или собственного механизма наследования.

Продвинутые примеры работы с классом View

Пример: View с поддержкой вложенных секций и блоков

Реализация, похожая на Blade или Twig, но на чистом PHP.

Пример

class View {
    protected $blocks = [];
    protected $blockStack = [];
    protected $data = [];
    protected $layout = null;

    public function startBlock($name) {
        $this->blockStack[] = $name;
        ob_start();
    }

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

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

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

    public function render($template, $data = []) {
        $this->data = array_merge($this->data, $data);
        extract($this->data);
        ob_start();
        include $template;
        $content = ob_get_clean();
        if ($this->layout) {
            ob_start();
            include $this->layout;
            return ob_get_clean();
        }
        return $content;
    }
}

Шаблон page.php:

Пример

<?php $view->extend('layout.php') ?>
<?php $view->startBlock('content') ?>
    <h1>Привет, мир</h1>
<?php $view->endBlock() ?>
<?php $view->startBlock('scripts') ?>
    <script>alert('OK');</script>
<?php $view->endBlock() ?>

Макет layout.php:

Пример

<!DOCTYPE html>
<html>
<head><title>Мой сайт</title></head>
<body>
<?php $view->block('content') ?>
<?php $view->block('scripts') ?>
</body>
</html>

Результат выполнения:

<!DOCTYPE html>
<html>
<head><title>Мой сайт</title></head>
<body>
    <h1>Привет, мир</h1>
    <script>alert('OK');</script>
</body>
</html>

Возможные ошибки: непарные startBlock/endBlock приведут к некорректному выводу. Лучше проверять стек.

Пример: ViewModel - отдельный класс для передачи данных

Используется для строгой типизации и автодополнения в IDE.

Пример

class UserViewModel {
    public string $name;
    public int $age;
    public ?string $avatar;

    public function __construct(string $name, int $age, ?string $avatar = null) {
        $this->name = $name;
        $this->age = $age;
        $this->avatar = $avatar;
    }
}

class View {
    public function render($template, $viewModel) {
        $view = $viewModel;
        ob_start();
        include $template;
        return ob_get_clean();
    }
}

Контроллер:

Пример

$user = new UserViewModel('Иван', 30, 'avatar.jpg');
$view = new View();
echo $view->render('user.php', $user);

Шаблон user.php:

Пример

<h2><?= $view->name ?></h2>
<p>Возраст: <?= $view->age ?></p>
<img src="<?= $view->avatar ?>" alt="">

Недостаток: требуется создавать отдельный класс для каждой страницы, что может быть избыточно. Подходит для крупных проектов.

Пример: View с поддержкой partials и include

Метод для вставки подшаблонов с передачей данных.

Пример

class View {
    protected $templateDir = 'templates';

    public function render($template, $data = []) {
        extract($data);
        ob_start();
        include $this->templateDir . '/' . $template;
        return ob_get_clean();
    }

    public function partial($partial, $data = []) {
        return $this->render($partial, $data);
    }
}

Шаблон page.php:

Пример

<h1>Главная</h1>
<?= $view->partial('_menu.php', ['items' => $menuItems]) ?>

А в _menu.php:

Пример

<nav>
<ul>
<?php foreach ($items as $item): ?>
    <li><?= $item ?></li>
<?php endforeach; ?>
</ul>
</nav>

Результат - навигация включена в страницу.

Проблема: если partial вызывается внутри другого partial, может возникнуть конфликт переменных. Рекомендуется передавать только необходимые данные.

Пример: View с использованием буферизации для захвата вывода в переменную

Пример

class View {
    public function capture(callable $callback) {
        ob_start();
        $callback();
        return ob_get_clean();
    }

    public function render($template, $data = []) {
        extract($data);
        ob_start();
        include $template;
        return ob_get_clean();
    }
}

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

Пример

$view = new View();
$sidebar = $view->capture(function() use ($view) {
    echo $view->render('sidebar.php', ['links' => $links]);
});
echo $view->render('layout.php', ['sidebar' => $sidebar]);

Ошибки: вложенная буферизация может привести к переполнению памяти, если не закрывать ob_start. Нужно следить за глубиной.

Пример: View с кэшированием результата рендеринга

Пример

class View {
    protected $cacheDir = 'cache/views';
    protected $ttl = 3600;

    public function renderCached($template, $data = [], $key = null) {
        if (!$key) {
            $key = md5($template . serialize($data));
        }
        $cacheFile = $this->cacheDir . '/' . $key . '.html';
        if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $this->ttl)) {
            return file_get_contents($cacheFile);
        }
        $content = $this->render($template, $data);
        file_put_contents($cacheFile, $content);
        return $content;
    }

    public function render($template, $data = []) {
        extract($data);
        ob_start();
        include $template;
        return ob_get_clean();
    }
}

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

Пример

$view = new View();
echo $view->renderCached('home.php', ['news' => $news], 'home_news');

Кэш сохраняется в файл, повторный запрос отдаёт сохранённый HTML.

Ошибки: не забыть создать директорию кэша и дать права на запись. Кэш не сбрасывается при изменении данных - проблема актуальности.

Класс представления в PHP - comments

En
Class view php (php)