Создание и использование шаблонов
Дата обновления перевода 2024-08-01
Создание и использование шаблонов
Шаблон - это лучший способ организовать и отобразить 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 пытается получить значение переменной
в следующем порядке:
$foo['bar']
(массив и элемент);$foo->bar
(объект и публичное свойство);$foo->bar()
(объект и публичный метод);$foo->getBar()
(объект и метод getter);$foo->isBar()
(объект и метод isser);$foo->hasBar()
(объект и метод hasser);- Если ничего из вышеперечисленного не существует, используйте
null
(или вызовите исключение, если включена опция 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>
{# ... #}
{% for post in 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:
# значение 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' => ['...', '...'],
];
}
}
Базовый 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 Streams.
Отображение шаблона в сервисах
Внедрите сервис 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' => ['...', '...'],
]);
}
}
Отображение шаблона в электронных письмах
Прочтите документы об интеграции Mailer и 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 template методу загрузчика 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 %}
{# содержание этой переменной сбрасывается в содержание страницы,
и видимо на веб-странице #}
{{ 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'}}) }}
Наследование шаблонов и макеты
По мере роста вашего приложения, вы будете находить все больше повторяющихся элементов между страницами, таких как заголовки, нижние колонтитулы, боковые панели и т.д. Добавление шаблонов и встраивание контроллеров controllers может помочь, но когда страницы имеют общую структуру, лучше использовать наследование.
Концепт наследования шаблонов 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-сущность <
). Приложения Symfony безопасны по умолчанию, так как они
выполняют автоматическое экранирование вывода:
1 2 3
<p>Hello {{ name }}</p>
{# если 'name' - '<script>alert('hello!')</script>', Twig выведет это:
'<p>Hello <script>alert('hello!')</script></p>' #}
Если вы отображаете переменную, которой можно доверять, и которая имеет HTML-содержание, используйте фильтр Twig raw, чтобы отключить экранирование вывода для этой переменной:
1 2 3
<h1>{{ product.title|raw }}</h1>
{# если 'product.title' - 'Lorem <strong>Ipsum</strong>', Twig выведет
именно это вместо 'Lorem <strong>Ipsum</strong>' #}
Прочтите документацию экранирования вывода 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, поверьте, не был ли уже реализован нужный вам фильтр/функция в:
- Фильтах и функциях Twig по умолчанию;
- Фильтрах и функциях Twig, добавленных Symfony;
- Официальных расширениях Twig, связанных со строками, HTML, разметой, интернационализацией и т.д.
Создайте класс расширения
Представьте, что вы хотите создать новый фильтр под названием 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(): array
{
return [
new TwigFilter('price', [$this, 'formatPrice']),
];
}
public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string
{
$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-вызываемое, определенное в getFice()
:
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($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',')
{
$price = number_format($number, $decimals, $decPoint, $thousandsSep);
$price = '$'.$price;
return $price;
}
}
If you're using the default services.yaml
configuration, this will already
work! Otherwise, create a service
for this class and tag your service with twig.runtime
.