建立和使用模板
<img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!" />
就像你知道的,控制器(controller)負責處理每一個進入symfony程式的請求。實際上,控制器把大部分的繁重工作都委託給了其他地方,以便程式碼能夠被測試和重複使用。當一個controller需要產生HTML、CSS或其他內容時,它把這些工作給了一個模板引擎。在本章中,你將學習如何編寫功能強大的模板,用於把內容回傳給使用者、填充email,等等。你也將學會快捷方法,用巧妙的方法擴充模板,以及如何重複使用模板程式碼。
如何渲染模板請查看「框架開始」之控制器。
範本 ¶
範本就是產生任何以文字格式(html,xml,csv,LaTex…)為基礎的文字檔案。我們最熟悉的模板類型就是php 模板-包含文字和php程式碼的被PHP引擎解析的文字檔:
<!DOCTYPE html> <html> <head> <title>Welcome to Symfony!</title> </head> <body> <h1><?php echo $page_title ?></h1> <ul id="navigation"> <?php foreach ($navigation as $item); ?> <li> <a href="<?php echo $item->getHref(); ?>"> <?php echo $item->getCaption(); ?> </a> </li> <?php endforeach; ?> </ul> </body> </html>
但是,symfony 框架中有一個更強大的模板語言叫作Twig。 Twig可令你寫出簡潔易讀且對設計師友善的模板,在幾個方面比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定義了三個特殊的語法:
{{ ... }}
- 「說些什麼」:輸出一個變數值或一個表達式的結果到模板。
{% ... %}
- 「做些什麼」:控制模板邏輯的*tag(標籤)*,用於執行聲明,如for循環語句等。
{# ... #}
- 「進行註解」:它相當於php的
/* comment */
語法。它用於註解單行和多行。註釋的內容不作為頁面輸出。
twig也包含filters,它可以在模板渲染之前改變輸出內容。下例讓title變數在被渲染之前全部大寫:
{{ title|upper }}
Twig內建了大量的標籤(tags)和變數調節器(filters),預設就可以使用。你甚至可以利用Twig擴充來加入你自己的自訂 調節器和函數(甚至更多)。
註冊一個Twig擴充功能非常容易,建立一個新服務並打上Twig.extension
標籤。
Twig程式碼很像PHP程式碼,兩者有微妙的差別。下例使用了一個標準的for
標籤和cycle
函數來輸出10個div 標籤,用odd
、even
css類別交替顯示。
{% for i in 0..10 %} <div class="{{ cycle(['odd', 'even'], i) }}"> <!-- some HTML here --> </div>{% endfor %}
本章的範本例程,將同時使用twig和php來展示。
如果你不使用Twig或停用它,你需要使用kernel.execption
事件來實作一個你自己的例外處理。
Twig模板快取 ¶
twig是很快的,每個Twig模板被編譯成原生PHP類別並且快取起來。編譯過的類別被保存在var/cache/{environment}/twig
目錄下(其中{environment}
是環境,如dev
和 prod
),並在某些情況下可以同時調試,非常有用。更多關於環境的細節請參考:環境
當debug
模式可用時(dev
環境),如果一個twig模板改變,將會自動重新編譯。這意味著你可以在開發過程中隨意修改模板,而不必擔心要去清除快取了。
當debug
模式被關閉時(prod
環境),你必須手動的清除Twig快取目錄,以便重新產生Twig範本。記得在部署程式時一定要做到這一點。
模板繼承和佈局 ¶
大多數的時候,模板在專案中都有通用的元素,例如header,footer,sidebar等等。在Symfony中,我們將採用不同的思考角度來處理這個問題。一個模板可以被另外的模板裝飾。這個的工作原理跟PHP類別非常像,模板繼承讓你可以建立一個基礎「layout」模板,它包含你的網站所有通用元素,並被定義成blocks(如同一個「包含基礎方法的PHP基底類」)。一個子範本可以繼承layout基礎範本並覆寫它的任何一個block(就像「PHP子類別覆寫父類別中的特定方法」)。
首先建立一個layout基礎檔:
Twig:{# app/Resources/views/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>
PHP:<!-- app/Resources/views/base.html.php --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title><?php $view['slots']->output('title', 'Test Application') ?></title> </head> <body> <div id="sidebar"> <?php if ($view['slots']->has('sidebar')): ?> <?php $view['slots']->output('sidebar') ?> <?php else: ?> <ul> <li><a href="/">Home</a></li> <li><a href="/blog">Blog</a></li> </ul> <?php endif ?> </div> <div id="content"> <?php $view['slots']->output('body') ?> </div> </body> </html>
#雖然討論的是關於Twig的模板繼承,但在思維方式上twig和php模板之間是相同的。
該範本定義了一個簡單的兩列式html頁面。在本例中,三個地方 {% block %}
區域被定義了(即title
,sidebar
和body
)。每個block都可以被繼承它的子模板覆寫,或者保留現在這種預設實作。此模板也能直接渲染(輸出)。只不過此時只是顯示基礎模板所定義的內容,title
, sidebar
和 body
都會保持預設值。
一個子模板看起來是這樣的:
Twig:{# app/Resources/views/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 %}
php:<!-- app/Resources/views/blog/index.html.php --><?php $view->extend('base.html.php') ?> <?php $view['slots']->set('title', 'My cool blog posts') ?> <?php $view['slots']->start('body') ?> <?php foreach ($blog_entries as $entry): ?> <h2><?php echo $entry->getTitle() ?></h2> <p><?php echo $entry->getBody() ?></p> <?php endforeach ?><?php $view['slots']->stop() ?>
父模板以一個特殊的字串語法來表示base.html.twig
,這個路徑是相對於整個專案的app/Resources/views
目錄而言的。你也可以使用邏輯名稱相同的::base.html.twig
。參考下文的模板名稱和位置。
模板繼承的關鍵字是 {% extends %}
標籤。該標籤告訴模板引擎首先評估父模板,它設定了佈局並定義了若干blocks。然後子模板被渲染,上例中父模板中定義的title
和body
兩個blocks將會被子模板中的同名區塊內容所取代。根據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,來自父模板的內容將被顯示出來。父模板中的{% block %}
標籤內的內容,總是作為預設值來用。
你可以進行任意多個層級的範本繼承。 Symfony專案中一般使用「三級繼承」模式,來組織範本和頁面,參考如何使用繼承來組織你的Twig範本。
使用模板繼承時,需要注意:
如果在模板中使用
{% extends %}
,它必須是模板中的第一個標籤。你的基礎(版面)範本中的
{% block %}
標籤越多越好,記得,子範本不必定義父範本中的所有block 。基礎模板中的block定義得越多,你的佈局就越靈活。如果你發現在多個範本中有重複的內容,這可能表示你需要為該內容在父範本中定義一個
{% block %}
了。在某些情況下,更好的解決方案可能是把這些內容放到一個新模板中,然後在該模板中include
它。 (看下文的:包容其他模板)如果你需要從父模板取得一個block的內容,可以使用
{{ parent() }}
函數。如果你只是想在父級區塊上新增內容,而不是完全覆蓋它,這很有用:
{% block sidebar %} <h3>Table of Contents</h3> {# ... #} {{ parent() }}{% endblock %}
模板的命名和儲存位置 ¶
默認情況下,模板可以存放在兩個不同的位置:
app/Resources/views
程式級的views目錄可以存放整個程式的基礎模板(程式佈局和bundle模板),以及那些「用於覆寫第三方bundle的模板」的模板(如何覆寫第三方bundle的模板)。
path/to/bundle/Resources/views
每個第三方bundle的範本都會存放在它自己的Resources/views/
目錄(或子目錄)下。當你打算分享你的bundle時,你應該把它放在bundle中,而不是app/
目錄。
更多時候你要用的模板是在app/Resources/views/
目錄下。你需要的模板路徑是相對於這個目錄的。例如,去渲染/繼承app/Resources/views/base.html.twig
,你需要使用base.html.twig
的路徑,而要去渲染app/ Resources/views/blog/index.html.twig
時,你需要使用blog/index.html.twig
路徑。
在Bundle中引入模板 ¶
Symfony使用bundle:directory:filename
字串語法表示模板。這可以表示許多不同類型的模板,每種都存放在一個特定路徑下:
AcmeBlogBundle:Blog:index.html.twig
AcmeBlogBundle:Blog:index.html.twig
# 用來指定一個特定頁面的模板。字串分為三個部分,每個部分由冒號(
- )隔開,意義如下:
)範本位於AcmeBlogBundle,例如AcmeBlogBundle
:(
bundle src/Acme/BlogBundle- ;
的Blog
:(
目錄)表示模板位於
Resourcs/views Blog - 子目錄中;
)檔案的實際名稱為index.html.twig
:(
檔案名稱 index.html.twig
假設AcmeBlogBundle位於
src/Acme/BlogBundle
AcmeBlogBundle::layout.html.twig
這個語法指向了AcmeBlogBundle的父模板。沒有了中間的「目錄」部分(如blog
),模板應該位於AcmeBlogBundle的Resources/views/layout.html.twig
。是的,中間的兩個冒號意味著「控制器」子目錄部分被忽略了。
在如何覆寫第三方bundle的模板一文中,你將會了解到位於AcmeBlogBundle的模板,是如何被app/Resources/AcmeBlogBundle/views/
目錄下的同名模板所覆寫的,這種方式給了我們一個有力的途徑來覆蓋bundle作者提供的bundle模板。
模版的命名語法可能看起來比較熟悉-它類似於控制器命名模式中提到的約定。
模版字尾後綴¶
#每個模版都有兩個副檔名,用來指定格式(format) 和模版引擎(engine)。
檔案名稱 | Format | 引擎 |
---|---|---|
blog/index.html.twig | #HTML | Twig |
blog/index.html.php | HTML | PHP |
blog/index.css .twig | CSS | Twig |
預設情況下,Symfony的任何模板都可以寫成Twig或PHP引擎的,它由後綴(.twig
或 .php
)來決定使用哪個引擎。其中後綴的前一部分(.html
,.css
)表示最終產生的格式。不像引擎,它是決定symfony如何解析模板,這是一個很簡單的使用策略,你可以使用HTML(index.html.twig
),XML(index.xml.twig
)或任何其他格式作為渲染的資源。更多的細節,請閱讀模版格式部分。
可用的「engines」部分是可配置的,甚至可以增加一個新的引擎。請查看 如何設定和使用模板服務以了解更多細節。
Tags和Helps ¶
你已經了解了模板基礎,它們是如何命名以及如何使用模板繼承等基礎知識。最難的部分已經過去。接下來,我們將了解大量的可用工具來幫助我們完成常見的模板任務,例如包容其他模板,連結到一個頁面或引入圖片。
Symfony框架中內建了幾個特殊的Twig標籤和功能函數,來幫助模板設計者簡化工作。在PHP中,模板系統提供了一個可擴展的helper 系統用於在模板上下文中提供有用的功能。
你已經看到了一些內建的Twig標籤,像是({% block %}
& {% extends %}
)等,還有PHP helper $view['slots']
。現在,你將會學到更多。
引入其他模版 ¶
你經常需要在多個不同的頁面中包含同一個範本或程式碼片段。例如在一個「新聞文章」程式中,用來顯示文章的範本程式碼可能會被用到正文頁,或用到一個顯示「人氣文章」的頁面,甚至一個「最新文章」的清單頁等。
當你需要重複使用一些PHP程式碼時,你通常都是把這些程式碼放到一個PHP類別或函數中。同樣在模板中你也可以這麼做。透過把可重複使用的程式碼放到一個它自己的模板中,然後從其他模板中包容這個模板。首先,建立一個可重複使用模板如下:
Twig:{# app/Resources/views/article/article_details.html.twig #}<h2>{{ article.title }}</h2> <h3 class="byline">by {{ article.authorName }}</h3> <p> {{ article.body }}</p>
php:<!-- app/Resources/views/article/article_details.html.php --> <h2><?php echo $article->getTitle() ?></h2> <h3 class="byline">by <?php echo $article->getAuthorName() ?></h3> <p> <?php echo $article->getBody() ?></p>
在其他任何模板中引入這個模板很簡單:
Twig:{# app/Resources/views/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 %}
php:<!-- app/Resources/article/list.html.php --><?php $view->extend('layout.html.php') ?> <?php $view['slots']- >start('body') ?> <h1>Recent Articles</h1> <?php foreach ($articles as $article): ?> <?php echo $view->render( 'Article/article_details.html.php', array('article' => $article) ) ?> <?php endforeach ?><?php $view['slots']->stop() ?>
這個模板被包容時,使用了{ { include() }}
標籤。請注意,模板命名要遵循相同的典型約定。在article_details.html.twig
模板中使用article
變量,這是我們傳入模板的。本例中,你也可以完全不這樣做,因為在list.html.twig
模板中可用的所有變數也都可以在article_details.html.twig
中使用(除非你設定with_context為false)。
{'article':article}
語法是標準Twig雜湊映射(hash maps)的寫法(即是一個鍵值對數組)。如果你需要傳遞多個元素,可以寫成{'foo': foo, 'bar': bar}
。
連結到頁面 ¶
在你的程式中建立一個連結到其他頁面,對於模板來說是再普通不過的事情了。使用path
Twig函數(或php中的router
helper)基於路由配置來產生URLs而非在模板中寫死URLs。以後,如果你想修改一個特定頁面的URL,你只需要改變路由配置即可;模板會自動產生新的URL。
例如我們打算連結到「_welcome」頁面,首先定義其路由配置:
Annotations:// src/AppBundle/Controller/WelcomeController.php // ...use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class WelcomeController extends Controller{ /** * @Route("/", name="_welcome") */ public function indexAction() { // ... }}
YAML:# app/config/routing.yml_welcome: path: / defaults: { _controller: AppBundle:Welcome:index }
XAML:<!-- app/config/routing.yml --><?xml version="1.0" encoding="UTF-8" ?><routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> <route id="_welcome" path="/"> <default key="_controller">AppBundle:Welcome:index</default> </route></routes>
PHP:// app/config/routing.phpuse Symfony\Component\Routing\Route;use Symfony\Component\Routing\RouteCollection; $collection = new RouteCollection();$collection->add('_welcome', new Route('/', array( '_controller' => 'AppBundle:Welcome:index',))); return $collection;
要鏈到頁面,只需使用Twig的path
#函數來指定這個路由即可。
Twig:<a href="{{ path('_welcome') }}">Home</a>
php:<a href="<?php echo $view['router']->path('_welcome') ?>">Home</a>
如如預期的那樣,它產生了URL /
。現在,處理一個更複雜的路由:
Annotations:// src/AppBundle/Controller/ArticleController.php // ...use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class ArticleController extends Controller{ /** * @Route("/article/{slug}", name="article_show") */ public function showAction($slug) { // ... }}
TAML:# app/config/routing.ymlarticle_show: path: /article/{slug} defaults: { _controller: AppBundle:Article:show }
PHP:// app/config/routing.phpuse Symfony\Component\Routing\Route;use Symfony\Component\Routing\RouteCollection; $collection = new RouteCollection();$collection->add('article_show', new Route('/article/{slug}', array( '_controller' => 'AppBundle:Article:show',))); return $collection;
XAML:<!-- app/config/routing.xml --><?xml version="1.0" encodin g="UTF-8" ?><routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> <route id="article_show" path="/article/{slug}"> <default key="_controller">AppBundle:Article:show</default> </route></routes>
這種情況下,你需要指定路由名稱()以及一個。使用這個路由重新定義前文提到的模板,並正確鏈入文章。
Twig:{# app/Resources/views/article/recent_list.html.twig #}{% for article in articles %} <a href="{{ path('article_show', {'slug': article.slug}) }}"> {{ article.title }} </a>{% endfor %}
php:<!-- app/Resources/views/Article/recent_list.html.php --><?php foreach ($articles in $article): ?> <a href="<?php echo $view['router']->path('article_show', array( 'slug' => $article->getSlug(), )) ?>"> <?php echo $article->getTitle() ?> </a><?php endforeach ?>
你可以透過Twig的url
函數來產生絕對路徑:
Twig:<a href="{{ url('_welcome') }}">Home</a> php:<a href="<?php echo $view['router']->url( '_welcome', array()) ?>">Home</a>
連結到Assets ¶
範本通常也需要一些圖片,Javascript,樣式檔案和其他web資產。當然你可以寫死它們的路徑。如 /images/logo.png
。但Symfony透過Twig函數 asset()
,提供了一個更動態的選擇,。
Twig:<img src="{{ asset('images/logo.png') }}" alt="Symfony!" /> <link href="{{ asset('css/blog.css') }}" rel="stylesheet" />
php:<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" alt="Symfony!" /> <link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet" />
asset
函數的主要目的,就是讓你的程式更portable(可移動)。如果你的程式在主機根目錄下(如http://example.com
),產生的路徑應該是 /images/logo.png
。但是如果你的程式位於一個子目錄中(如http://example.com/my_app
),asset路徑在生成時應該要帶有子目錄(如/my_app/images/logo .png
) 。 asset
函數負責打點這些,它根據你的程式 “是如何使用的” 而產生相應的正確路徑。
另外,如果你使用asset
函數,symfony可以自動追加一個query string(查詢字串)到你的資產,以保證被更新的靜態資源不會在部署時被快取.例如,/images/logo.png
可能看起來是 /images/logo.png?v2
。參考version配置一文以了解更多。
如果你需要assets資源的絕對URL,可以使用absolute_url()
Twig函數:
<img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!" />
在Twig中包容樣式表和Javascript ¶
每個網站中都不能完全沒有樣式表和javascript檔。在Symfony中,這些內容可以利用模板繼承來優雅地處理。
本節教你包容stylesheet和javaScript資源時的背後思想。 Symfony支援另一個類別函式庫叫Assetic, 它允許你在遵循這一想法時,對這些資源做更多有趣事情。參考如何使用Assetic 進行資產管理以了解更多細節。
首先在你的基礎佈局模板中加入兩個blocks來保存你的資源,一個叫stylesheets
,放在head
標籤裡,另一個叫javascript
,放在body
結束標籤上面一行。這些blocks將包含你整個網站所需的全部stylesheets和javascripts。
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>
這也太簡單了吧!但如果你想從子模板中包容一個額外的stylesheet或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
block並把你新的樣式表標籤放到該區塊裡。當然,由於你只是想把它加到父塊兒的內容中(而不是真的替代它們),所以你需要先用parent()
函數來獲取基礎模板中的所有stylesheets
區塊中的內容。
你也可以包容位於你bundle的Resources/public
資料夾下的assets資源。你需要執行php bin/console assets:install target [–symlink]
指令,它會把檔案移到(或symlink到)正確的位置(預設目標位置是「web」資料夾)。
<link href="{{ asset('bundles/acmedemo/css/contact.css') }}" rel="stylesheet" />
最終結果是,頁面中同時包容了main.css
和contact.css
兩個樣式表。
引用Request,User或Session物件 ¶
Symfony在Twig中給了你一個全域的app
變量,可以用來存取當前用戶、請求以及更多物件。
參考如何在Twig中透過app變數存取到User, Request, Session和更多物件以了解細節。
輸出轉義 ¶
在渲染任意內容時,Twig自動進行“輸出轉義(output escaping)”,為的是保護你免受Cross Site Scripting (XSS)跨站攻擊。
假設description
是I <3 this product
:
<!-- output escaping is on automatically -->{{ description }} <!-- I <3 this product --> <!-- disable output escaping with the raw filter -->{{ description|raw }} <!-- I <3 this product -->
PHP範本不會自動轉義內容。
更多細節,參考如何對模板輸出進行轉義。
總結 ¶
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推薦使用Twig模板,因為它更簡潔,高效,能更好的處理繼承等。
整體而言,在你處理範本問題的時候,它是一個強大的工具。在某些情況下,你可能不需要渲染模板,在symfony中,這絕對是沒有問題的。