PHP 很适合模板

Avatar of Chris Geelhoed
Chris Geelhoed

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

PHP 模板经常因为促进次优代码而受到批评——但事实并非如此。 让我们看看 PHP 项目如何在不依赖专门的模板引擎的情况下强制执行基本模型-视图-控制器(MVC)结构。

但首先,让我们简单回顾一下 PHP 的历史

PHP 作为 HTML 模板工具的历史充满了曲折。

最早用于 HTML 模板的编程语言之一是 C,但很快发现它使用起来很繁琐,并且通常不适合这项任务。

Rasmus Lerdorf 考虑到这一点创建了 PHP。 他并不反对使用 C 来处理后端业务逻辑,但想要一种更好的方法来为前端生成动态 HTML。 PHP 最初被设计为一种模板语言,但随着时间的推移,它采用了更多功能,并最终成为了一种独立的完整的编程语言。

PHP 在编程模式和 HTML 模式之间切换的独特能力被发现非常方便,但也让程序员很容易编写难以维护的代码——将业务逻辑和模板逻辑混合在一起的代码。 一个 PHP 文件可以从一些 HTML 模板开始,然后突然进入一个高级 SQL 查询,没有任何提示。 这种结构难以阅读,而且很难重用 HTML 模板。

随着时间的推移,Web 开发社区发现,为 PHP 项目强制执行严格的 MVC 结构越来越有价值。 模板引擎被创建为一种有效地将视图与其控制器分离的方法。

为了完成这项任务,模板引擎通常具有以下特点

  1. 引擎刻意削弱了对业务逻辑的支持。 例如,如果开发人员想要执行数据库查询,他们需要在控制器中进行该查询,然后将结果传递给模板。 在模板的 HTML 中间进行查询是不可能的。
  2. 引擎在幕后处理常见的安全风险。 即使开发人员未能验证用户输入并将输入直接传递给模板,模板通常也会自动转义任何危险的 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 函数转换为关联数组。 请注意,每个值的键不是 titleexcerptauthor,而是使用更通用的名称:headingbodyfooting。 我们这样做是因为卡片网格组件旨在支持任何类型的数据,而不仅仅是帖子。 例如,它可以很容易地用于显示带有引文和客户名称的推荐网格。

数据准备完成后,现在可以将其传递给我们的 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 包含 headingbodyfooting 的键,因此现在在模板中可以使用相同名称的变量。

在本例中,我们可以相当肯定我们的数据没有 XSS 危害,但有可能该视图将来会与用户输入一起使用,因此通过 htmlspecialchars 传递每个值是谨慎的做法。 如果我们的数据中存在脚本标记,它将被安全地转义。

在渲染变量之前检查每个变量是否包含非空值也很有帮助。 这允许在不将空 HTML 标记留在我们的标记中时省略变量。


PHP 模板引擎很棒,但有时使用 PHP 来完成其最初的设计目的也很合适:生成动态 HTML。

PHP 中的模板并不一定会导致难以维护的意大利面条式代码。 只要稍加预见,我们就可以实现一个基本的 MVC 系统,将视图和控制器彼此分离,而且可以使用很少的代码来完成。