View в MVC

16.06.2023 в 09:31
21873
+1210

Сегодня мы сделаем компонент View, то самое “V” в архитектуре MVC. View – это представление, то есть та часть программы, которая формирует то, что видит пользователь.

В случае приложения на языке PHP, в подавляющем большинстве случаев представление занимается формированием HTML-кода. Вообще, это довольно простая часть кода, которой даётся только имя шаблона и список переменных, которые в этот шаблон нужно подставить.

Итак, давайте рассмотрим простейший пример и создадим для начала только шаблон. Путь до него будет следующим: templates/main/main.php

Шаблон main.php

Давайте запишем в него HTML-код для нашей будущей странички
templates/main/main.php

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Мой блог</title>
    <style>
        .layout {
            width: 100%;
            max-width: 1024px;
            margin: auto;
            background-color: white;
            border-collapse: collapse;
        }

        .layout tr td {
            padding: 20px;
            vertical-align: top;
            border: solid 1px gray;
        }

        .header {
            font-size: 30px;
        }

        .footer {
            text-align: center;
        }

        .sidebarHeader {
            font-size: 20px;
        }

        .sidebar ul {
            padding-left: 20px;
        }

        a, a:visited {
            color: darkgreen;
        }
    </style>
</head>
<body>

<table class="layout">
    <tr>
        <td colspan="2" class="header">
            Мой блог
        </td>
    </tr>
    <tr>
        <td>
            <h2>Статья 1</h2>
            <p>Всем привет, это текст первой статьи</p>
            <hr>

            <h2>Статья 2</h2>
            <p>Всем привет, это текст второй статьи</p>
        </td>

        <td width="300px" class="sidebar">
            <div class="sidebarHeader">Меню</div>
            <ul>
                <li><a href="/">Главная страница</a></li>
                <li><a href="/about-me">Обо мне</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td class="footer" colspan="2">Все права защищены (c) Мой блог</td>
    </tr>
</table>

</body>
</html>

Теперь давайте откроем наш контроллер MainController и изменим его метод main()
src/MyProject/Controllers/MainController.php

<?php

namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        include __DIR__ . '/../../../templates/main/main.php';
    }
}

Теперь откроем http://myproject.loc/ и полюбуемся результатом:

Результат вывода шаблона

Для начала давайте немного облегчим шаблон и вынесем стили в отдельный файл. Для этого в папке www создадим файл styles.css.
www/styles.css

.layout {
    width: 100%;
    max-width: 1024px;
    margin: auto;
    background-color: white;
    border-collapse: collapse;
}

.layout tr td {
    padding: 20px;
    vertical-align: top;
    border: solid 1px gray;
}

.header {
    font-size: 30px;
}

.footer {
    text-align: center;
}

.sidebarHeader {
    font-size: 20px;
}

.sidebar ul {
    padding-left: 20px;
}

a, a:visited {
    color: darkgreen;
}

Теперь подключим этот файл со стилями в шаблоне:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Мой блог</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>

<table class="layout">
    <tr>
        <td colspan="2" class="header">
            Мой блог
        </td>
    </tr>
    <tr>
        <td>
            <h2>Статья 1</h2>
            <p>Всем привет, это текст первой статьи</p>
            <hr>

            <h2>Статья 2</h2>
            <p>Всем привет, это текст второй статьи</p>
        </td>

        <td width="300px" class="sidebar">
            <div class="sidebarHeader">Меню</div>
            <ul>
                <li><a href="/">Главная страница</a></li>
                <li><a href="/about-me">Обо мне</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td class="footer" colspan="2">Все права защищены (c) Мой блог</td>
    </tr>
</table>

</body>
</html>

И снова убедимся, что всё работает.
Отображение при подключении внешних стилей

Давайте теперь попробуем передавать в шаблон переменные. Вместо явно заданных статей сделаем переменную со статьями:
src/MyProject/Controllers/MainController.php

<?php

namespace MyProject\Controllers;

class MainController
{
    public function main()
    {
        $articles = [
            ['name' => 'Статья 1', 'text' => 'Текст статьи 1'],
            ['name' => 'Статья 2', 'text' => 'Текст статьи 2'],
        ];
        include __DIR__ . '/../../../templates/main/main.php';
    }
}

А теперь выведем эти статьи в шаблоне:
templates/main/main.php

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Мой блог</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>

<table class="layout">
    <tr>
        <td colspan="2" class="header">
            Мой блог
        </td>
    </tr>
    <tr>
        <td>
            <?php foreach ($articles as $article): ?>
                <h2><?= $article['name'] ?></h2>
                <p><?= $article['text'] ?></p>
                <hr>
            <?php endforeach; ?>
        </td>

        <td width="300px" class="sidebar">
            <div class="sidebarHeader">Меню</div>
            <ul>
                <li><a href="/">Главная страница</a></li>
                <li><a href="/about-me">Обо мне</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td class="footer" colspan="2">Все права защищены (c) Мой блог</td>
    </tr>
