无论你是需要从一个控制器渲染 HTML 还是生成一封 email 的内容,模板都是从你的应用中组织并渲染 HTML 的最佳方式。在 Symfony 中模板的创建工作是由 Twig 完成:一个灵活、快速并且安全的模板引擎。
Twig 模板语言
Twig 模板语言允许你撰写简明、高可读性的模板,这种模板对于 web 设计者更友好,在某种程度上,比 PHP 模板更加强大。看一下后面这个 Twig 模板实例。即使你第一次看到 Twig,你也很可能明白它的大部分含义:
1 |
|
Twig 语法基于这三个结构:
{{ ... }},用来显示变量的内容或是执行一个表达式的结果;{% ... %},用来运行一些逻辑,比如一个条件语句或是循环语句;{# ... #},用来添加注释到模板中(不像 HTML 注释,这些注释不会被包含进渲染的页面中)。
你不能在一个 Twig 模板中运行 PHP 代码,但是 Twig 提供了一些公共方法来用来在模板中运行一些逻辑。比如,过滤器可以在渲染前修改内容,像 upper 过滤器将内容转为大写字母:
1 | {{ title|upper }} |
Twig 内置了一系列高灵活性的 标签,过滤器 和 方法。在 Symfony 应用中你可以使用这些 由Symfony 定义的 Twig 过滤器和方法,并且你可以创建你自己的 Twig 过滤器和方法。
Twig 在 生产 环境(因为模板被编译成 PHP 代码并且会被自动缓存) 中是非常快速的,而使用 开发 环境(因为当你改变模板内容的时候,它们会被自动编译)更加方便一些。
Twig 配置
Twig 有一些配置选项来定义一些选项,比如用来显示数字和日期的格式化规则,模板的缓存等等。阅读 Twig 配置参考 来了解更多内容。
创建模板
在解释如何创建和渲染一个模板的细节之前,快速观察一下后面示例的完整处理过程。首先,你需要在 template 目录创建一个新文件用来存储模板内容:
1 | {# templates/user/notifications.html.twig #} |
然后创建一个 控制器 用来渲染模板并传入需要的变量:
1 | // src/Controller/UserController.php |
模板命名
Symfony 推荐一下命名规则:
- 使用 蛇形命名法 命名文件名和路径名(例如
blog_posts.twig,admin/default_theme/blog/index.twig等); - 为文件名定义两个扩展名(例如
index.html.twig或blog_posts.xml.twig)其中首个扩展名(html,xml等)会成为模板最后生成文件的格式。
尽管模板通常生成 HTML 内容,他们也可以生成任何其他基于文本的格式。这就是为什么双扩展约定简化了为多种格式创建和渲染模板的方式。
模板位置
模板一般默认保存在 tempalte/ 目录。当一个服务或是控制器渲染 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 试图用一下顺序获取变量中的值:
$foo['bar'](数组和元素);$foo->bar(对象和公共属性);$foo->bar()(对象和公共方法);$foo->getBar()(对象和 getter 方法);$foo->isBar()(对象和 isser 方法);$foo->hasBar()(对象和 hasser 方法);- 如果以上都不符合,使用
null.
这允许改进你的应用程序代码,而无需更改模板代码(你可以从应用程序概念验证的数组变量开始,然后移动到具有方法的对象等)。
链接到页面
不需要手写 URL 链接,使用 path() 函数可以生成基于 路由配置 的 URL。
之后,如果你想修改部分页面的 URL,你所需要做的只是修改路由配置:模板会自动生成新的 URL。
请考虑以下路由配置:
1 | // src/Controller/BlogController.php |
1 | # config/routes.yaml |
1 | <!-- config/routes.xml --> |
1 | // config/routes.php |
使用 path() 这个 Twig 函数链接这些页面,同时传递路由名作为首个参数并且路由参数作为选项的第二个参数:
1 | <a href="{{ path('blog_index') }}">Homepage</a> |
path() 函数生成相关的 URL。如果你需要生成绝对 URL(例如为 Email 或是 RSS 订阅渲染模板),使用 url() 函数,它将使用同 path() 一样的参数(例如 ... )。
链接到 CSS,JavaScript 和图片资产
如果一个模板需要连接到一个静态资产(例如一个图片),Symfony 提供了一个 asset() Twig 函数用来帮助生成 URL。首先,安装 asset 包:
1 | > composer require symfony/asset |
你现在就可以在模板中使用 asset() 函数了:
1 | {# the image lives at "public/images/logo.png" #} |
asset() 函数的主要目的是是你的应用程序更加轻便。如果你的应用程序处于主机的根目录(例如 https://example.com),那么渲染路径就会是 /images/logo.png。但是如果你的应用程序处于一个子目录(比如 https://example.com/my_app),每个资产的路径将会携带子目录渲染(例如 /my_app/images/logo.png)。asset() 函数通过确定应用程序的使用方式,并相应地生成正确的路径来处理此问题。
asset()函数支持各种缓存破坏技术,可以基于 版本,版本格式,json_manifest_path 配置选项。
如果你需要以现代方式对 JavaScript 和 CSS 资产进行打包、版本控制和压缩,请阅读 Symfony 的 Webpack Encore,
如果你需要 URL 的绝对路径,像下面一样使用 absolute_url() Twig 函数:
1 | <img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!"/> |
应用全局变量
Symfony 会创建一个上下文对象,这个对象会以命名为 app 的变量的方式自动注入每一个 Twig 模板。它提供了访问一些应用程序信息的方法:
1 | <p>Username: {{ app.user.username ?? 'Anonymous user' }}</p> |
app 变量(它是 Symfony\Bridge\Twig\AppVariable 的一个实例)可以让你访问这些变量:
app.session
当前用户对象 或者如果用户未被授权就为 null
app.request
Symfony\Component\HttpFoundation\Request 的对象,存储了当前的 请求数据(根据你的应用程序,这可以是一个子请求 或是一个常规请求)。
app.session
Symfony\Component\HttpFoundation\Session\Session 的对象,可以代表当前用户的会话 ,而会话不存在时为 null。
app.flashes
一个存储会话中所有 闪现消息 的数组。你也可以获取指定类型的消息(例如 app.flashes('notice'))。
app.environment
当前配置环境(dev,prod 等)的名称。
app.debug
如果在调试模式 中值为 True,其他情况下为 False。
app.token
一个 Symfony\Component\Security\Core\Authentication\Token\TokenInterface 对象,代表当前安全令牌。
除了 Symfony 注入的全局应用变量外,您还可以 自动将变量注入到所有 Twig 模板。
渲染模板
在控制器中渲染模板
如果你的控制器继承自 AbstractController,使用 render() 辅助方法:
1 | // src/Controller/ProductController.php |
如果你的控制器不是继承自 AbstractController,你需要 在控制器中调用服务 并且使用 twig 服务的 render() 方法。
在服务中渲染模板
注入 twig Symfony 服务到你自己的服务中,并且使用它的 render() 方法。当时使用 服务自动写入 时,你只需要在服务的构造函数中添加一个参数,并使用 Twig\Environment 类作为类型约束1参数:
1 | // src/Service/SomeService.php |
在服务中渲染模板
阅读关于 邮件与 Twig 集成 的文档。
在服务中渲染模板
尽管模板通常在服务器或是服务中进行渲染,你也可以不需要任何变量,直接从路由定义中渲染静态页面。使用特殊的由 Symfony 提供的 Symfony\Bundle\FrameworkBundle\Controller\TemplateController。
1 | # config/routes.yaml |
1 | <!-- config/routes.xml --> |
1 | // config/routes.php |
5.1 版本新特性:
context选项在 Symfony 5.1 中首次被引入
检查一个模板是否存在
模板在应用程序使用一个 Twig 模板加载器 加载,它也提供了一个方法来检测模板的存在状态。首先,获取加载器:
1 | // 在一个继承自 AbstractController 的控制器中 |
然后,传入 Twig 模板的路径到加载器的 exist() 方法中:
1 | if ($loader->exists('theme/layout_responsive.html.twig')) { |
调试模板
Symfony 提供了一些辅助方法来帮助你调试模板中的问题。
检查模板语法
lint:twig 命令检查你的 Twig 模板中是否存在任何的语法错误。在发布你的应用程序到生产环境之前运行它是非常有用的(例如在你的持续集成服务器)。
1 | # 检查所有的应用程序模板 |
检查模板信息
debug:twig 命令列出有关 Twig 的所有可用信息(函数,过滤器,全局变量等)。检查你的 自定义 Twig 扩展 是否正常工作,以及检查 安装包 时添加的 Twig 功能非常有用:
1 | # 列出一般信息 |
模板输出公共方法
Symfony 提供了一个 dump() 函数 作为 PHP 的 var_dump() 函数的更高级的替代品。这个函数在检查任何变量内容时非常有用,并且你也可以在 Twig 模板中使用它。
首先,确保 VarDumper 组件已经安装到了应用程序中:
1 | > composer require symfony/var-dumper |
然后,根据你的需求选择使用 {% dump %} 或是 {{ dump() }} 函数:
1 | {# templates/article/recent_list.html.twig #} |
为了防止泄漏敏感信息, dump() 函数(标签)只在 dev 和 test 配置环境 中可用。如果你想在生产环境中使用它,你将会见到一个 PHP 错误。
复用模板内容
引入模板
如果确认一些 Twig 代码在一些模板中是重复的,你可以把它提取到一个单一的 “模板片” 中并且在另外的模板中引入它。假设以下显示用户信息代码将在几个位置重复:
1 | {# templates/blog/index.html.twig #} |
首先,创建一个名为 blog/_user_profile.html.twig 的新 Twig 模板(_ 前缀是可选的,但它是用于更好地区分完整模板和模板片段的约定)。
然后,从原始的 blog/index.html.twig 模板中删除该内容,并添加以下内容以包含模板片段:
1 | {# templates/blog/index.html.twig #} |
include() Twig 函数将要包含的模板的路径作为参数。包含的模板可以访问包含它的所有模板变量(使用 with_context 选项来控制此变量)。
你还可以将变量传递给包含的模板。这对于重命名变量很有用。假设你的模板将用户信息存储到名为 blog_post.author 的变量中,而不是模板片段期望的 user 变量中。使用以下方法重命名变量:
1 | {# templates/blog/index.html.twig #} |
嵌入控制器
包含模板片段 可用于在多个页面上重用相同的内容。但是,在某些情况下,这种技术不是最佳解决方案。
假设模板片段显示三篇最新的博客文章。为此,它需要进行数据库查询来获取这些文章。使用 include() 函数时,你需要在包含片段的每个页面中执行相同的数据库查询。这不是很方便。
更好的选择是将执行某些控制器的结果嵌入到 render() 和 controller() Twig 函数中。
首先,创建呈现一定数量的最近文章的控制器:
1 | // src/Controller/BlogController.php |
然后,创建 blog/_recent_articles.html.twig 模板片段(模板名称中的 _ 前缀是可选的,但它是用于更好地区分完整模板和模板片段的约定):
1 | {# templates/blog/_recent_articles.html.twig #} |
现在,你可以通过任何模板调用此控制器以嵌入其结果:
1 | {# templates/base.html.twig #} |
使用 controller() 函数时,不会使用常规 Symfony 路由访问控制器,而是通过专用于为这些模板片段服务的特殊 URL 访问控制器。在 fragments 选项中配置该特殊 URL:
1 | # config/packages/framework.yaml |
1 | <!-- config/packages/framework.xml --> |
1 | // config/packages/framework.php |
嵌入控制器需要向这些控制器发出请求,并因此渲染一些模板。如果嵌入大量控制器,这会对应用程序性能产生重大影响。如果可能,请 缓存模板片段。
模板还可以使用
hinclude.jsJavaScript 库 异步嵌入内容 。
模板继承和布局
随着应用程序的增长,你将在页面之间找到越来越多的重复元素,如页眉、页脚、边栏等。包括模板和嵌入控制器可以提供帮助,但当页面共享公共结构时,最好使用继承。
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 | {# templates/base.html.twig #} |
Twig 块标记 定义了可以在子模板中覆盖的页面部分。它们可以为空,如 content 块或定义默认内容(如 title 块),当子模板不覆盖它们时,将显示这些内容。
blog/layout.html.twig 模板可以像这样:
1 | {# templates/blog/layout.html.twig #} |
模板继承自 base.html.twig,仅定义 content 块的内容。父模板块的其余部分将显示其默认内容。但是,它们可以被第三级继承模板(如 blog/index.html.twig)覆盖,该模板显示博客索引:
1 | {# templates/blog/index.html.twig #} |
这个模板从第二级模板(blog/layout.html.twig)扩展,但覆盖不同父模板的块:page_contents 来自 blog/layout.html.twig 并且 title 来自 base.html.twig
当你渲染 blog/index.html.twig 模板时,Symfony 使用三个不同的模板来创建最终内容。此继承机制可提高工作效率,因为每个模板仅包含其唯一的内容,并且将重复的内容和 HTML 结构留给某些父模板。
在使用
extend时,禁止子模板在块外部定义模板部分。以下代码引发一个语法错误:
1 | {# app/Resources/views/blog/index.html.twig #} |
阅读 Twig 模板继承 文档,详细了解如何在重写模板和其他高级功能时重用父块内容。
输出转义
假设你的模板包含显示用户名的 Hello 代码。如果恶意用户将 <script>alert('hello!')</script> 为用户名称,并且输出该值不变,则应用程序将显示 JavaScript 弹出窗口。
这被称为 跨站点脚本(XSS) 攻击。虽然前面的示例似乎无害,但攻击者可以编写更高级的 JavaScript 代码来执行恶意操作。
要防止这类攻击,使用“输出转义”转换具有特殊含义的字符(例如,将 < 替换为 < HTML 实体)。默认情况下,Symfony 应用程序是安全的,因为它们通过 Twig 自动逃逸选项 执行自动输出转义:
1 | <p>Hello {{ name }}</p> |
如果要呈现受信任的变量并包含 HTML 内容,请使用 Twig 原始过滤器 禁用该变量的输出转义:
1 | <h1>{{ product.title|raw }}</h1> |
阅读 Twig 输出转义文档,详细了解如何禁用块甚至整个模板的输出转义。
模板命名空间
尽管大多数应用程序将其模板存储在默认的 templates/ 目录中,但你可能需要将部分模板或所有模板存储在不同的目录中。使用 twig.paths 选项配置这些额外的目录。每个路径都定义为一个 key: value,其中 key 是模板目录,value 是 Twig 命名空间,下面将对此进行解释:
1 | # config/packages/twig.yaml |
1 | <!-- config/packages/twig.xml --> |
1 | // config/packages/twig.php |
渲染模板时,Symfony 首先在不定义命名空间的 twig.paths 目录中搜索模板,然后返回默认模板目录(通常为 templates/)。
使用上述的配置,如果应用程序呈现例如 layout.html.twig 模板,Symfony 将首先查找 email/default/templates/layout.html.twig 和 backend/templates/layout.html.twig。如果存在这些模板中的任何一个,Symfony 将使用它,而不是使用 templates/layout.html.twig,这可能是你想要使用的模板。
Twig 使用命名空间解决了这个问题,命名空间将多个模板分组到与其实际位置无关的逻辑名称下。更新以前的配置以为每个模板目录定义命名空间:
1 | # config/packages/twig.yaml |
1 | <!-- config/packages/twig.xml --> |
1 | // config/packages/twig.php |
现在,如果渲染 layout.html.twig 模板,Symfony 将渲染 templates/layout.html.twig 文件。使用特殊语法 @ + 命名空间来引用其他命名空间模板(例如 @email/layout.html.twig 和 @admin/layout.html.twig)。
单个 Twig 命名空间可以与多个模板目录关联。在这种情况下,添加路径的顺序很重要,因为 Twig 将从第一个定义的路径开始查找模板。
bundle模板
如果你在应用程序中 安装 packages/bundles,它们可能包含自己的 Twig 模板(在每个 bundle 的资源/视图/目录中)。为了避免弄乱自己的模板,Symfony 在 bundle 名称后创建的自动命名空间下添加 bundle 模板。
例如,名为 AcmeFobundle 的 bundle 的模板可在 AcmeFoo 命名空间下使用。如果此 bundle 包含模板 <your-project>/vendor/acmefoo-bundle/Resources/views/user/profile.html.twig,你可以用 @AcmeFoo/user/profile.html.twig 引用它。
你还可以 覆盖 bundle 模板 ,以防要更改原始 bundle 模板的某些部分。