Створення та використання шаблонів

Дата оновлення перекладу 2024-06-10

Створення та використання шаблонів

Шаблон - це кращий спосіб організувати та відобразити HTML всередині вашого додатку, незалежно від того, чи потрібно вам відобразити HTML з контролера, чи згенерувати зміст електронного листа. Шаблони в Symfony створюються за допомогою Twig: гнучкий, швидкий та безпечний двигун шаблонів.

Мова шаблонів Twig

Мова шаблонів Twig дозволяє вам писати ємні та читані шаблони, які більш дружні по відношенню до веб-дизайнерів і, у багатьох сенсах, більш потужні, ніш шаблони PHP. Подивіться на наступний приклад шаблону Twig. Навіть якщо це перший раз, коли ви бачиті Twig, ви скоріше за все зрозумієте більшу частину:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to Symfony!</title>
    </head>
    <body>
        <h1>{{ page_title }}</h1>

        {% if user.isLoggedIn %}
            Hello {{ user.name }}!
        {% endif %}

        {# ... #}
    </body>
</html>

Синтаксис Twig засновується на наступних трьох конструкціях:

  • {{ ... }}, використовується для відображення змісту змінної або результату оцінки виразу;
  • {% ... %}, використовується для виконання деякої логіки, на кшталт умовності або циклу;
  • {# ... #}, використовується для додавання коментарів у шаблон (на відміну від коментарів HTML, ці коментарі не додаються на відображувану сторінку).

Ви не можете запустити PHP-код всередині шаблінов Twig, але Twig надає утиліти для виконання деякої логіки у шаблонах. Наприклад, фильтри змінюють зміст до його відображення, наприклад, фільтр upper перетворює зміст у великі літери:

1
{{ title|upper }}

Twig постачається з довгим списком тегів, фільтрів та функцій, які доступні за замовчуванням. У додатках Symfony ви можете також використовувати ці фільтри та функції Twig, визначені Symfony і ви можете створювати власні функції та фільтри Twig.

Twig швидкий у середовищі prod (так як шаблони компілюються в PHP та кешуються автоматично), але зручніше використовувати середовище dev (тому що шаблони повторно компілюються автоматично при їх зміні).

Конфігурація Twig

Twig має декілька опцій конфігурації для визначення речей на кшталт формату, використовуваного для відображення цифр та дат, кешування шаблонів і т.д. Прочитайте довідник конфігурації Twig, щоб дізнатися про них більше.

Створення шаблонів

Перед детальним поясненням того, як створювати та відображати шаблони, подивіться на наступний приклад для швидкого розуміння всього процесу. Спочатку вам потрібно створити новий файл у каталозі templates/, щоб зберігати зміст шаблону:

1
2
3
{# templates/user/notifications.html.twig #}
<h1>Hello {{ user_first_name }}!</h1>
<p>У вас {{ notifications|length }} нових сповіщень.</p>

Потім, створіть контролер, який відображає цей шаблон та передає йому необхідні змінні:

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
// src/Controller/UserController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class UserController extends AbstractController
{
    // ...

    public function notifications(): Response
    {
        // отримати інформацію користувача та сповіщення якимось чином
        $userFirstName = '...';
        $userNotifications = ['...', '...'];

        // шлях шаблону - це відносний шлях файлу з `templates/`
        return $this->render('user/notifications.html.twig', [
            // цей масив визначає змінні, передані шаблону, де ключ - це
            // ім'я змінної, а значення - значення змінної
            // (Twig рекомендує використання імен змінних snake_case : 'foo_bar' вместо 'fooBar')
            'user_first_name' => $userFirstName,
            'notifications' => $userNotifications,
        ]);
    }
}

Найменування шаблонів

Symfony рекомендує наступні імена шаблонів:

  • Використовуйте snake case для імен файлів та каталогів (наприклад, blog_posts.html.twig, admin/default_theme/blog/index.html.twig, і т.д.);
  • Визначте два розширення для імен файлів (наприклад, index.html.twig або blog_posts.xml.twig) у вигляді першого розширення (html, xml, і т.д.), фінальний формат якого генеруватиме шаблон.

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

Розташування шаблонів

Шаблони зберігаються за замовчуванням у каталозі templates/. Коли сервіс або контролер відображає шаблон product/index.html.twig, вони насправді посилаються на файл <your-project>/templates/product/index.html.twig.

Каталог шаблонів за замовчуванням можна сконфігурувати за допомогою опції twig.default_path , і ви можете додати більше каталогів шаблонів як пояснюється пізніше у цій статті.

Змінні шаблонів

Розповсюдженою необхідністю шаблонів є виведення значень, які зберігаються у шаблонах, переданих з контролера або сервісу. Змінні зазвичай зберігають об'єкти та масиви, а не рядки, числа та булеві значення. Тому Twig надає швидкий доступ до складних PHP-змінниих. Розгляньте наступний шаблон:

1
<p>{{ user.name }} додав коментар у {{ comment.publishedAt|date }}</p>

Нотація user.name означає, що ви хочете відобразити деяку інформацію (name), що зберігається у змінній (user). user - це масив чи об'єкт? name - це властивість чи метод? У Twig це не має значення.

При використанні нотації foo.bar, Twig намагається отримати значення змінної у наступному порядку:

  1. $foo['bar'] (масив і елемент);
  2. $foo->bar (об'єкт і публічна властивість);
  3. $foo->bar() (об'єкт і публічний метод);
  4. $foo->getBar() (об'єкт і метод getter);
  5. $foo->isBar() (об'єкт і метод isser);
  6. $foo->hasBar() (об'єкт і метод hasser);
  7. Якщо нічого з перерахованого вище не існує, використайте null (або викличте виключення Twig\Error\RuntimeError, якщо увімкнена опція strict_variables ).

Це дозволяє розвивати код вашого додатку, не змінюючи код шаблону (ви можете почати зі змінних масиву для апробації концепту додатку, а потім перейти до об'єктів з методами і т.д.)

Посилання на сторінки

Замість написання URL посилань від руки, використайте функцію path(), щоб згенерувати URL, засновуючись на конфігурації маршрутизації .

Пізніше, якщо ви захочете змінити URL конкретної сторінки, все, що вам треба буде зробити - це змінити конфігурацію маршрутизації: шаблони автоматично згенерують новий URL.

Розгляньте наступну конфігурацію маршрутизації:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Controller/BlogController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class BlogController extends AbstractController
{
    #[Route('/', name: 'blog_index')]
    public function index(): Response
    {
        // ...
    }

    #[Route('/article/{slug}', name: 'blog_post')]
    public function show(string $slug): Response
    {
        // ...
    }
}

Використайте функцію Twig path(), щоб послатися на ці сторінки та передати ім'я маршруту в якості першого аргументу, а параметри маршруту в якості необов'язкового другого аргументу:

1
2
3
4
5
6
7
8
9
10
11
<a href="{{ path('blog_index') }}">Homepage</a>

{# ... #}

{% лоя посту у blog_posts %}
    <h1>
        <a href="{{ path('blog_post', {slug: post.slug}) }}">{{ post.title }}</a>
    </h1>

    <p>{{ post.excerpt }}</p>
{% endfor %}

Функція path() генерує відносні URL. Якщо вам потрібно згенерувати абсолютні URL (наприклад, при відображенні шаблонів для електронної пошти або стрічок RSS), використайте функцію url(), яка бере ті ж аргументи, що і path() (наприклад, <a href="{{ url('blog_index') }}"> ... </a>).

Посилання на ресурси CSS, JavaScript та зображень

Якщо шаблону потрібно послатися на статичний ресурс (наприклад, зображення), Symfony надає функцію Twig asset(), щоб допомогти згенерувати цей URL. Спочатку встановіть пакет asset:

1
$ composer require symfony/asset

Тепер ви можете використовувати функцію asset():

1
2
3
4
5
6
7
8
{# зображення живе тут "public/images/logo.png" #}
<img src="{{ asset('images/logo.png') }}" alt="Symfony!"/>

{# CSS-файл живе тут "public/css/blog.css" #}
<link href="{{ asset('css/blog.css') }}" rel="stylesheet"/>

{# JS-файл живе тут "public/bundles/acme/js/loader.js" #}
<script src="{{ asset('bundles/acme/js/loader.js') }}"></script>

Головною метою функції asset() є зробити ваш застосунок більш портативним. Якщо ваш застосунок живе у корені вашого хостингу (наприклад, https://example.com), відображений шлях має бути /images/logo.png. Але якщо ваш застосунок живе у підкаталозі (наприклад, https://example.com/my_app), кожний шлях має відображатися з підкаталогом (наприклад, /my_app/images/logo.png). Функція asset() піклується про це, визначаючи те, як ваш застосунок використовується, і відповідно генеруючи правильні шляхи.

Tip

Функція asset() підтримує різні техніки посилення кешу через опції конфігурації version , version_format , та json_manifest_path .

Якщо вам для ресурсів потрібні абсолютні URL, використайте функцію Twwig absolute_url() наступним чином:

1
2
3
<img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!"/>

<link rel="shortcut icon" href="{{ absolute_url('favicon.png') }}">

Побудова, версіонування та більш просунутий CSS, обробка JavaScript та зображень

Для допомоги з побудовою, версіонуванням та зменшенням ваших ресурсів JavaScript та CSS сучаснним чином, прочитайте про Symfony Webpack Encore.

Глобальна змінна для всього додатку

Symfony створює об'єкт контексту, який автоматично впроваджується у кожний шаблон Twig у вигляді змінної під назвою app. Вона надає доступ до деякої інформації додатку:

1
2
3
4
5
<p>Username: {{ app.user.username ?? 'Anonymous user' }}</p>
{% if app.debug %}
    <p>Request method: {{ app.request.method }}</p>
    <p>Application Environment: {{ app.environment }}</p>
{% endif %}

Змінна app (яка є екземпляром AppVariable) надає вам доступ до таких змінних:

app.user
Поточний об'єкт користувача або null, якщо користувач не аутентифікований.
app.request
Об'єкт Request, який зберігає поточні дані запиту data (в залежності від вашого додатку, це може бути підзапитом або звичайним запитом).
app.session
Об'єкт Session, який надає поточну сесію користувача або null, якщо її немає.
app.flashes
Масив усіх флеш-повідомлень , що зберігаються у сесії. Ви можете також отримати лише повідомлення певного типу (наприклад, app.flashes('notice')).
app.environment
Ім'я поточного середовища конфігурації (dev, prod, і т.д.).
app.debug
True, якщо у режимі налагодження . False - в інших режимах.
app.token
Об'єкт TokenInterface, що надає токен безпеки.
app.current_route
Імʼя маршруту, асоційованого з поточним запитом або null, якщо жодний запит не доступний (еквівалентно app.request.attributes.get('_route'))
app.current_route_parameters
Масив з параметрами, переданими маршруту для поточного запиту, або порожній масив, якщо жодний запит не доступний (еквівалентно app.request.attributes.get('_route_params'))
app.locale
Локаль, яка використовується у поточному контексті перемикача локалі .
app.enabled_locales
Локалі, включені у додатку.

На додаток до глобальної змінної app, впровадженої Symfony, ви також можете автоматично впроваджувати змінні до всіх шаблонів Twig, як описано у наступному розділі.

Глобальні змінні

Twig дозволяє вам автоматично впроваджувати одну або більше змінних у всі шаблони. Ці глобальні змінні визначені в опції twig.globals всередині основного файлу конфігурації Twig:

1
2
3
4
5
# config/packages/twig.yaml
twig:
    # ...
    globals:
        ga_tracking: 'UA-xxxxx-x'

Тепер, змінна ga_tracking доступна у всіх шаблонах Twig, тому ви можете використовувати її без необхідності чіткої передачі з контролера або сервісу, який відображає шаблон:

1
<p>The Google tracking code is: {{ ga_tracking }}</p>

На додаток до статичних значень, глобальні змінні Twig також можуть посилатися на сервіси з сервіс-контейнера. Головним недоліком є те, що ці сервіси не завантажуються ліниво. Іншими словами, як тільки завантажується Twig, ваш сервіс інстанціюєтьься, навіть якщо ви не викоритовуєте цю глобальну змінну.

Щоб визначити сервіс як глобальну змінну Twig, додайте до рядку ID сервісу префікс у вигляді символу @, що є звичним синтаксисом для посилання на сервіси у параметрах контейнера :

1
2
3
4
5
6
# config/packages/twig.yaml
twig:
    # ...
    globals:
        # the value is the service's id
        uuid: '@App\Generator\UuidGenerator'

Тепер ви можете використовувати змінну uuid у будь-якому шаблоні Twig, щоб отримати доступ до сервісу UuidGenerator:

1
UUID: {{ uuid.generate }}

Компоненти Twig

Компоненти Twig - це альтернативний спосіб відображення шаблонів, де кожний шаблон привʼязано до "класу компонента". Це робить простішим відображення та повторне виокристання маленьких "одиниць" шаблонів - таких як сповіщення, розмітка для модального вікна або бічної панелі категорій.

Щоб дізнатися більше, див. Компонент UX Twig.

Компоненти Twig також мають ще одну суперсилу: вони можуть ставати "живими", коли вони автоматично оновлюються (через Ajax) під час взаємодії користувача з ними. Наприклад, коли користувач друкує у полі, ваш компонент Twig повторно відобразиться через Ajax, щоб відобразити список результатів!

Щоб дізнатися більше, див. Компонент UX Live.

Відображення шаблонів

Відображення шаблону в контролерах

Якщо ваш контролер розширюється з AbstractController , використайте помічника render():

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
// src/Controller/ProductController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    public function index(): Response
    {
        // ...

        // метод `render()` повертає об'єкт `Response` зі змістом,
        // створеним шаблоном
        return $this->render('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        // метод `renderView()` повертає лише зміст, створений шаблоном,
        // тому ви можете використати цей зміст пізніше в об'єкті `Response`
        $contents = $this->renderView('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        return new Response($contents);
    }
}

Якщо ваш контролер не розширюється з AbstractController, вам буде необхідно отримати сервіси у вашому контролері та використати метод render() сервісу twig.

Ще однією опцією є використання атрибута #[Template()] у методі контролера, щоб визначити шаблон для відображення:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Controller/ProductController.php
namespace App\Controller;

use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    #[Template('product/index.html.twig')]
    public function index(): array
    {
        // ...

        // при використанні атрибута #[Template()], вам потрібно лише повернути масив
        // з параметрами для передачі шаблону (саме атрибут створить та поверне обʼєкт
        // Response).
        return [
            'category' => '...',
            'promotions' => ['...', '...'],
        ];
    }
}

base AbstractController також надає методи renderBlock()` та renderBlockView():

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
// src/Controller/ProductController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    // ...

    public function price(): Response
    {
        // ...

        // метод `renderBlock()` повертає обʼєкт `Response` зі змістом
        // блоку
        return $this->renderBlock('product/index.html.twig', 'price_block', [
            // ...
        ]);

        // метод `renderBlockView()` повертає лише зміст, створений блоком шаблона,
        // тому ви можете використовувати цей зміст пізніше в обʼєкті `Response`
        $contents = $this->renderBlockView('product/index.html.twig', 'price_block', [
            // ...
        ]);

        return new Response($contents);
    }
}

Це може стати в нагоді при роботі з блоками в наслідуванні шаблонів або при використанні потоків Turbo.

Відображення шаблону у сервісах

Впровадьте сервіс Symfony twig у ваші власні сервіси та використайте його метод render(). При використанні автомонтування сервісів, вам знадобиться лише додати аргумент у конструктор сервісу та додату підказку до класу Environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Service/SomeService.php
namespace App\Service;

use Twig\Environment;

class SomeService
{
    public function __construct(
        private Environment $twig,
    ) {
    }

    public function someMethod(): void
    {
        // ...

        $htmlContents = $this->twig->render('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);
    }
}

Відображення шаблону в електронних листах

Прочитайте документи про поштову програму та інтеграцію з Twig .

Відображення шаблону прямо з маршруту

Хоча шаблони зазвичай відображаються у контролерах та сервісах, ви можете відобразити статичні сторінки, які не вимагають змінних, прямо з визначення маршруту. Використайте спеціальний TemplateController, наданий Symfony:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# config/routes.yaml
acme_privacy:
    path:          /privacy
    controller:    Symfony\Bundle\FrameworkBundle\Controller\TemplateController
    defaults:
        # шлях шаблону для відображення
        template:  'static/privacy.html.twig'

        # статус-код відповіді (за замовчуванням: 200)
        statusCode: 200

        # спеціальні опції, визначені Symfony для установки кешу сторінки
        maxAge:    86400
        sharedAge: 86400

        # чи має кешування застосовуватися лише до кешів клієнтів
        private: true

        # за бажанням ви можете визначити деякі аргументи, передані шаблону
        context:
            site_name: 'ACME'
            theme: 'dark'

Перевірка існування шаблону

Шаблони завантажуються у застосунок з використанням завантажувача шаблонів Twig, який також надає метод для перевірки існування шаблонів. Спочатку отримайте завантажувач:

1
2
3
4
5
6
7
8
9
10
11
use Twig\Environment;

class YourService
{
    // це код припускає, що ваш сервіс використовує автомонтування для впровадження залежностей
    // у іншому випадку, вручну впровадьте сервіс під назвою 'twig'
    public function __construct(Environment $twig)
    {
        $loader = $twig->getLoader();
    }
}

Потім, передайте шлях шаблону Twig методу завантажувача exists():

1
2
3
4
if ($loader->exists('theme/layout_responsive.html.twig')) {
    // шаблон існує, зробіть щось
    // ...
}

Налагодження шаблонів

Symfony надає декілька утиліть для допомоги з налагодженням проблем у ваших шаблонах.

Перевірка дотримання стандартів кодування шаблонів Twig

Команда lint:twig перевіряє, щоб у ваших шаблонах Twig не було ніяки синтаксичних помилок. Корисно виконувати її до запуску вашого додатку у виробництво (наприклад, на вашому сервері безперервної інтеграції):

1
2
3
4
5
6
7
8
9
10
11
12
# перевірити всі шаблони додатку
$ php bin/console lint:twig

# ви також можете перевірити каталоги та окремі шаблони
$ php bin/console lint:twig templates/email/
$ php bin/console lint:twig templates/article/recent_list.html.twig

# ви також можете побачити застарілі функції, використовувані у ваших шаблонах
$ php bin/console lint:twig --show-deprecations templates/email/

# ви також можете виключити каталоги
$ php bin/console lint:twig templates/ --excludes=data_collector --excludes=dev_tool

7.1

Опція для виключення каталогів була представлена в Symfony 7.1.

При запуску лінтера всередині дій GitHub, виведення автоматично адаптується до формату, який вимагається GitHub, але ви можете також форсувати цей формат:

1
$ php bin/console lint:twig --format=github

Дослідження інформації Twig

Команда debug:twig перераховує всю доступну інфорамцію про Twig (функції, фільтри, глобальні змінні і т.д.). Вона корисна для перевірки того, чи правильно працюють ваші користувацькі розширення Twig, та перевірки функцій Twig, доданих при установці пакетів :

1
2
3
4
5
6
7
8
# перерахувати загальну інформацію
$ php bin/console debug:twig

# відфільтрувати виведення за будь-яким ключовим словом
$ php bin/console debug:twig --filter=date

# передати шлях шаблону, щоб вказати фізичний файл, який буде завантажено
$ php bin/console debug:twig @Twig/Exception/error.html.twig

Утиліти скидання Twig

Symfony надає функцію dump() в якості покращеної альтернативи PHP-функції var_dump(). Ця функція корисна для дослідження змісту будь-якої змінної, і ви можете використовувати її у шаблонах Twig.

Спочатку переконайтеся, що компонент VarDumper встановлений у додатку:

1
$ composer require symfony/var-dumper

Потім, використайте або тег {% dump %}, або функцію {{ dump() }}, в залежності від ваших потреб:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{# templates/article/recent_list.html.twig #}
{# зміст цієї змінної відправляється Панелі інструментів веб-налагодження
   замість його скидання всередині змісту сторінки #}
{% dump articles %}

{% for article in articles %}
    {# зміст цієї змінної скидається всередині змісту сторінки
       and they are visible on the web page #}
    {{ dump(article) }}

    {# опціонально, використайте іменовані аргументи, щоб відобразити їх як ярлики поруч
       зі скинутим змістом #}
    {{ dump(blog_posts: articles, user: app.user) }}

    <a href="/article/{{ article.slug }}">
        {{ article.title }}
    </a>
{% endfor %}

Щоб уникнути витоку конфіденційної інформації, функція/тег dump() доступна лише у середовищах конфігурації dev і test. Якщо ви спробуєте використати її у середовищі prod, то побачите PHP-помилку.

Повторне використання змісту шаблонів

Додавання шаблонів

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

1
2
3
4
5
6
7
{# templates/blog/index.html.twig #}

{# ... #}
<div class="user-profile">
    <img src="{{ user.profileImageUrl }}" alt="{{ user.fullName }}"/>
    <p>{{ user.fullName }} - {{ user.email }}</p>
</div>

Спочатку створіть новий шаблон Twig під назвою blog/_user_profile.html.twig (префікс _ необов'язковий, але це угода, використовувана для кращої диференціації між повними шаблонами та їхніми фрагментами).

Потім, видаліть цей зміст з початкового шаблону blog/index.html.twig, і додайте наступне, щоб додати фрагмент шаблону:

1
2
3
4
{# templates/blog/index.html.twig #}

{# ... #}
{{ include('blog/_user_profile.html.twig') }}

Функція Twig include() бере аргумент шляху шаблону, щоб додати його. Доданий шаблон має доступ до всіх змінних шаблону, який його містить (використовуйте опцію with_context, щоб контролювати це).

Ви також можете передати змінні доданому шаблону. Це корисно, наприклад, для перейменування змінних. Уявіть, що ваш шаблон зберігає інформацію користувача у змінній під назвою blog_post.author, а не змінній user, яку очікує фрагмент шаблону. Використайте наступне, щоб перейменувати змінну:

1
2
3
4
{# templates/blog/index.html.twig #}

{# ... #}
{{ include('blog/_user_profile.html.twig', {user: blog_post.author}) }}

Вбудовування контролерів

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

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

Кращою алтернативою буде вбудувати результат виконання деякого контролера з функціями Twig render() і controller().

Спочатку створіть контролер, який відображає визначене число недавніх статей:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/BlogController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
// ...

class BlogController extends AbstractController
{
    public function recentArticles(int $max = 3): Response
    {
        // якось отримати найсвіжіші статті (наприклад, зробити запит до бази даних)
        $articles = ['...', '...', '...'];

        return $this->render('blog/_recent_articles.html.twig', [
            'articles' => $articles
        ]);
    }
}

Потім створіть фрагмент шаблону blog/_recent_articles.html.twig (префікс _ у назві шаблону необов'язковий, але це угода, використовувана для кращої диференціації між повними шаблонами та їхніми фрагментами):

1
2
3
4
5
6
{# templates/blog/_recent_articles.html.twig #}
{% for article in articles %}
    <a href="{{ path('blog_show', {slug: article.slug}) }}">
        {{ article.title }}
    </a>
{% endfor %}

Тепер ви можете викликати цей контролер з будь-якого шаблону, щоб вбудувати його результат:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{# templates/base.html.twig #}

{# ... #}
<div id="sidebar">
    {# якщо контролер асоційований з маршрутом, використайте функції path() або url() #}
    {{ render(path('latest_articles', {max: 3})) }}
    {{ render(url('latest_articles', {max: 3})) }}

    {# якщо ви не хочете розкривати контролер публічним URL, використовуйте
       функцію controller(), щоб визначити контролер для виконання #}
    {{ render(controller(
        'App\\Controller\\BlogController::recentArticles', {max: 3}
    )) }}
</div>

При використанні функції controller(), контролери не доступні з використанням звичайного маршруту Symfony, але доступні через спеціальний URL, використовуваний виключно для обслуговування цих фрагментів шаблону. Сконфігуруйте цей спеціальний URL в опції fragments:

1
2
3
4
# config/packages/framework.yaml
framework:
    # ...
    fragments: { path: /_fragment }

Caution

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

Як вбудовувати асинхронний зміст з hinclude.js

Шаблони також можуть вбудовувати зміст асинхронно за допомогою бібліотеки JavaScript hinclude.js.

Спочатку, додайте бібліотеку hinclude.js на вашу сторінку,
пославшись на неї з шаблону або додавши її у JavaScript вашого додатку, використовуючи Webpack Encore.

Так як вбудований зміт походить з іншої сторінки (або контролера, якщо на те пішло), Symfony використовує версію стандартної функції render() для конфігурації тегів hinclude у шаблонах:

1
2
{{ render_hinclude(controller('...')) }}
{{ render_hinclude(url('...')) }}

Note

При використанні функції controller(), ви також маєте сконфігурувати опцію шляху фрагментів .

Якщо JavaScript відключено або займає забагато часу для завантаження, ви можете відобразити зміст за замовчуванням, відобразивши якийсь шаблон:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    # ...
    fragments:
        hinclude_default_template: hinclude.html.twig

Ви можете визначити шаблони за замовчуванням для функції render() (що перевизначить будь-який глобально визначений за замовчуванням шаблон):

1
2
3
{{ render_hinclude(controller('...'),  {
    default: 'default/content.html.twig'
}) }}

Або ви також можете вказати рядок длля відображення в якості змісту за замовчуванням:

1
{{ render_hinclude(controller('...'), {default: 'Loading...'}) }}

Використайте опцію attributes, щоб визначити значення опцій hinclude.js:

1
2
3
4
5
6
7
{# за замовчуванням, міжсайтові запити не використовують повноваження типу куки, авторизації,
   заголовків або сертифікатів клієнтів TLS; встановіть цю опцію як 'true', щоб використовувати їх #}
{{ render_hinclude(controller('...'), {attributes: {'data-with-credentials': 'true'}}) }}

{# за замовчуванням, код JavaScript, включений у завантажений зміст, не виконується;
   встановіть цю опцію як 'true', щоб виконати цей код JavaScript #}
{{ render_hinclude(controller('...'), {attributes: {evaljs: 'true'}}) }}

Наслідування шаблонів та макети

У міру зростання вашого додатку, ви будете знаходити все більше повторюваних елементів між сторінками, таких як заголовки, нижні колонтитули, бічні панелі і т.д. Додавання шаблонів і вбудовування контролероів може допомогти, але коли сторінки мають спільну структуру, краще використовувати наслідування.

Концепт наслідування шаблонів Twig схожий на наслудвання PHP-класів. Ви визначаєте батьківський шаблон, з якого можуть розширюватися інші шаблони, а дочірні шаблони можуть перевизначати частини батьківського.

Symfony рекомендує наступне трьохрівневе наслідування шаблонів для середніх та складних додатків:

  • templates/base.html.twig, визначає спільні елементи всіх шаболнів додатку, такі як <head>, <header>, <footer>, і т.д..;
  • templates/layout.html.twig, розширюється з base.html.twig та визначає структуру змісту, використовувану на (практично) всіх сторінках, таку як зміст у двох колонках + макет бічної панелі. Деякі розділи додатку можуть визначать свої власні макети (наприклад, templates/blog/layout.html.twig);
  • templates/*.html.twig, сторінки додатку, що розширюються з головного шаблону layout.html.twig або будь-кого іншого макета розділу.

На практиці, шаблон base.html.twig виглядатиме так:

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
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}My Application{% endblock %}</title>
        {% block stylesheets %}
            <link rel="stylesheet" type="text/css" href="/css/base.css"/>
        {% endblock %}
    </head>
    <body>
        {% block body %}
            <div id="sidebar">
                {% block sidebar %}
                    <ul>
                        <li><a href="{{ path('homepage') }}">Home</a></li>
                        <li><a href="{{ path('blog_index') }}">Blog</a></li>
                    </ul>
                {% endblock %}
            </div>

            <div id="content">
                {% block content %}{% endblock %}
            </div>
        {% endblock %}
    </body>
</html>

Тег блоків Twig визначає розділи сторінки, які можна перевизначити у дочірніх шаблонах. Вони можуть бути порожніми, як блок content, або визначати зміст за замовчуванням, як блок title, який відображається, коли дочірній шаблон їх не перевизначає.

Шаблон blog/layout.html.twig може бути таким:

1
2
3
4
5
6
7
8
{# templates/blog/layout.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>Blog</h1>

    {% block page_contents %}{% endblock %}
{% endblock %}

Шаблон розширюється з base.html.twig та визначає лише зміст блоку content. Решта блоків батьківського шаблону будуть відображати свій зміст за замовчуванням. Однак, вони можуть бути перевизначені наслідуванням шаблонів третього рівня, таким як blog/index.html.twig, який відображає індекс блогу:

1
2
3
4
5
6
7
8
9
10
11
{# templates/blog/index.html.twig #}
{% extends 'blog/layout.html.twig' %}

{% block title %}Blog Index{% endblock %}

{% block page_contents %}
    {% for article in articles %}
        <h2>{{ article.title }}</h2>
        <p>{{ article.body }}</p>
    {% endfor %}
{% endblock %}

Цей шаблон розширюється з шаблону другого рівня (blog/layout.html.twig), але перевизначає блоки різних батьківських шаблонів: page_contents з blog/layout.html.twig і title з base.html.twig.

Коли ви відображаєте шаблон blog/index.html.twig, Symfony використовує три різних шаблони для створення фінального змісту. Цей механізм наслідування посилює вашу продуктивність, тому що кожний шаблон містить лише свій унікальний зміст, і залишає повторюваний зміст та HTML-структуру якимось батьківським шаблонам.

Caution

При використанні extends, дочірньому шаблону заборонено визначати частини шаблону поза блоком. Наступний код викликає помилку SyntaxError:

1
2
3
4
5
6
7
8
{# app/Resources/views/blog/index.html.twig #}
{% extends 'base.html.twig' %}

{# рядок нижче не зафіксований тегом "block" #}
<div class="alert">Some Alert</div>

{# наступне є валідним #}
{% block content %}My cool blog posts{% endblock %}

Прочитайте документи наслідування шаблонів Twig, щоб дізнаится більше про те, як при перевизначенні використовувати зміст батьківських блоків повторно, та про інші просунуті функції.

Екранування виведення

Уявіть, що ваш шаблон містить код Hello {{ name }} для відображення імені користувача. Якщо зловмисний користувач встановить <script>alert('hello!')</script> в якості свого імені, і ви виведете це значення без змін, застосунок відобразить спливаюче вікно JavaScript.

Це відомо як атака міжсайтового скриптингу (XSS). І хоча попередній приклад виглядає невинним, атакуючий може написати більш просунутий код JavaScript, щоб виконати зловмисні дії.

Щоб запобігти цій атаці, використовуйте "екранування виведення", щоб перетворити символи, які мають особливе значення (наприклад, замініть < на HTML-сутність &lt;). Додатки Symfony безпечні за замовчуванням, так як вони виконують автоматичне екранування виведення завдяки опції автоекранування Twig :

1
2
3
<p>Hello {{ name }}</p>
{# якщо 'name' - '<script>alert('hello!')</script>', Twig виведе це:
   '<p>Hello &lt;script&gt;alert(&#39;hello!&#39;)&lt;/script&gt;</p>' #}

Якщо ви відображаєте змінну, якій можна довіряти, і яка має HTML-зміст, використовуйте фільтр Twig raw, щоб відключити екранування виведення для цієї змінної:

1
2
3
<h1>{{ product.title|raw }}</h1>
{# якщо 'product.title' - 'Lorem <strong>Ipsum</strong>', Twig виведе
   саме це замість 'Lorem &lt;strong&gt;Ipsum&lt;/strong&gt;' #}

Прочитайте документацію екранування виведення Twig, щоб дізнатися більше про те, як відключити екранування виведення для блоку або навіть всього шаблону.

Простори імен шаблонів

Хоча більшість додатків зберігає свої шаблони у каталозі за замовчуванням templates/, вам може знадобитися зберігати деякі (або всі) з них у інших каталогах. Використайте опцію twig.paths, щоб сконфігурувати ці додаткові каталоги. Кожний шлях визначається як пара key: value, де key - це каталог шаблону, а value - простір імен Twig, що пояснюється пізніше:

1
2
3
4
5
6
7
8
# config/packages/twig.yaml
twig:
    # ...
    paths:
        # каталоги відносні до кореневого каталогу проекту (але ви також
        # можете використовувати абсолютні каталоги)
        'email/default/templates': ~
        'backend/templates': ~

При відображенні шаблону, Symfony спочатку шукає його у каталогах twig.paths, які не визначають простір імен, а потім повертається до каталогу шаблонів за замовчуванням (зазвичай, templates/).

Використовуючи конфігурацію вище, якщо ваш застосунок відображає, наприклад, шаблон layout.html.twig, Symfony спочатку шукатиме у email/default/templates/layout.html.twig і backend/templates/layout.html.twig. Якщо будь-який з цих шаблонів існує, Symfony використає його замість використання templates/layout.html.twig, щоб скоріш за все є тим шаблоном, який ви хотіли використати.

Twig вирішує цю проблему за допомогою просторів імен, які групують декілька шаблонів під одним логічним ім'ям, не пов'язаним з їх реальним розташуванням. Оновіть попередню конфігурацію, щоб визначити простір імен для кожного каталогу шаблонів:

1
2
3
4
5
6
# config/packages/twig.yaml
twig:
    # ...
    paths:
        'email/default/templates': 'email'
        'backend/templates': 'admin'

Тепер, якщо ви відобразите шаблон layout.html.twig, Symfony відображатиме файл templates/layout.html.twig. Використайте спеціальний синтаксис @ + простір імен, щоб посилатися на інші шаблони з просторами імен (наприклад, @email/layout.html.twig і @admin/layout.html.twig).

Note

Один простір імен Twig може асоціюватися більш ніж з одним каталогом шаблонів. У такому випадку, порядк, в якому додаються шляхи, важливий, так як Twig почне шукати шаблони з першого визначеного шляху.

Шаблони пакетів

Якщо ви встановлюєте пакети у своєму додатку, вони можуть містити власні шаблони Twig (у каталозі Resources/views/ кожного пакета). Щоб уникнути плутанини з вашими власними шаблонами, Symfony додає шаблони пакетів під автоматичним простором імен, створеним за ім'ям пакета.

Наприклад, шаблони під назвою AcmeFooBundle доступні під простором імен AcmeFoo. Якщо цей пакет включає у себе шаблон <your-project>/vendor/acmefoo-bundle/Resources/views/user/profile.html.twig, ви можете послатися на нього так @AcmeFoo/user/profile.html.twig.

Tip

Ви також можете перевизначати шаблони пакетів у випадку, якщо ви хочете змінити деякі частини початкових шаблонів пакета.

Написання розширення Twig

Розширення Twig дозволяють створення користувацьких функцій, фільтрів та іншого для використання у ваших шаблонах Twig. Перерд тим, як писати ваше власне розширення Twig, перевірте, чи не реалізовані вже фільтр/функція, які вам потрібні, у:

Створіть клас розширення

Уявіть, що ви хочете створити новий фільтр під назвою price, який форматує число як валюту:

1
2
3
4
{{ product.price|price }}

{# передати у 3 опціональних аргументах #}
{{ product.price|price(2, ',', '.') }}

Створіть клас, що розширює AbstractExtension та заповніть логіку:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Twig/AppExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters()
    {
        return [
            new TwigFilter('price', [$this, 'formatPrice']),
        ];
    }

    public function formatPrice($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',')
    {
        $price = number_format($number, $decimals, $decPoint, $thousandsSep);
        $price = '$'.$price;

        return $price;
    }
}

Якщо ви хочете створити функкцію замість фільтру, визначіть метод getFunctions():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Twig/AppExtension.php
namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class AppExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('area', [$this, 'calculateArea']),
        ];
    }

    public function calculateArea(int $width, int $length): int
    {
        return $width * $length;
    }
}

Tip

Окрім користувацьких фільтрів та функцій ви також можете зареєструвати глобальні змінні.

Реєстрація розширення як сервісу

Далі, зареєструйте ваш клас як сервіс та додайте тег twig.extension. Якщо ви використовуєте конфігурацію services.yaml за замовчуванням , ви закінчили! Symfony автоматично знатиме про ваш новий сервіс та додасть тег.

Тепер ви можете почати використовувати ваш фільтр у будь-якому шаблоні Twig. За бажанням, виконайте цю команду, щоб підтвердити, що ваш новий фільтр був успішно зареєстрований:

1
2
3
4
5
# відобразити всю інформацію про Twig
$ php bin/console debug:twig

# відобразити тільки інформацію про конкретний фільтр
$ php bin/console debug:twig --filter=price

Створення ліниво завантажуваних розширень Twig

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

Якщо розширення не визначають залежностей (тобто, якщо ви не впрооваджуєте в них сервіси), це не впливає на продуктивність. Однак, якщо розширення визначають багато складних залежностей (наприклад, тих, що створюють з'єднання з базами даних), втрата продуктивності може бути значною.

Ось чому Twig дозволяє відокремити визначення розширення від його реалізації. Дотримуючись того ж прикладу, що і раніше, першою зміною буде видалити метод formatPrice() з розширення і оновити PHP, визначений у getFilters():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Twig/AppExtension.php
namespace App\Twig;

use App\Twig\AppRuntime;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            // логіка цього фільтру тепе реалізується в іншому класі
            new TwigFilter('price', [AppRuntime::class, 'formatPrice']),
        ];
    }
}

Потім створіть новий клас AppRuntime (це не обов'язково, але ці класи мають суфікс Runtime за домовленістю) і додайте логіку попереднього методу formatPrice():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Twig/AppRuntime.php
namespace App\Twig;

use Twig\Extension\RuntimeExtensionInterface;

class AppRuntime implements RuntimeExtensionInterface
{
    public function __construct()
    {
        // цей простий приклад не визначає ніякої залежності, але у ваших власних
        // розширеннях вам знадобиться впроваджувати сервіси, використовуючи цей конструктор
    }

    public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string
    {
        $price = number_format($number, $decimals, $decPoint, $thousandsSep);
        $price = '$'.$price;

        return $price;
    }
}

Якщо ви використовуєте конфігурацію services.yaml за замовчуванням, це вже працюватиме! В іншому випадку, створіть сервіс для цього класу і додайте до вашого сервісу тег twig.runtime.