Stempler 引擎提供了一个强大且灵活的模板引擎,它能够在词法分析器、解析器和 AST 编译级别进行自定义。 默认情况下,该驱动程序与 Spiral 框架应用程序的 Web 构建版本一起启用,并支持类似 Blade 的指令和回显、HTML 组件、栈等。
在本节中,我们将引导您完成使用 Stempler 创建和渲染基本视图的步骤。
第一步是创建视图文件。视图文件应保存在 app/views
目录(或在 ViewsBootloader
中配置的任何其他目录)中。 Stempler 模板的文件扩展名必须是 .dark.php
。
让我们创建一个名为 welcome.dark.php
的视图文件,其内容如下:
Hello, {{ $name }}!
并将其存储在 app/views
目录中。
现在我们可以从控制器渲染视图。
在我们的示例中,我们将使用 PrototypeTrait
来简化从容器获取 ViewsInterface
实例的操作。
了解更多 在 The Basics — Prototyping 部分阅读更多关于原型 trait 的内容。
namespace App\Endpoint\Web;
use Spiral\Prototype\Traits\PrototypeTrait;
class HomeController
{
use PrototypeTrait;
public function index(): string
{
return $this->views->render('welcome', [
'name' => 'John',
]);
}
}
您应该在屏幕上看到 Hello, John!
。
Stempler 模板也支持 PHP 底层语法:
Hello, <?= $name ?>!
危险 重要的是要注意,语法
{{ $name }}
提供自动转义,这有助于防止 XSS 攻击 等安全问题。另一方面,传统的 PHP 语法<?= $name ?>
不提供自动转义。如果您选择使用传统的 PHP 语法,建议手动转义变量以确保应用程序的安全性。
转义策略将根据您回显值的位置而改变。您可以在 script
标签内回显/嵌入您的值:
<script>
const value = {{ $name }};
</script>
它将根据值的类型进行不同的渲染:
如果是字符串值 ['name' => 'John']
,该值将自动被引用:
<script>
const value = "John";
</script>
要输出一个值而不进行任何自动转义,您可以使用替代语法。
{!! $value !!}
当您想输出 HTML 内容或其他不应转义的内容时,这会很有用。
这里有一个例子:
public function index(): string
{
return $this->views->render('welcome', [
'html' => '<div>Hello world</div>'
]);
}
视图文件:
{!! $html !!}
以及输出:
禁用转义后,HTML 内容将按原样输出,没有任何自动转义。
<div>Hello world</div>
除了经典的 echo 结构外,Stempler 还支持许多类似 Blade 的指令,以控制模板的业务逻辑。
与 Blade 或 Twig 不同,Stempler 指令仅负责管理业务逻辑。
Stempler 提供了几个循环指令,以帮助您管理模板中重复元素的渲染。这些指令使将动态内容合并到您的模板中变得容易。
注意 指令声明类似于原生 PHP 语法。
使用指令 @foreach
和 @endforeach
渲染循环:
<ul>
@foreach($items as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
使用指令 @for
和 @endfor
渲染循环:
<ul>
@for($i = 0; $i < 10; $i++)
<li>{{ $i }}</li>
@endfor
</ul>
使用指令 @while
和 @endwhile
渲染 while
循环:
<ul>
@while($i < 10)
<li>{{ $i }}</li>
@php $i++; @endphp
@endwhile
</ul>
使用 @break
和 @continue
指令中断您的循环:
<ul>
@while(true)
<li>{{ $i }}</li>
@if($i++ > 10)
@break
@endif
@endwhile
</ul>
注意
@break(2)
等同于break 2
。在下面阅读更多关于if
指令的内容。
Stempler 提供了几个用于在模板中创建条件语句的指令。这些指令被转录成原生 PHP 代码,并提供了一种更具可读性和效率的方式来处理模板中的条件。
这些例子给出了以下变量:
return $this->views->render('welcome', [
'value' => 123
]);
要创建简单的条件语句,请使用 @if
和 @endif
指令。
@if($value === 123)
Hello World
@endif
要添加 else
条件,请使用 @else
指令。
@if($value !== 123)
Value is not 123
@else
@endif
对于更复杂的条件,请使用 @elseif
指令。
@if($value === 124)
Value is not 124
@elseif($value === 123)
Value is 123
@else
Another value
@endif
@unless
指令允许您创建否定条件,并且可以像 @if
指令一样与 @else
和 @elseif
一起使用。
@unless($value === 124)
Value is not 124
@endunless
注意 您可以将
@else
和@elseif
与@unless
指令一起使用。
分别使用 @empty
和 @isset
条件来检查变量是否为空或已设置。
@empty($value)
Value is empty
@endempty
对于更复杂的条件,您可以使用 @switch
、@case
和 @break
语句。
@switch($value)
@case(123) value is 123 @break
@case(124) value is 124 @break
@case(125) value is 125 @break
@endswitch
@json
指令允许您在页面内渲染 JSON 数据。要使用它,只需将一个变量传递给该指令,如下所示:
@json($value)
注意
@json
指令等同于json_encode($value)
。
并设置一个变量:
return $this->views->render('welcome', [
'value' => ...
]);
输出将是:
如果是字符串值 ['value' => 'Hello world']
:
"Hello world"
将 JSON 数据嵌入到 JavaScript 语句中可能很有用:
这是一个视图模板的示例,其值为 ['value' => ['key' => 'value']]
:
<script type="text/javascript">
var value = @json($value);
console.log(value.key);
</script>
然后,生成的视图将如下所示:
<script type="text/javascript">
var value = {"key":"value"};
console.log(value.key);
</script>
Spiral 提供了许多可以在模板中使用的框架特定指令,包括:
要将容器依赖项调用到模板中,请使用 @inject($variable, "class")
指令:
@inject($app, App\App::class)
{{ get_class($app) }}
要创建一个路由,请使用指令 @route
:
<a href="@route('home:index')">click me</a>
您可以使用 controller:action
模式来处理由 default route
或路由名称处理的目标:
$router->addRoute(
'html',
new Route('/<action>.html', new Controller(HomeController::class))
);
使用第二个参数传递参数:
<a href="@route('html', ['action' => 'index'])">click me</a>
这些参数将自动 slugify 到路由 URL 中。 在路由模式中未找到的那些参数将作为查询参数传递:
<a href="@route('html', ['action' => 'index', 'id' => 10])">click me</a>
结果 /index.html?id=10
。
了解更多 在 HTTP — 路由 部分阅读更多关于路由和命名路由的内容。
要将 PHP 逻辑嵌入到您的模板中,请使用经典的 <?php
和 ?>
标签,或替代的 @php
和 @endphp
:
@php
echo "hello world";
@endphp
只需双写 'at' 字母,例如
@@ // -> 将呈现为 '@'
Stempler 提供了一种通过自定义指令扩展其功能的方法。 自定义指令是一个扩展 Spiral\Stempler\Directive\AbstractDirective
类的类,并实现一个 render
方法,该方法接受 Spiral\Stempler\Node\Dynamic\Directive
参数。
要创建自定义指令,请按照以下步骤操作:
创建一个扩展 Spiral\Stempler\Directive\AbstractDirective
并实现 render 方法的类,该方法具有所需的功能。
namespace App\Integration\Stempler;
use Spiral\Stempler\Directive\AbstractDirective;
use Spiral\Stempler\Node\Dynamic\Directive;
final class DatetimeDirective extends AbstractDirective
{
public function renderDateTime(Directive $directive): string
{
return '<?php echo date("Y-m-d H:i:s"); ?>';
}
}
注意 也可以实现
Spiral\Stempler\Directive\DirectiveRendererInterface
,以便更低级别地访问渲染过程。
使用引导程序类中的 StemplerBootloader::addDirective()
方法注册自定义指令。
namespace App\Application\Bootloader;
use App\Integration\Stempler\DatetimeDirective;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Stempler\Bootloader\StemplerBootloader;
final class CustomDirectiveBootloader extends Bootloader
{
public function boot(StemplerBootloader $stempler): void
{
$stempler->addDirective(DatetimeDirective::class);
}
}
自定义指令可以在模板中使用,通过使用适当的语法调用它。
这是模板代码:
<div>
@dateTime
</div>
这是由该指令生成的最终 PHP 代码:
<div>
<?php echo date("Y-m-d H:i:s"); ?>
</div>
通过使用自定义指令,您可以向模板引擎添加自定义功能并在不同的模板中重复使用。
您可以使用 Directive
对象的 body
和 values
属性将值传递给自定义指令。这些属性可用于访问传递给指令的值。 这允许您将动态值传递给指令,使其更灵活和可重用。
这是一个使用 body
属性的示例:
public function renderDateTime(Directive $directive): string
{
return \sprintf('<?php echo date(%s ?? "Y-m-d H:i:s"); ?>', $directive->body);
}
例子:
<div>
@dateTime('l')
</div>
此指令将生成以下 PHP 代码:
<div>
<?php echo date('l'); ?>
</div>
要访问传递给指令的由逗号分隔的特定值:
public function renderDateTime(Directive $directive): string
{
return \sprintf(
'<?php echo date(%s, %s); ?>',
$directive->values[0], // first value passed to the directive
$directive->values[1] // second value passed to the directive
);
}
例子:
@php
$format = 'Y-m-d H:i:s';
$timestamp = 199999999;
@endphp
<div>
@dateTime($format, $timestamp)
</div>
此指令将生成以下 PHP 代码:
<?php
$format = 'Y-m-d H:i:s';
$timestamp = 199999999;
?>
<div>
<?php echo date($format, $timestamp); ?>
</div>
警告 这些值不会自动转义,因此您必须在使用它们之前手动转义它们。
要获取有关指令从何处调用的信息,请使用 $directive->getContext()->getPath()
方法:
public function renderDateTime(Directive $directive): string
{
return \sprintf(
<<<PHP
<?php echo date(%s, %s); ?>
<!-- invoked from "%s" template -->
PHP,
$directive->values[0],
$directive->values[1],
$directive->getContext()->getPath()
);
}
处理此指令时,它将生成以下 PHP 代码:
<div>
<?php echo date($format, $timestamp); ?>
<!-- invoked from "welcome" template -->
</div>
随着您的视图变得越来越复杂,正确分离页面和布局特定的内容至关重要。 Stempler 提供了几个控制语句来实现这一点。
首先,让我们为我们的页面创建一个标准的 HTML 模板:
<!DOCTYPE html>
<html>
<head>
<title>This is homepage.</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body>
Page content
</body>
</html>
很可能,您的应用程序将包含多个页面模板。为了避免代码重复,Stempler 提供了继承父布局的能力。
注意 Stempler 将模板和父布局编译为优化的 PHP 代码。您可以排除任意数量的布局,而不会影响性能。
创建一个布局:
<!DOCTYPE html>
<html>
<head>
<title>This is homepage.</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body>
Page content
</body>
</html>
现在,我们可以通过 extends:path
标签使用 home.dark.php
扩展此布局:
<extends:layout.base/>
注意 使用分隔符
.
将目录名称包含到您的模板中。
或者,使用语法:
<extends path="layout/base"/>
注意 您可以在此类声明中使用视图命名空间,例如:
<extends path="default:layout/base"/>
。
扩展父布局并没有多大意义,除非我们可以重新定义其某些内容。 要定义一个可替换的块,请使用标签 <block:name>
。 相应地更改 layout/base.dark.php
:
<!DOCTYPE html>
<html>
<head>
<title><block:title/></title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body>
<block:content>
Default content body.
</block:content>
</body>
</html>
注意 您可以在
<block:name></block:name>
标签对内包含默认块内容。
要重新定义块值,请在 home.dark.php
模板中使用 block:name
或类似的标签:
<extends:layout.base/>
<block:title>Homepage</block:title>
<block:content>
This is homepage content.
</block:content>
在您的块定义短字符串或用作标签参数的情况下,使用替代语法 ${name|default}
。 将布局更改为:
<!DOCTYPE html>
<html>
<head>
<title>${title|Default title}</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body class="${body-class|default}">
<block:content>
Default content.
</block:content>
</body>
</html>
短语法值可以通过 <block:name>value</block:name>
标签提供给父布局。
<extends:layout.base/>
<block:title>Homepage</block:title>
<block:body-class>homepage</block:body-class>
<block:content>
This is homepage content.
</block:content>
您可以使用 extends
标签属性传递一些块值以避免大型子模板,相应地更改 app/views/home.dark.php
:
<extends:layout.base title="Homepage" body-class="homepage"/>
<block:content>
This is homepage content.
</block:content>
在两种情况下,生成的 HTML 都将如下所示:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body class="homepage">
This is homepage content.
</body>
</html>
要保留父块内容,请在重新定义的块的任何位置使用 <block:parent/>
:
<extends:layout.base title="Homepage" body-class="homepage"/>
<block:content>
This is homepage content.
<block:parent/>
</block:content>
生成的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body class="homepage">
This is homepage content.
Default content.
</body>
</html>
使用 ${parent}
,以在短块定义中实现相同的目标:
<extends:layout.base title="Homepage" body-class="homepage ${parent}"/>
<block:content>
This is homepage content.
<block:parent/>
</block:content>
输出:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body class="homepage default">
This is homepage content.
Default content.
</body>
</html>
可以将布局基于其他布局创建,创建 app/views/layout/page.dark.php
:
<extends:layout.base body-class="page ${parent}"/>
<block:content>
<div class="page-wrapper">
<block:page/>
</div>
</block:content>
注意 扩展标签始终需要完整的路径规范,请确保包含
layout
目录。
您可以在 app/views/home.dark.php
中扩展此布局而不是 base
:
<extends:layout.page title="Homepage" body-class="homepage ${parent}"/>
<block:page>
Page content.
</block:page>
生成的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
</head>
<body class="homepage page default">
<div class="page-wrapper">
Page content.
</div>
</body>
</html>
注意 您可以根据需要嵌套任意数量的模板,它只会影响编译速度。
Stempler 包含聚合在模板中定义的多个块的能力。
您通常需要向您的布局添加自定义 JS 或 CSS 资源。为了实现这一点,请使用 block
指令,将必要的资源包装在一个块中,然后在您的子模板中将其追加到它。
修改 app/views/layout/base.dark.php
为:
<!DOCTYPE html>
<html>
<head>
<title>${title|Default title}</title>
<block:styles>
<link rel="stylesheet" href="/styles/welcome.css"/>
</block:styles>
</head>
<body class="${body-class|default}">
<block:content>
Default content.
</block:content>
</body>
</html>
要在您的页面模板中添加自定义样式资源:
<extends:layout.base title="Homepage" body-class="homepage ${parent}"/>
<block:styles>
<block:parent/>
<link rel="stylesheet" href="/styles/homepage.css"/>
</block:styles>
<block:page>
Page content.
</block:page>
生成的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
<link rel="stylesheet" href="/styles/homepage.css"/>
</head>
<body class="homepage default">
Default content.
</body>
</html>
为了演示如何使用栈实现以下内容,我们应该从 app/views/home.dark.php
中的一个简单示例开始。使用 <stack:collect name="name"/>
创建一个栈占位符:
collect name="my-stack">
default content
</stack:collect>
要将一个值追加到栈中:
<stack:collect name="my-stack">
default content
</stack:collect>
<stack:push name="my-stack">
my value
</stack:push>
结果的 HTML:
default content
my value
要将一个值预先添加到栈中:
<stack:collect name="my-stack">
default content
</stack:collect>
<stack:prepend name="my-stack">
my value
</stack:prepend>
输出:
my value
default content
您可以在 push
和 prepend
标签之前或之后定位栈定义:
<stack:prepend name="my-stack">
my value
</stack:prepend>
<stack:collect name="my-stack">
default content
</stack:collect>
如果它位于同一标签树级别上,则栈标签将仅聚合 push
和 prepend
值。
例如,这将起作用:
<stack:collect name="my-stack">
default content
</stack:collect>
// stack my-stack is active here
<div>
// and here
<stack:prepend name="my-stack">
my value
</stack:prepend>
</div>
而此示例将不起作用:
<div>
// stack my-stack is active here
<stack:collect name="my-stack">
default content
</stack:collect>
// and here
</div>
// stack my-stack is not active at this level
<stack:prepend name="my-stack">
my value
</stack:prepend>
注意 此限制是由栈收集器的 AST 本质引起的。
要在不将占位符级别提高的情况下绕过此限制,请使用 stack:collect
属性 level
:
<div>
<stack:collect name="my-stack" level="1">
default content
</stack:collect>
</div>
<stack:prepend name="my-stack">
my value
</stack:prepend>
属性 level
将栈配置为多个激活级别更高。 例如,此示例将不起作用:
<div>
// stack my-stack is active here
<div>
// stack my-stack is active here
<stack:collect name="my-stack" level="1">
default content
</stack:collect>
</div>
</div>
// stack my-stack is no active at this level
<stack:prepend name="my-stack">
my value
</stack:prepend>
但是这个会起作用:
<div>
// stack my-stack is active here
<div>
// stack my-stack is active here
<stack:collect name="my-stack" level="2">
default content
</stack:collect>
</div>
</div>
// stack my-stack is active here
<stack:prepend name="my-stack">
my value
</stack:prepend>
您可以将值推送到在父布局中定义的栈中。 相应地修改 app/views/layout/base.dark.php
:
<!DOCTYPE html>
<html>
<head>
<title>${title|Default title}</title>
<stack:collect name="styles" level="2">
<link rel="stylesheet" href="/styles/welcome.css"/>
</stack:collect>
<block:resources/>
</head>
<body class="${body-class|default}">
<block:content>
Default content.
</block:content>
</body>
</html>
现在您可以从 app/views/home.dark.php
推送该值:
<extends:layout.base title="Homepage" body-class="homepage ${parent}"/>
<block:resources>
<stack:push name="styles">
<link rel="stylesheet" href="/styles/homepage.css"/>
</stack:push>
</block:resources>
<block:page>
Page content.
</block:page>
注意 您必须确保
stack:push
位于其中一个扩展块中。 参见下面如何绕过它。
正如您在前面的示例中看到的那样,同时使用栈和块并不方便。这是因为栈收集发生在父布局扩展之后。将栈保留在任何 block
之外将使其排除在模板之外。
所有在子模板中定义的位于 block
标签之外的 stempler 块都将出现在系统块 context
中。 我们可以像这样修改父布局 app/views/layout/base.dark.php
:
<!DOCTYPE html>
<html>
<head>
<title>${title|Default title}</title>
<stack:collect name="styles" level="2">
<link rel="stylesheet" href="/styles/welcome.css"/>
</stack:collect>
</head>
<body class="${body-class|default}">
<block:content>
Default content.
</block:content>
</body>
<block:context/>
</html>
现在我们可以在 app/views/home.dark.php
中定义栈,如下所示:
<extends:layout.base title="Homepage" body-class="homepage ${parent}"/>
<stack:push name="styles">
<link rel="stylesheet" href="/styles/homepage.css"/>
</stack:push>
some random string
<block:page>
Page content.
</block:page>
要了解上下文的工作原理,请查看生成的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Homepage</title>
<link rel="stylesheet" href="/styles/welcome.css"/>
<link rel="stylesheet" href="/styles/homepage.css"/>
</head>
<body class="homepage default">
Default content.
</body>
some random string
</html>
请注意,some random string
被添加而不是 block:context
,此内容由 app/views/home.dark.php
声明。您最有可能将模板的块定义之间的区域用于注释和其他控制指令。
要从最终使用中隐藏此类内容,请在 app/views/layout/base.dark.php
中使用 <hidden></hidden>
标签:
<!DOCTYPE html>
<html>
<head>
<title>${title|Default title}</title>
<stack:collect name="styles" level="2">
<link rel="stylesheet" href="/styles/welcome.css"/>
</stack:collect>
</head>
<body class="${body-class|default}">
<block:content>
Default content.
</block:content>
</body>
<hidden>
<block:context/>
</hidden>
</html>
现在,堆叠将像以前一样工作。 但是,some random string
不会出现在页面上。
注意 将栈与继承和 组件 结合使用,以创建特定于领域的渲染 DSL。
Stempler 提供了创建开发人员驱动的模板组件作为虚拟标签的能力。
在许多情况下,您的模板不仅会重用父布局,还会重用模板部分,例如:
<extends:layout.base title="Homepage"/>
<block:content>
This is the homepage.
<div class="article">
<div class="title">Article title</div>
<div class="preview">article preview</div>
</div>
<div class="article">
<div class="title">Article title 2</div>
<div class="preview">article preview 2</div>
</div>
<div class="article">
<div class="title">Article title 3</div>
<div class="preview">article preview 3</div>
</div>
</block:content>
我们可以将 article div 移到单独的模板 app/views/partial/article.dark.php
中: