PHP 模板经常因为促进次优代码而受到批评——但事实并非如此。 让我们看看 PHP 项目如何在不依赖专门的模板引擎的情况下强制执行基本模型-视图-控制器(MVC)结构。
但首先,让我们简单回顾一下 PHP 的历史
PHP 作为 HTML 模板工具的历史充满了曲折。
最早用于 HTML 模板的编程语言之一是 C,但很快发现它使用起来很繁琐,并且通常不适合这项任务。
Rasmus Lerdorf 考虑到这一点创建了 PHP。 他并不反对使用 C 来处理后端业务逻辑,但想要一种更好的方法来为前端生成动态 HTML。 PHP 最初被设计为一种模板语言,但随着时间的推移,它采用了更多功能,并最终成为了一种独立的完整的编程语言。
PHP 在编程模式和 HTML 模式之间切换的独特能力被发现非常方便,但也让程序员很容易编写难以维护的代码——将业务逻辑和模板逻辑混合在一起的代码。 一个 PHP 文件可以从一些 HTML 模板开始,然后突然进入一个高级 SQL 查询,没有任何提示。 这种结构难以阅读,而且很难重用 HTML 模板。
随着时间的推移,Web 开发社区发现,为 PHP 项目强制执行严格的 MVC 结构越来越有价值。 模板引擎被创建为一种有效地将视图与其控制器分离的方法。
为了完成这项任务,模板引擎通常具有以下特点
- 引擎刻意削弱了对业务逻辑的支持。 例如,如果开发人员想要执行数据库查询,他们需要在控制器中进行该查询,然后将结果传递给模板。 在模板的 HTML 中间进行查询是不可能的。
- 引擎在幕后处理常见的安全风险。 即使开发人员未能验证用户输入并将输入直接传递给模板,模板通常也会自动转义任何危险的 HTML。
模板引擎现在是许多 Web 技术栈中的一个主要功能。 它们使代码库更易于维护、更安全,因此这并不奇怪。
然而,也可以使用纯 PHP 处理 HTML 模板。 您可能出于多种原因想要这样做。 也许您正在处理一个遗留项目,不想引入任何额外的依赖项,或者也许您正在处理一个非常小的项目,并且希望保持尽可能轻量级。 或者也许这个决定完全不由你决定。
PHP 模板的用例
我经常使用纯 PHP 模板的一个地方是 WordPress,它默认情况下不强制执行严格的 MVC 结构。 我觉得引入一个完整的模板引擎有点太过繁琐,但我仍然希望将业务逻辑与我的模板分离,并且希望我的视图可以重复使用。
无论您的理由是什么,使用纯 PHP 来定义您的 HTML 模板有时是最佳选择。 本文探讨了如何以合理专业的方式完成此操作。 该方法代表了 PHP 模板因其臭名昭著的意大利面条式代码而臭名昭著的风格与正式模板引擎提供的“禁止逻辑”方法之间的实际折衷方案。
让我们深入了解一个关于如何将基本模板系统付诸实践的示例。 再次,我们使用 WordPress 作为示例,但这可以切换到纯 PHP 环境或许多其他环境。 并且您无需熟悉 WordPress 即可理解。
目标是将我们的视图分解为组件,并在业务逻辑和 HTML 模板之间建立明显的区分。 具体来说,我们将创建一个显示卡片网格的视图。 每个卡片都将显示最近发布的帖子的标题、摘录和作者。
步骤 1:获取要渲染的数据
第一步是获取我们要在视图中显示的数据。 这可能涉及执行 SQL 查询或使用框架/CMS 的 ORM 或辅助函数间接访问数据库。 它也可能涉及向外部 API 发出 HTTP 请求或从表单或查询字符串中收集用户输入。
在本例中,我们将使用 WordPress 的 get_posts
辅助函数获取一些要显示在我们主页上的帖子。
<?php // index.php
$wp_posts = get_posts([
'numberposts' => 3
]);
我们现在可以访问要显示在卡片网格中的数据,但我们需要在将其传递给视图之前进行一些额外的工作。
步骤 2:准备模板数据
get_posts
函数返回一个包含 WP_Post
对象的数组。 每个对象都包含我们需要显示的帖子标题、摘录和作者信息,但我们不想将视图与 WP_Post
对象类型耦合,因为我们可能想在项目的其他地方使用卡片显示其他类型的数据。
相反,将每个帖子对象转换为中立数据类型,例如关联数组是有意义的。
<?php // index.php
$wp_posts = get_posts([
'numberposts' => 3
]);
$cards = array_map(function ($wp_post) {
return [
'heading' => $wp_post->post_title,
'body' => $wp_post->post_excerpt,
'footing' => get_author_name($wp_post->post_author)
];
}, $wp_posts);
在本例中,每个 WP_Post
对象都使用 array_map
函数转换为关联数组。 请注意,每个值的键不是 title
、excerpt
和 author
,而是使用更通用的名称:heading
、body
和 footing
。 我们这样做是因为卡片网格组件旨在支持任何类型的数据,而不仅仅是帖子。 例如,它可以很容易地用于显示带有引文和客户名称的推荐网格。
数据准备完成后,现在可以将其传递给我们的 render_view
函数。
<?php // index.php
// Data fetching and formatting same as before
render_view('cards_grid', [
'cards' => $cards
]);
当然,render_view
函数尚不存在。 让我们来定义它。
步骤 3:创建渲染函数
// Defined in functions.php, or somewhere else that will make it globally available.
// If you are worried about possible collisions within the global namespace,
// you can define this function as a static method of a namespaced class
function render_view($view, $data)
{
extract($data);
require('views/' . $view . '.php');
}
此函数接受要渲染的视图的名称和一个关联数组,该数组表示要显示的任何数据。 extract
函数接受关联数组中的每个项目并为其创建一个变量。 在本例中,我们现在有一个名为 $cards
的变量,其中包含我们在 index.php
中准备的项目。
由于视图在其自己的函数中执行,因此它获得了自己的作用域。 这样很好,因为它允许我们使用简单的变量名称,而不必担心冲突。
函数的第二行打印与传递的名称匹配的视图。 在这种情况下,它在 views/cards_grid.php
中查找视图。 让我们继续创建该文件。
步骤 4:创建模板
<?php /* views/cards_grid.php */ ?>
<section>
<ul>
<?php foreach ($cards as $card) : ?>
<li>
<?php render_view('card', $card) ?>
</li>
<?php endforeach; ?>
</ul>
</section>
此模板使用刚刚提取的 $cards
变量,并将其渲染为无序列表。 对于数组中的每个卡片,模板都会渲染一个子视图:单个卡片视图。
拥有一个单个卡片的模板很有用,因为它让我们能够直接渲染单个卡片或将其用于项目的其他地方的另一个视图。
让我们定义基本的卡片视图
<?php /* views/card.php */ ?>
<div class="card">
<?php if (!empty($heading)) : ?>
<h4><?= htmlspecialchars($heading) ?></h4>
<?php endif;
if (!empty($body)) : ?>
<p><?= htmlspecialchars($body) ?></p>
<?php endif;
if (!empty($footing)) : ?>
<span><?= htmlspecialchars($footing) ?></span>
<?php endif; ?>
</div>
由于传递给渲染函数的 $card
包含 heading
、body
和 footing
的键,因此现在在模板中可以使用相同名称的变量。
在本例中,我们可以相当肯定我们的数据没有 XSS 危害,但有可能该视图将来会与用户输入一起使用,因此通过 htmlspecialchars
传递每个值是谨慎的做法。 如果我们的数据中存在脚本标记,它将被安全地转义。
在渲染变量之前检查每个变量是否包含非空值也很有帮助。 这允许在不将空 HTML 标记留在我们的标记中时省略变量。
PHP 模板引擎很棒,但有时使用 PHP 来完成其最初的设计目的也很合适:生成动态 HTML。
PHP 中的模板并不一定会导致难以维护的意大利面条式代码。 只要稍加预见,我们就可以实现一个基本的 MVC 系统,将视图和控制器彼此分离,而且可以使用很少的代码来完成。
这是制作模板的旧方法,但效果很好!我更喜欢使用 Blade 或 Twig,但如果你有自己的小型框架,这种方法可以完美地工作。:)
所以,你必须记住每次都使用
htmlspecialchars
,否则就会存在潜在的 XSS 漏洞。换句话说,它默认不安全,这意味着它不安全。我们当然不能推荐“你只需要记住一次又一次地做 X,顺便说一下,做 X 会让你的代码可读性降低”作为构建安全可靠软件的方法?这正是你不能将 PHP 用作模板语言的原因,这真的很不幸,因为它是 PHP 真正设计出来的唯一用途。
PHP 能解决这个问题吗?嗯,看起来 Rasmus 认为 XSS 的解决方案与“魔法引号”(还记得它们吗?) 基本上是一样的——在这里描述和分析——https://lukeplant.me.uk/blog/posts/why-escape-on-input-is-a-bad-idea/
我对你的论点感到困惑,因为每个模板语言都允许显示 html (据我所知)。因此,每个模板系统都同样容易受到攻击。
我使用 Mustache,即使在 PHP 中也是如此,因为我想要设计师专注于设计,而不是代码。而且他们不能做得足够好,用 PHP 制作模板。而那些能够做到的人,不是优秀的设计师。
角色分离是最好的长期解决方案。我们中那些既能设计又能编码的人,可以处理 XSS 问题。
一个更好的方法通常是在数据到达模板之前对数据进行转义。模板不应该真正决定什么需要转义,什么不需要转义,它应该假设所有数据都适合显示。
同样,我通常不会将内容包裹在段落标签中,调用代码应该能够决定内容是单个段落,还是需要在那个位置添加更多内容。如果我正在实现这样的代码,我通常会让调用函数在传递到模板之前包裹和转义内容。
我使用 Smarty 模板引擎来进行 PHP,它非常不错。
太棒了。这就是我爱 PHP 的原因,简单有效,只要你以正确的方式使用它。
好主意。作为替代方案,Timber 是一个很好的解决方案,它提供了一种强大的伪 MVC 方式来处理 WordPress。它使用 Twig 进行模板化,与不得不将模板散布在打开/关闭 PHP 标签和 echo 语句中相比,使用 Twig 非常好。它还提供了一些出色的工具来处理图像调整大小、源集和缓存等,并允许你无缝地调用 WP hook 和函数等。
如果 Timber 的功能太多,或者对于现有代码库来说重构的程度太大,你仍然可以通过将 Twig 作为依赖项来获得 Twig 模板化的益处——然后你可以将上下文数据传递到渲染/编译函数中。
Laravel 中的 Blade 视图是 MVC 模式最好的例子之一。
将
$cards
的准备工作放在回调函数中。示例
function callModel() {
return function() { ...}
}
此函数调用数据传递引擎,例如模型。
然后从 html 文档中调用
$cards= ($model->callModel())();
请注意,html 部分不是数据传递引擎(模型)的扩展,而是应用程序的独立层。
HTML 是汽车的解耦车身。与引擎的通信是通过触发钥匙来完成的。
不需要带有
extract()
等等的模板引擎。只需使用 php 的include
构建 html 结构,并在你需要数据的地方从 html 回调模型。我再也不会回头使用模板引擎了。
与当前的模板引擎相比,它仍然看起来像意大利面条代码。
<?php
仍然会影响可读性。至少对我来说是这样。我同意,我启用了 PHP 的短代码,因为我很少与 XML 合作,我可以在那里添加特殊的
<?php
标签。它比这要好得多。