2019年3月24日 星期日

Symfony4.2入門教學【第五篇】模板(Templates)

如同上一篇文章的說明,控制器負責處理對Symfony應用的每一個請求,產生回應的內容通常是呈現模板(rendering a template)。

實際上,控制器將大部分繁重的工作委託給第三方,以便測試或重複利用程式碼。當控制器需要產生HTML、CSS或其他內容,這個工作會交給模板引擎(template engine)。

在這篇文章裡,您將學習如何編寫可用於將內容回傳給用戶,填寫電子郵件正文等的強大模板。你將會學到捷徑、巧妙地延伸模板的方法和如何重複利用模板程式碼。


模板

模板是一種能生成任何基本文字格式的文字檔(HTML, CSS, CSV, LaTex...),你應該很熟悉PHP模板 - 能讓PHP解讀的一種摻雜文字和PHP程式碼的文字檔。
<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to Symfony!</title>
    </head>
    <body>
        <h1><?= $page_title ?></h1>

        <ul id="navigation">
            <?php foreach ($navigation as $item): ?>
                <li>
                    <a href="<?= $item->getHref() ?>">
                        <?= $item->getCaption() ?>
                    </a>
                </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>
但Symfony提供更強大的模板語言稱Twig,Twig能使你寫出更簡潔易讀的模板,對web設計人員更友善,並在某些方面,比PHP模板更加強大:
<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to Symfony!</title>
    </head>
    <body>
        <h1>{{ page_title }}</h1>

        <ul id="navigation">
            {% for item in navigation %}
                <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
            {% endfor %}
        </ul>
    </body>
</html>
Twig訂了三種特殊語句:
{{ ... }}
"Say something": 在模板上印出變數或表示式的結果。

{% ... %}
"Does something": 一個能控制模板邏輯的標籤(tag),可以用來執行陳述(statement),像是迴圈(for-loop)。

