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

Дата оновлення перекладу 2022-11-01

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

Уважний читач помітив, що наш фреймворк жорстко кодує те, як виконується конкретний "код" (шаблони). Для простих сторінок як ті, що ми створювали до цього, це не проблема, але якщо ви хочете додати більше логіки, то вам доведеться помістити її в сам шаблон, що скоріш за все - не найкраща ідея, особливо якщо ви все же тримаєте в голові принцип розділення особливостей.

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

Змініть частину відображення фреймворку у шаболні, щоб вона виглядала так:

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(), і, відповідно до поверненого значення, створює новий обʼєкт Відповіді.

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