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

03/07/2015 0 шаблон, генератор шаблонів, шаблон Twig, шаблон PHP, екранування

Продовження. Початок читайте тут:

1) "Створення та використання шаблонів (частина 1)";

2) "Створення та використання шаблонів (частина 2)".

Підключення у Twig таблиць стилів та скриптів Java

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

! Даний параграф розділу “Створення та використання шаблонів” (частина 3) познайомить вас із філософією того, що ж стоїть за підключенням таблиць стилів та JavaScript у Symfony. Symfony має у своєму складі окрему бібліотеку під назвою Assetic, яка й підпадає під обговорювану нами філософію, але дозволяє робити із цими ресурсами набагато цікавіші речі! Зацікавилися? В такому разі більше інформації про використання Assetic ви отримаєте за посиланням "Як Використовувати Assetic для Управління Ресурсами".

Розпочнімо з додавання до уже існуючої бази даних шаблонів ще двох блоків, що будуть зберігати ваші ресурси: першу назвемо stylesheets (поміщаємо всередині тегу head), а іншу - javascripts (розміщуємо саме над місцем закриття тегу body). Дані блоки міститимуть усі таблиці стилів та усі скрипти Java, що знадобляться вам по всьому сайту:

Twig

{# app/Resources/views/base.html.twig #}
<html>
    <head>
        {# ... #}

        {% block stylesheets %}
            <link href="{{ asset('css/main.css') }}" rel="stylesheet" />
        {% endblock %}
    </head>
    <body>
        {# ... #}

        {% block javascripts %}
            <script src="{{ asset('js/main.js') }}"></script>
        {% endblock %}
    </body>
</html>

PHP

// app/Resources/views/base.html.php
<html>
    <head>
        <?php ... ?>

        <?php $view['slots']->start('stylesheets') ?>
            <link href="<?php echo $view['assets']->getUrl('css/main.css') ?>" rel="stylesheet" />
        <?php $view['slots']->stop() ?>
    </head>
    <body>
        <?php ... ?>

        <?php $view['slots']->start('javascripts') ?>
            <script src="<?php echo $view['assets']->getUrl('js/main.js') ?>"></script>
        <?php $view['slots']->stop() ?>
    </body>
</html>

Це ж так просто! Але що робити, коли вам потрібно буде підключити додаткову таблицю стилів чи певний JavaScript з дочірнього шаблона? Припустімо, вас уже є сторінка з контактами і потрібно тепер підключити таблицю стилів contact.css тільки для цієї одної сторінки. В середині шаблона цієї сторінки зробіть наступні маніпуляції:

Twig

{# app/Resources/views/contact/contact.html.twig #}
{% extends 'base.html.twig' %}

{% block stylesheets %}
    {{ parent() }}

    <link href="{{ asset('css/contact.css') }}" rel="stylesheet" />
{% endblock %}

{# ... #}

PHP

// app/Resources/views/contact/contact.html.twig
<?php $view->extend('base.html.php') ?>

<?php $view['slots']->start('stylesheets') ?>
    <link href="<?php echo $view['assets']->getUrl('css/contact.css') ?>" rel="stylesheet" />
<?php $view['slots']->stop() ?>

У дочірньому шаблоні ви просто перевизначаєте блок stylesheets і розміщуєте тег нового стилю усередині даного блоку. Звісно, оскільки ви просто хочете додати деякий контент до батьківського блоку (а не повністю перезаписати його), просто використайте Twig функцію parent() для підключення всього того, що є у блоці stylesheets базового шаблона.

Ви також маєте змогу підключити ресурси, розміщені у папці Resources/public ваших пакетів. Вам потрібно запустити команду php app/console assets:install target [--symlink], яка перемістить файли до потрібного місця (чи зробить символічну вказівку на них). По замовчуванню вони перемістяться у “web”.

<link href="{{ asset('bundles/acmedemo/css/contact.css') }}" rel="stylesheet" />

Кінцевий результат - це сторінка, що включає як таблицю стилів main.css, так і contact.css.

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

Під час кожнісінького запиту Symfony по замовчуванню визначатиме глобальну змінну шаблонів app у генераторах шаблонів Twig та PHP. Змінна app - приклад Глобальної Змінної, яка автоматично надасть вам доступи до деяких особливих змінних додатку:

app.security

Контекст безпеки.

app.user

Поточний об'єкт користувача.

app.request

Об'єкт запит.

app.session

Об'єкт сесії.

app.environment

Поточне середовище (dev, prod тощо).

app.debug

Якщо в режимі debug, присвоїться значення true. В іншому випадку буде false.

Twig

<p>Username: {{ app.user.username }}</p>
{% if app.debug %}
    <p>Request method: {{ app.request.method }}</p>
    <p>Application Environment: {{ app.environment }}</p>
{% endif %}

PHP

<p>Username: <?php echo $app->getUser()->getUsername() ?></p>
<?php if ($app->getDebug()): ?>
    <p>Request method: <?php echo $app->getRequest()->getMethod() ?></p>
    <p>Application Environment: <?php echo $app->getEnvironment() ?></p>
<?php endif ?>

! Для версії Symfony 2.6

Глобальна змінна app.security (або метод $app->getSecurity() у шаблонах PHP) не рекомендується для використання у версії Symfony 2.6. Використовуйте натомість app.user ($app->getUser()) та is_granted() ($view['security']->isGranted()).

! Ви можете самостійно додавати власні глобальні змінні шаблонів. Детальніше див. у прикладі Довідника в розділі про Глобальні Змінні.

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

Справжнісіньким серцем системи створення та оперування шаблонами є шаблонізатор, або просто engine. Цей особливий об'єкт відповідальний за відображення шаблонів та повернення їх вмісту. Так, коли ви відображаєте певний шаблон, скажімо, у контролері, ви безпосередньо використовуєте сервіс шаблонізації. Для прикладу:

return $this->render('article/index.html.twig');

є повним еквівалентом наступному:

use Symfony\Component\HttpFoundation\Response;

$engine = $this->container->get('templating');
$content = $engine->render('article/index.html.twig');

return $response = new Response($content);

Шаблонізатор (або просто "сервіс") попередньо налаштований для автоматичного виконання роботи всередині Symfony. Звісно, його й пізніше можна налаштувати у файлі налаштувань додатку:

YAML

# app/config/config.yml
framework:
    # ...
    templating: { engines: ['twig'] }

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <!-- ... -->
    <framework:config>
        <framework:templating>
            <framework:engine>twig</framework:engine>
        </framework:templating>
    </framework:config>
</container>

PHP

// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...

    'templating' => array(
        'engines' => array('twig'),
    ),
));

У вашому розпорядженні декілька опцій налаштування і всі вони описуються у додатку Configuration Appendix.

! Шаблонізатор twig є обов'язковим для використання тоді, коли ви застосовуєте веб-профайлер (а також чимало інших сторонніх пакетів).

Перевизначення шаблонів пакета

Спільнота Symfony пишається тим, що створила й забезпечує підтримку високоякісних пакетів (див. KnpBundles.com) для найрізноманітніших функцій. Як тільки ви почнете використовувати у своїй роботі певний сторонній пакет, вам, швидше за все, доведеться перевизначити та налаштувати під свої потреби один чи кілька його шаблонів.

Уявімо, що ви встановили для свого проекту AcmeBlogBundle - вигаданий нами щойно пакет з відкритим вихідним кодом. Назагал ви усім задоволені, от тільки б ще перевизначити сторінку блогу "list" (“список”) та адаптувати її саме під макет вашого додатку! “Покопирсавшись” трішки у контролері Blog пакета AcmeBlogBundle, виявите наступне:

public function indexAction()
{
    // some logic to retrieve the blogs
    $blogs = ...;

    $this->render(
        'AcmeBlogBundle:Blog:index.html.twig',
        array('blogs' => $blogs)
    );
}

Коли відображається шаблон AcmeBlogBundle:Blog:index.html.twig, Symfony його шукає у двох різних місцях:

1. app/Resources/AcmeBlogBundle/views/Blog/index.html.twig

2. src/Acme/BlogBundle/Resources/views/Blog/index.html.twig

Для того, аби перевизначити пакетний шаблон, просто скопіюйте шаблон index.html.twig з пакета до app/Resources/AcmeBlogBundle/views/Blog/index.html.twig (директорії app/Resources/AcmeBlogBundle не існуватиме, тому вам потрібно її створити). А ось тепер і можна налаштовувати шаблон під свої вимоги.

! Додаєте чи переносите шаблон у нове місце? В такому разі, швидше за все, доведеться почистити кеш (php app/console cache:clear), навіть якщо ви знаходитеся в режимі налагодження (debug mode).

З подібною логікою ви ще зустрінетеся при роботі з базовими шаблонами пакета. Припустімо також, що кожен шаблон в AcmeBlogBundle наслідує базовий шаблон під назвою AcmeBlogBundle::layout.html.twig. Як ми й писали вище, Symfony шукатиме шаблон одразу у двох місцях:

1. app/Resources/AcmeBlogBundle/views/layout.html.twig

2. src/Acme/BlogBundle/Resources/views/layout.html.twig

І ще раз повторимося: для того, щоб перевизначити шаблон, просто скопіюйте його з пакета до app/Resources/AcmeBlogBundle/views/layout.html.twig. Після цього можете адаптувати шаблон так, як вам заманеться.

А тепер повернемося на крок назад. Ви зауважите, що Symfony завжди розпочинає пошуки шаблона з директорії app/Resources/{BUNDLE_NAME}/views/. І коли виявляється, що запитуваного шаблона там немає, він починає перевіряти директорію Resources/views самого пакета. Що це значить? А значить це, що усі шаблони пакета можна перевизначити, просто розмістивши їх до правильної піддиректорії app/Resources.

! Ви також можете перевизначити шаблони зсередини самого пакета, використовуючи наслідування пакетів. Більше про це читайте в оригіналі за посиланням How to Use Bundle Inheritance to Override Parts of a Bundle.

Перевизначення шаблонів ядра

Так як фреймворк Symfony сам по собі - це просто пакет, ті шаблони, що лежать у ядрі Symfony, можна перевизначати та переписувати аналогічним чином, як ми уже описували вище. Для прикладу, пакет ядра TwigBundle містить численні шаблони “виняток” ("exception") та “помилка” ("error"), і кожен з них можна перевизначати через копіювання з директорії Resources/views/Exception пакета TwigBundle до (як ви, напевно, уже здогадалися) директорії app/Resources/TwigBundle/views/Exception.

Трирівневе наслідування

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

      - створити файл app/Resources/views/base.html.twig, що міститиме основну розмітку вашого додатку (власне, як ми бачили у попередньому прикладі). Внутрішньосистемна назва цього шаблона - base.html.twig;

      - створити шаблон для кожного “розділу” вашого сайту. Наприклад, функціонал блога покриється шаблоном blog/layout.html.twig, який буде містити лише специфічні для “розділу” блог елементи;

{# app/Resources/views/blog/layout.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Blog Application</h1>

    {% block content %}{% endblock %}
{% endblock %}

      - створити окремі шаблони для кожної зі сторінок і зробити так, аби кожен з них розширював відповідний шаблон цілого розділу. Наприклад, сторінка "index" гіпотетично могла би називатися blog/index.html.twig і видавала би список усіх актуальних постів у блозі.

{# app/Resources/views/blog/index.html.twig #}
{% extends 'blog/layout.html.twig' %}

{% block content %}
    {% for entry in blog_entries %}
        <h2>{{ entry.title }}</h2>
        <p>{{ entry.body }}</p>
    {% endfor %}
{% endblock %}

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

При створенні власного додатку саме вам обирати, чи використовувати даний метод, а чи проігнорувати його, просто розширюючи базовий шаблон шаблонами кожної окремої сторінки напряму (наприклад, {% extends 'base.html.twig' %}). Тришаблонна модель вважається наразі кращою практикою у ситуаціях, коли треба використати пакети третіх сторін, щоб, таким чином, завжди можна було перевизначити базовий шаблон пакета і правильно розширити базову розмітку додатку.

Екранування

При створені HTML із шаблона завжди треба пам'ятати про можливий ризик: змінна шаблона може містити небажаний HTML чи небезпечний клієнтський скрипт. Що ж тоді отримаємо в результаті? Динамічний контент може зламати HTML код сторінки-результату чи дати шанс зловмисникам здійснити атаку під назвою Міжсайтовий Скриптінг (XSS). Розглянемо класичний приклад:

Twig

Hello {{ name }}

PHP

Hello <?php echo $name ?>

Уявіть, що користувач для свого імені вводить наступний код:

<script>alert('hello!')</script>

Без екранування шаблон, який ми отримаємо в результаті, викине нам JavaScript повідомлення у виринаючому вікні:

Hello <script>alert('hello!')</script>

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

Але не поневіряйтеся, є вихід! І це екранування (чи “output escaping” англійською). З ним той самий шаблон матиме безневинний вигляд, і він буквально вдрукує на екран тег script:

Hello &lt;script&gt;alert(&#39;helloe&#39;)&lt;/script&gt;

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

Екранування у Twig

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

В деяких випадках екранування потрібно буде деактивувати, а саме коли ви відображаєте змінну, якій довіряєте та яка містить розмітку, яку не можна екранувати. Уявімо, що користувачі з доступом адміністратора можуть писати статті з HTML кодом. За замовчуванням Twig буде екранувати тіло статті.

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

{{ article.body|raw }}

Ви також можете деактивувати екранування всередині блоку {% block %} чи навіть для цілого шаблона. Детальніше про це можна почитати у Twig документації за посиланням Output Escaping.

Екранування у PHP

При роботі PHP шаблонами екранування не здійснюється автоматично. А це означає, що допоки ви не екранізуєте змінну вручну, ви незахищені. Щоб використати екранування, використовуйте спеціальний метод перегляду escape():

Hello <?php echo $view->escape($name) ?>

За замовчуванням метод escape() припускає, що змінна відображається у контексті HTML (і тому змінна екранізується, аби для HTML бути безпечною). Другий аргумент дозволяє вам змінити контекст. наприклад, щоб вивести будь-який рядок у JavaScript, спробуйте контекст js:

var myMsg = 'Hello <?php echo $view->escape($name, 'js') ?>';

Налагодження

Використовуючи PHP, ви можете застосувати функцію dump() компонента VarDumper, якщо потрібно швидко визначити значення пропущеної змінної. Це справді стане у нагоді у контролері, наприклад:

// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;

// ...

class ArticleController extends Controller
{
    public function recentListAction()
    {
        $articles = ...;
        dump($articles);

        // ...
    }
}

! Після цього вся вихідна інформація функції dump() відображається у панелі інструментів веб-розробника.

Цей же механізм можна застосувати у Twig шаблонах за посередництвом функції dump:

{# app/Resources/views/article/recent_list.html.twig #}
{{ dump(articles) }}

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

Змінні будуть скинуті тільки тоді, коли налаштування debug у Twig (а саме у config.yml) буде true. За замовчуванням це означає, що змінні буде скинуто до середовища dev, але не до prod.

Перевірка синтаксису

Використовуючи консольну команду twig:lint, можна здійснити перевірку шаблонів Twig на помилки:

# You can check by filename:
$ php app/console twig:lint app/Resources/views/article/recent_list.html.twig

# or by directory:
$ php app/console twig:lint app/Resources/views

Формати шаблонів

Шаблони - це узагальнений спосіб відображення контенту в будь-якому форматі. І хоча у більшості випадків пересічний користувач буде використовувати шаблони для відображення HTML контенту, шаблон може з легкістю генерувати і JavaScript, і CSS, і XML чи будь-який інший формат, який тільки спаде вам на думку.

Для прикладу, один і той же “ресурс” ("resource") часто відображається одразу у кількох форматах. Для відображення сторінки index статей блогу у форматі XML просто включіть назву формату до назви самого шаблона:

      - назва шаблона XML: article/index.xml.twig

      - назва файлу шаблона XML: index.xml.twig

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

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

public function indexAction(Request $request)
{
    $format = $request->getRequestFormat();

    return $this->render('article/index.'.$format.'.twig');
}

getRequestFormat об'єкта Request приймає по замовчуванню html, але він може повертати будь-який інший формат на основі того, який саме формат запитував користувач. Маршрутизатор найчастіше й керує запитуваним форматом, і маршрут можна налаштувати так, аби /contact надсилав запит на формат html, а /contact.xml - на xml. Детальніше з цим можна ознайомитися у розділі “Маршрутизація (частина 2)”, підрозділі “Маршрутизація вищого рівня у прикладах”.

Для створення посилань, що містять у собі параметр формату, до хешу параметра додайте ключ _format:

Twig

<a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}">
    PDF Version
</a>

PHP

<a href="<?php echo $view['router']->generate('article_show', array(
    'id' => 123,
    '_format' => 'pdf',
)) ?>">
    PDF Version
</a>

Висновки

Шаблонізатор у Symfony - це потужний інструмент, що стає у нагоді, коли необхідно згенерувати презентаційний контент у форматі HTML, XML чи, зрештою, в будь-якому іншому форматі. І хоча типовим способом генерації контенту в контролері є саме шаблони, використовувати їх геть не обов’язково. Об’єкт Response як вихідні дані з контролера створюється як із використанням шаблонів, так і без них:

// creates a Response object whose content is the rendered template
$response = $this->render('article/index.html.twig');

// creates a Response object whose content is simple text
$response = new Response('response content');

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

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

Читайте більше у Довіднику:

      - How to Use PHP instead of Twig for Templates

      - How to Customize Error Pages

      - How to Write a custom Twig Extension

Поділитися