Twig:子模板可以覆盖父模板中包含的文件中的块吗?
Twig: Can a child template override a block from a file included in the parent template?
我有一个较大的 base.twig 文件,我想将其分成三个部分:header.twig、content.twig 和 footer.twig。我无法从我的子模板中获取块来覆盖父模板中包含的块,我想知道这是否可能,如果不可能,那么 Twig-ish 解决方案可能是什么样子。
我设置了一个简单的例子来说明这个问题。我正在检索一个 Wordpress 页面并使用 Timber 处理 Twig 模板。被调用的 PHP 模板是 page-test.php:
<?
$context = Timber::get_context();
Timber::render('test_child.twig', $context);
?>
渲染的 Twig 模板是 test_child.twig:
{% extends 'test_base.twig' %}
{% block content_main %}
<h1>Random HTML</h1>
{% endblock content_main %}
父模板 test_base.twig 是:
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
{% include 'test_content.twig' %}
</body>
</html>
最后,包含的模板 test_content.twig 是这样的:
<div class="main">
{% block content_main %}
{% endblock content_main %}
</div>
结果输出如下所示:
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
<div class="main">
</div>
</body>
</html>
如您所见,<div>
没有内容。我期望它包含来自 test_child.twig.
的 <h1>Random HTML</h1>
片段
为什么 test_child.twig 中的块没有覆盖 test_content.twig 中包含的同名块到 test_base.twig 中?如果这种方法根本行不通,那么完成某件事的最佳 Twig-ish 方法是什么?
这对于 twig
确实是不可能的,因为包含的文件与调用它们的模板没有关联。为了解释我自己,请看一下这个片段
{{ include('foo.twig') }}
这段代码会被twig
编译器解析成PHP
,编译成的代码是
$this->loadTemplate("foo.twig", "main.twig", 6)->display($context);
现在我们可以通过查看 Twig_Template::loadTemplate
的来源进一步调查这个问题。如果你看一下那个特定的函数,我们会看到,因为你将 string
传递给函数,函数 loadTemplate
将在 class Twig_Environment
中被调用
在最后一个函数中,我们可以清楚地看到 Twig_Environment::loadTemplate
函数没有传递任何信息,也没有向您包含的模板传递您呈现的模板实例。唯一通过 (by value
) 的是变量 $context
,它包含您从控制器发送到您正在渲染的模板的所有变量。
我猜这样编码的主要原因之一是因为包含的文件在任何情况下都应该是可重用的,并且不应该有像 (non-existant) 块这样的依赖关系来渲染它们
TwigTemplate.php
protected function loadTemplate($template, $templateName = null, $line = null, $index = null) {
try {
if (is_array($template)) return $this->env->resolveTemplate($template);
if ($template instanceof self) return $template;
if ($template instanceof Twig_TemplateWrapper) return $template;
return $this->env->loadTemplate($template, $index);
} catch (Twig_Error $e) {
if (!$e->getSourceContext()) $e->setSourceContext($templateName ? new Twig_Source('', $templateName) : $this->getSourceContext());
if ($e->getTemplateLine()) throw $e;
if (!$line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
Environment.php
public function loadTemplate($name, $index = null) {
$cls = $mainCls = $this->getTemplateClass($name);
if (null !== $index) {
$cls .= '_'.$index;
}
if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls];
}
if (!class_exists($cls, false)) {
$key = $this->cache->generateKey($name, $mainCls);
if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
$this->cache->load($key);
}
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
$this->cache->write($key, $content);
$this->cache->load($key);
if (!class_exists($mainCls, false)) {
/* Last line of defense if either $this->bcWriteCacheFile was used,
* $this->cache is implemented as a no-op or we have a race condition
* where the cache was cleared between the above calls to write to and load from
* the cache.
*/
eval('?>'.$content);
}
if (!class_exists($cls, false)) {
throw new Twig_Error_Runtime(sprintf('Failed to load Twig template "%s", index "%s": cache is corrupted.', $name, $index), -1, $source);
}
}
}
因为我不确定你为什么有这个 set-up,这将是一个更 twig
风格的设置。请注意,您必须先在基础 class 中定义方块,因为扩展 class 中的 "defining" 方块会尝试显示父方块,而不会创建新方块。
test_main.twig
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
{% block content_main %}
{% include 'block.twig' %}
{% endblock %}
</body>
</html>
test_child.twig
{% extends "test_main.twig" %}
{% block content_main %}
{% include "test_content.twig" %}
{% endblock %}
test_content.twig
<div class="main">
Lorem Lipsum
</div>
不幸的是,这不适用于 include。
当我尝试将一些 SEO 值从我的产品控制器传递到包含元标记的基本模板时,我也遇到了这个问题。
您还必须对内部模板使用 "extends",并让您的控制器使用内部模板而不是 middle/layout。
然后您可以在内部模板上定义一个单独的块,它可以直接覆盖基本模板的块。
你可以在这个Fiddle中看到一个工作示例(注意内部模板是主要的)
https://twigfiddle.com/1ve5kt
我有一个较大的 base.twig 文件,我想将其分成三个部分:header.twig、content.twig 和 footer.twig。我无法从我的子模板中获取块来覆盖父模板中包含的块,我想知道这是否可能,如果不可能,那么 Twig-ish 解决方案可能是什么样子。
我设置了一个简单的例子来说明这个问题。我正在检索一个 Wordpress 页面并使用 Timber 处理 Twig 模板。被调用的 PHP 模板是 page-test.php:
<?
$context = Timber::get_context();
Timber::render('test_child.twig', $context);
?>
渲染的 Twig 模板是 test_child.twig:
{% extends 'test_base.twig' %}
{% block content_main %}
<h1>Random HTML</h1>
{% endblock content_main %}
父模板 test_base.twig 是:
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
{% include 'test_content.twig' %}
</body>
</html>
最后,包含的模板 test_content.twig 是这样的:
<div class="main">
{% block content_main %}
{% endblock content_main %}
</div>
结果输出如下所示:
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
<div class="main">
</div>
</body>
</html>
如您所见,<div>
没有内容。我期望它包含来自 test_child.twig.
<h1>Random HTML</h1>
片段
为什么 test_child.twig 中的块没有覆盖 test_content.twig 中包含的同名块到 test_base.twig 中?如果这种方法根本行不通,那么完成某件事的最佳 Twig-ish 方法是什么?
这对于 twig
确实是不可能的,因为包含的文件与调用它们的模板没有关联。为了解释我自己,请看一下这个片段
{{ include('foo.twig') }}
这段代码会被twig
编译器解析成PHP
,编译成的代码是
$this->loadTemplate("foo.twig", "main.twig", 6)->display($context);
现在我们可以通过查看 Twig_Template::loadTemplate
的来源进一步调查这个问题。如果你看一下那个特定的函数,我们会看到,因为你将 string
传递给函数,函数 loadTemplate
将在 class Twig_Environment
中被调用
在最后一个函数中,我们可以清楚地看到 Twig_Environment::loadTemplate
函数没有传递任何信息,也没有向您包含的模板传递您呈现的模板实例。唯一通过 (by value
) 的是变量 $context
,它包含您从控制器发送到您正在渲染的模板的所有变量。
我猜这样编码的主要原因之一是因为包含的文件在任何情况下都应该是可重用的,并且不应该有像 (non-existant) 块这样的依赖关系来渲染它们
TwigTemplate.php
protected function loadTemplate($template, $templateName = null, $line = null, $index = null) {
try {
if (is_array($template)) return $this->env->resolveTemplate($template);
if ($template instanceof self) return $template;
if ($template instanceof Twig_TemplateWrapper) return $template;
return $this->env->loadTemplate($template, $index);
} catch (Twig_Error $e) {
if (!$e->getSourceContext()) $e->setSourceContext($templateName ? new Twig_Source('', $templateName) : $this->getSourceContext());
if ($e->getTemplateLine()) throw $e;
if (!$line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
Environment.php
public function loadTemplate($name, $index = null) {
$cls = $mainCls = $this->getTemplateClass($name);
if (null !== $index) {
$cls .= '_'.$index;
}
if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls];
}
if (!class_exists($cls, false)) {
$key = $this->cache->generateKey($name, $mainCls);
if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
$this->cache->load($key);
}
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
$this->cache->write($key, $content);
$this->cache->load($key);
if (!class_exists($mainCls, false)) {
/* Last line of defense if either $this->bcWriteCacheFile was used,
* $this->cache is implemented as a no-op or we have a race condition
* where the cache was cleared between the above calls to write to and load from
* the cache.
*/
eval('?>'.$content);
}
if (!class_exists($cls, false)) {
throw new Twig_Error_Runtime(sprintf('Failed to load Twig template "%s", index "%s": cache is corrupted.', $name, $index), -1, $source);
}
}
}
因为我不确定你为什么有这个 set-up,这将是一个更 twig
风格的设置。请注意,您必须先在基础 class 中定义方块,因为扩展 class 中的 "defining" 方块会尝试显示父方块,而不会创建新方块。
test_main.twig
<!DOCTYPE html>
<head>
<title>Twig test</title>
</head>
<body>
{% block content_main %}
{% include 'block.twig' %}
{% endblock %}
</body>
</html>
test_child.twig
{% extends "test_main.twig" %}
{% block content_main %}
{% include "test_content.twig" %}
{% endblock %}
test_content.twig
<div class="main">
Lorem Lipsum
</div>
不幸的是,这不适用于 include。
当我尝试将一些 SEO 值从我的产品控制器传递到包含元标记的基本模板时,我也遇到了这个问题。
您还必须对内部模板使用 "extends",并让您的控制器使用内部模板而不是 middle/layout。
然后您可以在内部模板上定义一个单独的块,它可以直接覆盖基本模板的块。
你可以在这个Fiddle中看到一个工作示例(注意内部模板是主要的) https://twigfiddle.com/1ve5kt