</table>

</body>
</html>

Если нам понадобится в другом контроллере или другом экшне добавить логику для работы с шаблонами, нам снова придется перечислять список переменных, а затем писать include с указанием полного пути для шаблона. Звучит не очень хорошо. Поэтому мы просто вынесем логику с подключением нужного шаблона в отдельный класс.

Создадим класс View.php по пути src/MyProject/View/View.php
Класс View

В конструкторе этого класса мы будем принимать путь до папки с шаблонами:
src/MyProject/View/View.php

<?php

namespace MyProject\View;

class View
{
    private $templatesPath;

    public function __construct(string $templatesPath)
    {
        $this->templatesPath = $templatesPath;
    }
}

Помимо этого давайте добавим метод, в который будем передавать имя конкретного шаблона и массив с переменными.

<?php

namespace MyProject\View;

class View
{
    private $templatesPath;

    public function __construct(string $templatesPath)
    {
        $this->templatesPath = $templatesPath;
    }

    public function renderHtml(string $templateName, array $vars = [])
    {
        extract($vars);

        include $this->templatesPath . '/' . $templateName;
    }
}

Функция extract извлекает массив в переменные. То есть она делает следующее: в неё передаётся массив ['key1' => 1, 'key2' => 2], а после её вызова у нас имеются переменные $key1 = 1 и $key2 = 2.

После этого мы просто подключаем файл с нужным шаблоном, получив путь до него, склеив пути до папки с шаблонами и именем конкретного шаблона.

Пришло время опробовать этот код в нашем контроллере. Создадим новый объект View в конструкторе контроллера, а затем внутри экшена вызовем renderHtml().
src/MyProject/Controllers/MainController.php

<?php

namespace MyProject\Controllers;

use MyProject\View\View;

class MainController
{
    private $view;

    public function __construct()
    {
        $this->view = new View(__DIR__ . '/../../../templates');
    }

    public function main()
    {
        $articles = [
            ['name' => 'Статья 1', 'text' => 'Текст статьи 1'],
            ['name' => 'Статья 2', 'text' => 'Текст статьи 2'],
        ];
        $this->view->renderHtml('main/main.php', ['articles' => $articles]);
    }
}

Теперь мы можем снова открыть сайт, и убедиться, что всё прекрасно работает.

Буфер вывода

В тот момент, когда мы подключаем файл c HTML-кодом, либо пишем в PHP-коде echo, либо совершаем какой-либо другой вывод данных, эти данные начинают сразу передаваться в поток вывода. И если что-то пойдёт не так, мы не сможем вернуть этот вывод и вывести вместо него какую-нибудь ошибку. Но в PHP есть возможность весь этот поток вывода положить во временный буфер вывода. Выглядит его использование следующим образом:
src/MyProject/View/View.php

public function renderHtml(string $templateName, array $vars = [])
{
    extract($vars);

    ob_start();
    include $this->templatesPath . '/' . $templateName;
    $buffer = ob_get_contents();
    ob_end_clean();
}

Если вы сейчас попробуете запустить наш скрипт, то увидите пустую страницу. Дело в том, что все данные, которые должны были быть переданы в поток вывода, оказались в переменной $buffer.
Для того, чтобы передать эти данные в поток вывода, достаточно только вывести переменную $buffer.
src/MyProject/View/View.php

public function renderHtml(string $templateName, array $vars = [])
{
    extract($vars);

    ob_start();
    include $this->templatesPath . '/' . $templateName;
    $buffer = ob_get_contents();
    ob_end_clean();

    echo $buffer;
}

Откройте страничку снова, и убедитесь, что всё вернулось на свои места.

Так в чём же профит? А профит в том, что мы можем обрабатывать ошибки, возникшие в процессе работы с шаблоном. Пока мы с вами не знакомы с понятием «Исключения», давайте предположим, что у нас при подключении шаблона произошла какая-то ошибка. Тогда мы могли бы обработать эту ошибку и не выводить пользователю неправильно отрисованный шаблон. Мы могли бы сделать что-то типа такого:
src/MyProject/View/View.php

public function renderHtml(string $templateName, array $vars = [])
{
    extract($vars);

    ob_start();
    include $this->templatesPath . '/' . $templateName;
    $buffer = ob_get_contents();
    ob_end_clean();

    $error = 'В шаблоне была ошибка!';

    if (empty($error)) {
        echo $buffer;
    } else {
        echo $error;
    }
}

Чуть позже мы вернёмся к обработке возможных ошибок, когда познакомимся с исключениями. А пока оставим этот код в таком состоянии:
src/MyProject/View/View.php

public function renderHtml(string $templateName, array $vars = [])
{
    extract($vars);

    ob_start();
    include $this->templatesPath . '/' . $templateName;
    $buffer = ob_get_contents();
    ob_end_clean();

    echo $buffer;
}

