Шаблонизация

Дата обновления перевода 2022-01-09

Шаблонизация

Проницательный читатель заметил, что наш фреймворк жёстко кодирует то, как выполняется конкретный "код" (шаблоны). Для простых страниц, как те, что мы создавали до этого, это не проблема, но если вы хотите добавить больше логики, то вам придётся поместить её в сам шаблон, что скорее всего не лучшая идея, особенно если вы всё ещё держите в уме принцип разделения особенностей.

Давайте отделим код шаблона от логики, добавив новый слой - контроллер: Миссия контроллера заключается в генерировании Ответа, основываясь на информации, переданной Запросом клиента.

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

1
2
3
4
5
6
7
8
9
10
11
// example.com/web/front.php

// ...
try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func('render_template', $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

Так как теперь отображение выполняется внешней функцией (здесь - render_template()), то нам нужно передать её атрибутам, ивзлечённым из URL. Мы могли бы передать их, как дополнительный аргумент к render_template(), но вместо этого, давайте используем другую функцию класа Request под названием атрибуты: Атрибуты запроса - это способ присоединить дополнительную информацию о Запросе, которая напрямую не связана с данными HTTP-запроса.

Теперь вы можете создать функцию render_template(), общий контроллер, который отображает шаблон при отстутствии специфической логики. Чтобы оставить тот же шаблон, что и прежде, атрибуты извлекаются до того, как отображается шаблон:

1
2
3
4
5
6
7
8
function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);

    return new Response(ob_get_clean());
}

Так как render_template используется в качестве аргумента PHP-функции call_user_func(), мы можем заменить его любым валидным обратным вызовом PHP. Это позволяет нам использовать функцию, анонимную функцию или метод класса в качестве контроллера... выбор за вами.

По соглашению, для каждого маршрута ассоциируемый с ним контроллер кофигурируется через атрибут маршрута _controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
$routes->add('hello', new Routing\Route('/hello/{name}', [
    'name' => 'World',
    '_controller' => 'render_template',
]));

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

Теперь маршрут может быть ассоциирован в любым контроллером, и конечно, вы всё ещё можете использовать render_template() для отображения шаблона:

1
2
3
4
5
6
$routes->add('hello', new Routing\Route('/hello/{name}', [
    'name' => 'World',
    '_controller' => function ($request) {
        return render_template($request);
    }
]));

Это достаточно гибкая система, так как вы можете изменять объект Ответа позднее и даже передавать дополнительные аргунметы к шаблону:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$routes->add('hello', new Routing\Route('/hello/{name}', [
    'name' => 'World',
    '_controller' => function ($request) {
        // $foo будет доступен в шаблоне
        $request->attributes->set('foo', 'bar');

        $response = render_template($request);

        // измените какой-то заголовок
        $response->headers->set('Content-Type', 'text/plain');

        return $response;
    }
]));

Вот обновлённая и улучшенная версия нашего фреймворка:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;

function render_template($request)
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);

    return new Response(ob_get_clean());
}

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->send();

Чтобы отметить рождение нашего нового фреймворка, давайте создадим новенькое приложение, которое требует простой логики. Наше приложение имеет одну страницу, которая сообщает, является ли заданный год високосным. При вызове /is_leap_year, вы получите ответ для текущего года, но вы также можете указать любой год в /is_leap_year/2009. Так как он общий, этот фреймворк не требует никаких изменений, просто создайте новый файл app.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// example.com/src/app.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;

function is_leap_year($year = null)
{
    if (null === $year) {
        $year = date('Y');
    }

    return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100);
}

$routes = new Routing\RouteCollection();
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
    'year' => null,
    '_controller' => function ($request) {
        if (is_leap_year($request->attributes->get('year'))) {
            return new Response('Yep, this is a leap year!');
        }

        return new Response('Nope, this is not a leap year.');
    }
]));

return $routes;

Функция is_leap_year() возвращает true, когда заданный год является високосным, в других случаях - false. Если год - null, то тестируется текущий год. Контроллер прост: он берёт год из атрибутов запроса, передаёт его функции is_leap_year(), и в соответствии с возвращённым значением, создаёт новый объект Ответа.

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