{# ... #)
"Comment something": 功能等同與PHP的句法/* comment */,用來新增多行或單行的註解,這些內容不會顯示在呈現的頁面上。

Twig也包含過濾器(filters),能在呈現頁面之前改變內容,下面的例子是讓title這個變數在呈現之前全部變成大寫:
{{ title|upper }}
預設情況下,這長串的tagsfiltersfunctions都可以在Twig使用,透過Twig Extension,你也可以克制自己的過濾器(filters)、函式(functions)等等,執行下面的指令看看有哪些:
php bin/console debug:twig
Twig程式碼看起來和PHP程式很像,只有一些微妙但美好的差異,下面的例子是用標準的標籤for和函式cycle()印出奇數、偶數交替使用不同class的10個div標籤:

在本文中,Twig和PHP模板範例都會有。

選擇Twig的原因?
Twig模板很簡單,並忽略不處理PHP標記,Twig模板系統設計的目的是用於表示式,而非程序邏輯。長期接觸後會慢慢愛上它並從中受益,當然也會都到各地web設計人員的青睞。

Twig還能達成PHP不能辦到的事情,像是控制空白、沙盒(sandboxing)、HTML自動轉譯(automatic HTML escaping)、手動內容輸出轉譯(manual contextual output escaping),和僅用於模板的自定義函數和過濾器。Twig包含許多功能,使得撰寫模板更加容易、簡潔,請看下面的範例,結合了環圈和邏輯陳述句if
<ul>
    {% for user in users if user.active %}
        <li>{{ user.username }}</li>
    {% else %}
        <li>No users found</li>
    {% endfor %}
</ul>

Twig模板快取
由於每個模板編譯成一個本地的PHP類(class)並進行緩存,使得Twig運行速度很快,而且這些事情都會自動執行,你什麼都不用做也不用你操心。開發時,Twig會聰明地在你做任何改變之後重新編譯,這代表Twig在正式環境中很快,但開發同時也很方便使用。


模板繼承與佈局

專案的模板通盛會共享一些共通元素,像是header、footer、sidebar等等,在Symfony裡比虛幻一個方式思考:模板可以讓另一個模板裝飾(decorate),這個概念相當於PHP類中的繼承,模板繼承允許你建立一個基本的佈局模板,其中包含定義為區塊(block)的常見元素(可以當作是有一些基本方法的PHP類),子模板能夠延伸基礎佈局,並覆寫父模板的任何區塊(可以想成PHP的子類也會覆蓋父類的某些方法)。

首先,建立一個基處佈局(base layout)的檔案:
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Test Application{% endblock %}</title>
    </head>
    <body>
        <div id="sidebar">
            {% block sidebar %}
                <ul>
                    <li><a href="/">Home</a></li>
                    <li><a href="/blog">Blog</a></li>
                </ul>
            {% endblock %}
        </div>

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

雖然關於模板繼承的討論將以Twig的形式進行,但Twig和PHP模板之間的理念是相同的。

這個模板定義基礎的HTML架構(skeleton)是一個兩大欄的頁面,在這個例子裡,定義了三個{% block %}區塊(titlesidebarbody),這個模板也可以直接被渲染,這麼做的話,titlesidebarbody三個區塊會在模板裡保持預設值。

子模板(child template)看起來可能會像是:
{# templates/blog/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}My cool blog posts{% endblock %}

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

父模板(parent template)會存在目錄template/裡,所以路徑為base.thml.twig,模板命名慣例在『模板命名與路徑』(原文)有更完整的說明。

模板繼承的關鍵字為這個{% extends %}標籤,這會告訴模板引擎先解析這個設定好佈局和定義一些區塊的基礎模板,子模板會根據指向titlebody的區塊取代原本父模板的內容進行呈現,根據blog_entries的值,輸出結果如下所示:
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>My cool blog posts</title>
    </head>
    <body>
        <div id="sidebar">
            <ul>
                <li><a href="/">Home</a></li>
                <li><a href="/blog">Blog</a></li>
            </ul>
        </div>

        <div id="content">
            <h2>My first post</h2>
            <p>The body of the first post.</p>

            <h2>Another post</h2>
            <p>The body of the second post.</p>
        </div>
    </body>
</html>
注意一下,既然子模板沒有定義sidebar這個區塊,內容就會沿用父模板的設定,在{% block %}裡的內容總是當作預設值來使用。

想要的話,你可以使用多層的模板繼承,請看『如何用繼承模板管理Twig模板』(原文)了解更多資訊。

使用模板繼承時,有些小技巧請銘記在心:
  • 如果在模板中使用{% extends %},這個標籤必須放在模板的最前面。
  • 在基礎模板有越多{% block %}的標籤越好,記住!子模板不需要定義父模板內所有的區塊,所以你可以根據需求盡可能地在基礎模板中建立越多的區塊,並幫每個區塊定義合理的預設值,基礎模板有越多區塊,能使你的佈局更加彈性。
  • 假設你發現在多個模板內有重複的內容,這可能代表你需要把這部分的內容移到父模板的{% block %}內當作一個區塊。在某些情況下,有一個更好的解法是把這些內容放到新的模板,並引用(include)(請看Including other Templates)。
  • 如果你需要取得父模板區塊的內容,你可以用函式{{ parent() }},你如果想要增加父模板區塊的內容而不是整個覆寫它的話,這會是一個很好用的方法。
{% block sidebar %}
    <h3>Table of Contents</h3>

    {# ... #}

    {{ parent() }}
{% endblock %}



模板命名與路徑

預設情況下,模板會放在兩個不同的路徑:
templates/
應用專案中的目錄views有應用程序範圍的基本模板(例如你應用中的布局、應用程序包的模板)。

vendor/path/to/CoolBundle/Resources/views/
每一個第三方程序在目錄Resources/views/(和子目錄)有它的模板,當你決定分享你的程序包時,你應該將模板放到程序包而非templates/這個目錄。

大部分你使用的模板會放在目錄templates/裡,你的使用路徑是依據這個目錄的相對路徑,舉例來說,如果要render/extendtemplates/base.html.twig,你會用路徑base.html.twig;如果要render/extend檔案templates/blog/index.html.twig,你會用路徑blog/index.html.twig

引用程序包裡的模板
假設你需要引用程序包裡的模板,Symfony使用Twig命名空間(namespace)的語句(@BundleName/directory/filename.html.twig),這能允許使用多種模板,而每一個都在它特定的路徑:
  • @AcmeBlog/Blog/index.html.twig: 這個語句用來指定指定特定頁面的模板,這三部分的字串分別用斜線(/)分隔開來,代表:
    • Blog: 這是程序包的名稱去掉後綴的Bundle,這個模板在AcmdBlogBundle(例如:src/Acme/BlogBundle)
    • index.html.twig: 目錄,表示模板位於Resources/views/的子目錄中。
    • : 檔案名稱,檔案的名稱為index.html.twig
  • @AcmeBlog/layout.html.twig: 這個句法引用屬於AcmeBlogBundle的基本模板,因為少了中間"目錄"的部分(如Blog),這個模板位於AcmeBlogBundle裡的Resources/views/layout.html.twig

使用這個命名空間的語句,而非真正的檔案路徑,來覆寫任何程序包內的模板。

模板後綴(Template Suffix)
每一個模板名稱都有兩個擴展(extension),指定該模板的格式(format)引擎(engine)

檔案名稱格式引擎
blog/index.html.twigHTMLTwig
blog/index.html.phpHTMLPHP
blog/index.css.twigCSSTwig

預設情況下,任何Symfony模板可以用Twig或PHP寫,最後一部分的擴展(e.g. .twig.php)指定這兩種引擎該用哪一個,第一部分的擴展(e.g. ..html.css等等)是模板最終產生的格式,這跟引擎不一樣,它決定了Symfony要如何解讀這個模板,這是有一種策略,用來處理將相同資源呈現成HTML()、XML或任何其他格式,更多的資訊可以讀『如何運用不同輸出格式的模板』(原文)這個章節。


標籤和小幫手

你已經了解基本的模板,如何命名、如何繼承模板,你已經度過最困難的部分,在這個章節,你將會學習很多能執行最常見的模板任務的工具,像是引用其他模板、連結到其他頁面或引用影像。

Symfony帶有幾個專門的Twig標籤和函式,可以簡化模板設計師的工作。在PHP中,模板系統提供一個可擴展的輔助系統,可以在模板的內容(context)中提供有用的功能。

你已經看過幾個內建的Twig標籤,像是{% block %}{% extends %},接著你會學到更多。

引用其他模板
你將會常常在一些頁面上想要引用相同的模板或部分的程式,例如,一個應用中的"最新文章",這個顯示文章用的模板程式,可能可以用於顯示文章詳細訊息的頁面上、顯示最受歡迎文章的頁面上,或在最新文章列表中。

當你需要重複使用一大堆PHP程式,一般來說,你會將這些程式碼搬到一個新的PHP類(class)或函式,這個同樣適用於模板,將重複使用的模板程式碼建立成自己獨立的模板,可以讓任何其他模板引用。首先,建立一個要重複使用的模板:
{# templates/article/article_details.html.twig #}
<h2>{{ article.title }}</h2>
<h3 class="byline">by {{ article.authorName }}</h3>

<p>
    {{ article.body }}
</p>
請用函式{{ include() }}來達成讓其他模板引用這個模板。
{# templates/article/list.html.twig #}
{% extends 'layout.html.twig' %}

{% block body %}
    <h1>Recent Articles<h1>

    {% for article in articles %}
        {{ include('article/article_details.html.twig', { 'article': article }) }}
    {% endfor %}
{% endblock %}
請注意,模板名稱遵循相同的典型約定,article_details.html.twig這個模板使用我們傳過去的變數article,在這個情況下,你可以不需要這樣做,因為所有list.html.twig中可用的變數,一樣可以在article_details.html.twig中使用(除非你將with_context設定false)。

句法{'article': article}是一個標準的Twig語句(i.e. 一個帶有命名鍵的數組),如果你需要傳多個元數,會像是{'foo': foo, 'bar': bar}

連結其他頁面
在應用哩,建立一個連結到其他頁面對模板來說,是一個相當常見的功能,可以利用Twig函式patyh(或PHP裡的小工具router)來參照路由設定產生URL,而不是直接將URL寫死在模板裡。之後假設你需要修改特定頁面的URL,你只要更改路由的配置,模板就會自動生成新的URL。

首先,透過以下的路由設定要連結的"welcome"頁面:
// 省略程式碼
要連接到這個頁面,請使用Twig函式path()來使用這個路由:
<a href="{{ path('welcome') }}">Home</a>
如同預期,這會產生URL/。現在,來看看這個更複雜的路由:
// 省略程式碼
在這個例子裡,你需要指定路由名稱(article_show)和參數{slug}的值。使用這個路由,再次訪問前一個章節用到的recent_list.html.twig這個模板,就可以正確地連結到文章:
{# templates/article/recent_list.html.twig #}
{% for article in articles %}
    <a href="{{ path('article_show', {'slug': article.slug}) }}">
        {{ article.title }}
    </a>
{% endfor %}
你也可以用Twig函式url()來產生絕對路徑。
<a href="{{ url('welcome') }}">Home</a>

連結到Assets
模板也經常需要影像、JavaScript、樣式表(stylesheets)和其他assets(e.g. /images/logo.png),但Symfony提供一個更動態的選項:Twig函式asset()

請安裝套件asset來使用這個函式:
composer require symfony/asset
現在可以使用函式asset()
<img src="{{ asset('images/logo.png') }}" alt="Symfony!" />

<link href="{{ asset('css/blog.css') }}" rel="stylesheet" />
函式asset()的主要功能是讓你的應用更方便移植,如果你的專案在網頁伺服器的跟目錄(e.g. http://example.com),所以給予的路徑會像是/images/logo.png。但如果你的專案放在子目錄內(e.g. http://example.com/my_app),所有的assets路徑就會多了一個子目錄(e.g. /my_app/images/logo.png),函式asset()能解決這個問題,它可以藉由確認你的應用路徑,並相應的產生正確的路徑。

透過versionversion_formatjson_manifest_path的設定,函式assets提供多元的緩存破壞(cache busting)技術

如果需要產生assets的絕對路徑,可以使用Twig函式absolute_url(),像是:
<img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!" />


在Twig引用Stylesheets和JavaScript

沒有任何網站是完全不需要用到Javscript或stylesheets(樣式表),藉由Symfony模板繼承的特點,這些assets可以很優雅地進行管理,

這個章節將教你如何在Symfony中引用樣式表和Javascript資源,如果你對編譯或創建這個assets有興趣的話,請查看『Webpack Encore documentation』,這個工具可以將webpack和其他現代的Javascript工具無縫集成到Symfony應用程序中。

我們新增兩個區塊來管理assets,一個叫做stylesheets,放在標籤head裡;另一個叫javascripts放在鄰近的標籤body裡,這些區塊將包含專案內所有樣式表和JavaScript。
{# templates/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>
這看起來跟一般的HTML很像,只是多了{% block %},你需要在子模板內引用其他的樣式表或javascript時是很方便。例如,假設有個聯絡頁面,且只有這個頁面需要引用一個樣式表contact.css,因此在連絡頁面的模板內,會像是:
{# templates/contact/contact.html.twig #}
{% extends 'base.html.twig' %}

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

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

{# ... #}
在子模板哩,你可以覆寫stylesheets區塊,並放了你新的樣式表標籤到區塊內,如果你想要新增父模板區塊的內容(而不是真的取代掉它),你可以用Twig函式parent()來引用基礎模板的stylesheets區塊內所有內容。

你也可以引用在你的程序包Resources/public/內的assets,你需要執行指令php bin/console assets:install target [--symlink],作用是複製(符號鏈接 symlink)檔案到正確的路徑(目標是應用中預設的目錄"public/")。
<link href="{{ asset('bundles/acmedemo/css/contact.css') }}" rel="stylesheet" />
最後結果會是一個包含main.jsmain.csscontact.css兩個stylesheets的頁面。


引用請求、用戶或Session

Symfony在Twig提供一個全域變數app,可以用來取得現在的使用者、請求等等資訊。

請看『How to Access the User, Request, Session & more in Twig via the app Variable』了解更多。


輸出轉義(Output Escaping)

在呈現任何內容時,Twig會自動轉譯輸出,以防你受跨站腳本(Cross Site Scription, XSS)攻擊。

假設description等於I <3 product="" span="" this="">:

<!-- output escaping is on automatically -->
{{ description }} <!-- I &lt;3 this product -->

<!-- disable output escaping with the raw filter -->
{{ description|raw }} <!-- I <3 this product -->
PHP模板不會自動轉義內容。

更多細節請看『How to Escape Output in Templates』。


結語

模板系統只是Symfony眾多工具之一,且它的功能很簡單:讓我們可以給予動態和複雜的HTML輸出,可以讓我們在最後回傳給使用者、寄出email等等。


繼續前進!

沉浸於Symfony其他部分之前,先看看『Symfony4.2入門教學【第六篇】設定(Configuration)』(原文)。


文章原文:
https://symfony.com/doc/4.2/configuration.html

系列文章:

沒有留言:

張貼留言