创建自己的 PHP 模板引擎 - 渲染 & Echo
我们来创建一个小型 PHP 模板引擎!
本文主要关注模板的渲染及 echo 输出能被 htmlspecialchars() 转义的数据。
在我们开始编写代码之前,我们需要注意任何编程项目中最重要的部分——为项目命名。我将称之为“Stencil”。
模板本身都将是纯PHP。我们不会创建任何像 Twig 或 Blade 这样的特殊语法,我们将只关注模板功能。
我们将从创建主类开始。
class Stencil
{
public function __construct(
protected string $path,
) {}
}
Stencil 类需要知道模板所在位置 —— 这样才能通过构造函数传递。
实际模板渲染需要一个render() 方法。
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
// ?
}
}
render() 方法接收模板名以及一个数据数组变量,使这些变量在模板内部可以访问。
我们需要做三件事:
- 创建到所请求的模板的路径。
- 确保模板存在。
- 使用所提供的数据渲染模板。
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
// ?
}
}
列表中的前两个很容易做到。Stencil 只会查找 .php 文件,因此形成路径只是连接一些字符串。如果请求的模板包含任何目录分隔符,则将处理目录中模板的嵌套。
如果该文件不存在,则抛出自定义的 TemplateNotFoundException。
为了涵盖列表中的第三点,即实际渲染模板,我们需要创建一个名为 Template 的新类。这将容纳模板可用的所有方法,并处理真实渲染方面的事务。
class Template
{
public function __construct(
protected string $path,
protected array $data = [],
) {}
public function render(): string
{
// ?
}
}
class Stencil
{
// ...
public function render(string $template, array $data = []): string
{
$path = $this->path . DIRECTORY_SEPARATOR . $template . '.php';
if (! file_exists($path)) {
throw TemplateNotFoundException::make($template);
}
return (new Template($path, $data))->render();
}
}
为了获得作为字符串的渲染模板,我们将利用 PHP 的输出缓冲区。当您调用 ob_start() 时,PHP 将开始捕获应用程序试图输出的任何内容(echo、原始HTML等)。
您可以将其作为字符串检索,然后使用 ob_get_clean() 停止捕获输出。这两个函数和 include 的组合将使我们能够评估模板文件。
class Template
{
// ...
public function render(): string
{
ob_start();
include $this->path;
return ob_get_clean();
}
}
这将处理模板渲染,但不会让模板访问 $data 中存储的数据变量。PHP是一种很棒的语言,它提供了另一个名为 extract() 的函数,该函数接受一组键值对。
数组中每个项的键将用于使用关联值在当前作用域中创建一个新变量。由于 include 及其相关内容总是在当前范围内执行 PHP 文件,因此模板将能够访问提取的变量。
class Template
{
// ...
public function render(): string
{
ob_start();
extract($this->data);
include $this->path;
return ob_get_clean();
}
}
完美!现在我们可以渲染一个模板,并使其可访问所提供的数据变量。有一件事我们还没有考虑… 如果我们想在 render() 方法中创建一些变量,我们的模板也应该可以访问这些变量。这不是我们想要的!
为了解决这个问题,我们需要将 extract() 和 include 调用包含在一个立即调用的闭包中——这样,模板将只能访问闭包内部的变量。
class Template
{
// ...
public function render(): string
{
ob_start();
(function () {
extract($this->data);
include $this->path;
})();
return ob_get_clean();
}
}
最后是在 echo 值时转义值的方法。闭包继承 $this,这意味着我们的模板将能够调用我们在 Template 类上定义的任何方法。让我们创建一个 e() 方法,该方法接受一个值并使用 htmlspecialchar() 对其进行转义。
class Template
{
// ...
public function e(?string $value): string
{
return htmlspecialchar($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
像这样,我们就为我们的 PHP 项目创建了一个消息的模板引擎。
<h1>
Hello, <?= $this->e($name) ?>!
</h1>
上面的模板也可以使用我们的引擎进行渲染:
$stencil->render('hello', [
'name' => 'Ryan'
]);
它将输出以下 HTML:
<h1>
Hello, Ryan!
</h1>
后续我们将实现对 partials 的支持,使我们能够分离出常见的模板并在多个地方使用它们。