Реиспользование шаблонов

Давайте в наш контроллер вернём экшн из прошлых уроков, который выводил приветствие.
src/MyProject/Controllers/MainController.php

<?php

namespace MyProject\Controllers;

use MyProject\View\View;

class MainController
{
    private $view;

    public function __construct()
    {
        $this->view = new View(__DIR__ . '/../../../templates');
    }

    public function main()
    {
        $articles = [
            ['name' => 'Статья 1', 'text' => 'Текст статьи 1'],
            ['name' => 'Статья 2', 'text' => 'Текст статьи 2'],
        ];
        $this->view->renderHtml('main/main.php', ['articles' => $articles]);
    }

    public function sayHello(string $name)
    {
        echo 'Привет, ' . $name;
    }
}

Давайте изменим его, чтобы он работал через шаблон.

public function sayHello(string $name)
{
    $this->view->renderHtml('main/hello.php', ['name' => $name]);
}

Ну и создадим сам шаблон для него.
templates/main/hello.php

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Мой блог</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>

<table class="layout">
    <tr>
        <td colspan="2" class="header">
            Мой блог
        </td>
    </tr>
    <tr>
        <td>
            Привет, <?= $name ?>!!!
        </td>

        <td width="300px" class="sidebar">
            <div class="sidebarHeader">Меню</div>
            <ul>
                <li><a href="/">Главная страница</a></li>
                <li><a href="/about-me">Обо мне</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td class="footer" colspan="2">Все права защищены (c) Мой блог</td>
    </tr>
</table>

</body>
</html>

Давайте теперь перейдём по адресу http://myproject.loc/hello/username и увидим, что всё прекрасно сработало:

Рендеринг другого шаблона

А теперь внимательно присмотритесь к нашим двум получившимся шаблонам. Согласитесь, у них всё абсолютно одинаковое, кроме текста, который мы выводим на странице. Давайте это исправим! Всё, что выше нашего контента – вынесем в один файл, всё что ниже – в другой. А в самих наших шаблонах будем эти два файла подключать.

Итак, выносим верхнюю часть (так называемую шапку сайта - хедер) в новый файл templates/header.php
templates/header.php

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Мой блог</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>

<table class="layout">
    <tr>
        <td colspan="2" class="header">
            Мой блог
        </td>
    </tr>
    <tr>
        <td>

Затем выносим нижнюю часть (называемую футером или подвалом) в файл templates/footer.php
templates/footer.php

        </td>

        <td width="300px" class="sidebar">
            <div class="sidebarHeader">Меню</div>
            <ul>
                <li><a href="/">Главная страница</a></li>
                <li><a href="/about-me">Обо мне</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td class="footer" colspan="2">Все права защищены (c) Мой блог</td>
    </tr>
</table>

</body>
</html>

После чего редактируем наши шаблоны:
templates/main/main.php

<?php include __DIR__ . '/../header.php'; ?>
<?php foreach ($articles as $article): ?>
    <h2><?= $article['name'] ?></h2>
    <p><?= $article['text'] ?></p>
    <hr>
<?php endforeach; ?>
<?php include __DIR__ . '/../footer.php'; ?>

templates/main/hello.php

<?php include __DIR__ . '/../header.php'; ?>
Привет, <?= $name ?>!!!
<?php include __DIR__ . '/../footer.php'; ?>

Должна получиться вот такая структура в шаблонах:
Структура папки с шаблонами

После этого заходим на странички http://myproject.loc/hello/username и http://myproject.loc/ и радуемся результату :)

Согласитесь, теперь можно очень просто добавлять новые странички на сайте, а также изменять шапку и футер только в одном месте.

Давайте повторим последовательность шагов, которые необходимо сделать для добавления новой странички:

  1. Добавляем экшн в контроллер (либо создаём ещё и новый контроллер);
  2. Добавляем для него роут в routes.php;
  3. Описываем логику внутри экшена и в конце вызываем у компонента view метод renderHtml();
  4. Создаём шаблон для вывода результата.

Вот и весь View.

Код с результатом на гитхабе.

loader
16.06.2023 в 09:31
21873
+1210
Домашнее задание

Сделайте чтобы title для каждой страницы можно было задавать через переменную для шаблона. В случае, когда title не передан, выводите заголовок по умолчанию - "Мой блог". Для страницы /hello/username сделайте title "Страница приветствия".

Комментарии
Этот урок набрал набрал достаточно большое количество комментариев и дальнейшее его комментирование отключено. Если вы хотели убедиться в правильности выполнения ДЗ или у вас возник вопрос по уроку, посмотрите ранее добавленные комментарии, кликнув по кнопке ниже. Скорее всего вы найдете там то, что искали. Если это не помогло - задайте вопрос в чате в телеграме - https://t.me/php_zone
Логические задачи с собеседований