V 视图

视图是处理…​…​视图的层。在这一层中,您可以找到所有用于呈现用户所看到的 HTML 的模板。虽然视图和应用程序其他部分之间的分离很容易看到,但这并不意味着视图是一个简单的部分。事实上,要正确编写视图,你必须学习一门新技术。让我们来详细了解一下。

Twig 简介

在第一次尝试编写视图时,我们混淆了 PHP 和 HTML 代码。我们已经知道逻辑不应该与 HTML 混在一起,但这并不是故事的结尾。在呈现 HTML 时,我们也需要一些逻辑。例如,如果我们要打印一本书的列表,我们就需要为每本书重复打印一定的 HTML 代码块。由于我们先验地不知道要打印的书籍数量,所以最好的选择是使用 foreach 循环。

很多人都会选择尽量减少视图中的逻辑。你可以设定一些规则,比如我们应该只包含条件和循环,这是呈现基本视图所需的合理逻辑量。问题是没有办法强制执行这种规则,其他开发人员很容易就开始在视图中添加大量逻辑。有些人认为没有人会这么做,因此对此没有意见,而另一些人则倾向于实施限制性更强的系统。这就是模板引擎的雏形。

您可以将模板引擎视为您需要学习的另一种语言。为什么要这么做呢?因为这种新 "语言" 比 PHP 更有局限性。这些语言通常只允许执行条件和简单的循环,仅此而已。开发人员无法在该文件中添加 PHP,因为模板引擎不会将其视为 PHP 代码。相反,它只会把代码打印到输出端—​响应的正文—​就像打印纯文本一样。此外,由于模板引擎是专门为编写模板而设计的,其语法与 HTML 混合时通常更容易阅读。几乎所有优点都是如此。

使用模板引擎的不便之处在于,它需要一些时间将新语言翻译成 PHP,然后再翻译成 HTML。这可能相当耗时,因此选择一个好的模板引擎非常重要。大多数模板引擎还允许缓存模板,从而提高性能。我们选择的是一个相当轻便且广泛使用的引擎:Twig。由于我们已经在 Composer 文件中添加了依赖关系,因此可以直接使用。

Twig 的设置非常简单。在 PHP 方面,只需指定模板的位置。通常的做法是使用 views 目录。创建该目录,并在 index.php 中添加以下两行:

$loader = new Twig_Loader_Filesystem(__DIR__ . '/views');
$twig = new Twig_Environment($loader);

book 视图

在这些章节中,当我们使用模板时,最好能看到您的工作成果。我们还没有实现任何控制器,因此无论请求是什么,我们都将强制 index.php 渲染一个特定的模板。我们可以开始渲染一本书的视图。为此,让我们在创建 twig 对象后,在 index.php 结尾添加以下代码:

$bookModel = new BookModel(Db::getInstance());
$book = $bookModel->get(1);

$params = ['book' => $book];
echo $twig->loadTemplate('book.twig')->render($params);

在前面的代码中,我们向 BookModel 请求 ID 为 1 的图书,获取图书对象,并创建一个数组,其中 book 键的值就是 book 对象的值。然后,我们告诉 Twig 加载 book.twig 模板,并通过发送数组来渲染它。这样,模板就会注入 $book 对象,这样你就可以在模板中使用它了。

现在让我们创建第一个模板。将以下代码写入 view/book.twig。按照惯例,所有 Twig 模板的扩展名为 .twig

<h2>{{ book.title }}</h2>
<h3>{{ book.author }}</h3>

<hr>

<p>
    <strong>ISBN</strong> {{ book.isbn }}
</p>

<p>
    <strong>Stock</strong> {{ book.stock }}
</p>
<p>
    <strong>Price</strong> {{ book.price|number_format(2) }} €
</p>

<hr>

<h3>Actions</h3>

<form method="post" action="/book/{{ book.id }}/borrow">
    <input type="submit" value="Borrow">
</form>

<form method="post" action="/book/{{ book.id }}/buy">
    <input type="submit" value="Buy">
</form>

由于这是你的第一个 Twig 模板,让我们一步一步来。你可以看到,大部分内容都是 HTML:一些标题、几个段落和两个带有两个按钮的表单。你可以认出 Twig 部分,因为它是用 {{ }} 括起来的。在 Twig 中,这些大括号之间的所有内容都会被打印出来。我们找到的第一个表单包含 book.title。还记得我们在渲染模板时注入了 book 对象吗?我们可以在这里访问它,但不是用通常的 PHP 语法。要访问对象的属性,请使用 . 而不是 ->。因此,book.title 将返回图书对象 title 属性的值,而 {{ }} 将使 Twig 将其打印出来。这同样适用于模板的其他部分。

除了访问对象的属性外,还有一个模板的作用更大。book.price|number_format(2) 获取图书的价格,并将其作为参数(使用管道符号)发送给函数 number_format,该函数的另一个参数是 2。这段代码基本上是将价格格式化为两个数字。在 Twig 中,你也有一些函数,但它们大部分都被简化为格式化输出,这是可以接受的逻辑量。

现在你是否已经确信,在视图中使用模板引擎是多么干净利落了?你可以在浏览器中试试:访问任何路径时,网络服务器都会执行 index.php 文件,强制渲染 book.twig 模板。

布局和分块

在设计网络应用程序时,您通常希望在大部分视图中共享一个通用布局。在我们的例子中,我们希望在视图的顶部始终有一个菜单,让我们可以进入网站的不同部分,甚至可以让用户在任何地方搜索书籍。与模型一样,我们希望避免代码重复,因为如果我们到处复制和粘贴布局,更新将是一场噩梦。相反,Twig 提供了定义布局的功能。

Twig 中的布局只是另一个模板文件。它的内容只是我们希望在所有视图(在我们的例子中,是菜单和搜索栏)中显示的通用 HTML 代码,并包含一些带标记的空隙(Twig 世界中的区块),您可以在其中注入每个视图的特定 HTML。您可以使用标签 {% block %} 来定义其中的一个块。让我们看看我们的 views/layout.twig 文件会是什么样子:

<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <div style="border: solid 1px">
        <a href="/books">Books</a>
        <a href="/sales">My Sales</a>
        <a href="/my-books">My Books</a>
        <hr>
        <form action="/books/search" method="get">
            <label>Title</label>
            <input type="text" name="title">
            <label>Author</label>
            <input type="text" name="author">
            <input type="submit" value="Search">
        </form>
    </div>
    {% block content %}{% endblock %}
</body>
</html>

正如您在前面的代码中所看到的,区块都有一个名称,以便使用布局的模板可以引用它们。在我们的布局中,我们定义了两个区块:一个用于视图标题,另一个用于内容本身。当模板使用布局时,我们只需为布局中定义的每个区块编写 HTML 代码,剩下的工作就交给 Twig 了。此外,为了让 Twig 知道我们的模板想要使用布局,我们使用了带有布局文件名的标记 {% extends %}。让我们更新 views/book.twig,以使用我们的新布局:

{% extends 'layout.twig' %}

{% block title %}
    {{ book.title }}
{% endblock %}

{% block content %}
<h2>{{ book.title }}</h2>
//...
</form>
{% endblock %}

在文件顶部,我们添加需要使用的布局。然后,我们打开一个带有引用名称的块标记,并在其中写入我们要使用的 HTML。你可以在块中使用任何有效的内容,无论是 Twig 代码还是纯 HTML。在我们的模板中,我们用书的标题作为 title 块,它指的是视图的标题。请注意,现在文件中的所有内容都在一个块中。现在就在浏览器中试试看,看看有什么变化。

分页 book 列表

让我们添加另一个视图,这次是一个分页的图书列表。为了查看您的工作结果,请更新 index.php 的内容,将上一节的代码替换为以下内容:

$bookModel = new BookModel(Db::getInstance());
$books = $bookModel->getAll(1, 3);

$params = ['books' => $books, 'currentPage' => 2];
echo $twig->loadTemplate('books.twig')->render($params);

在前面的代码段中,我们强制应用程序渲染 books.twig 模板,从第 1 页开始发送图书数组,每页显示 3 本图书。不过,这个数组可能并不总是返回 3 本书,这可能是因为数据库中只有 2 本书。这时,我们应该使用循环来遍历列表,而不是假设数组的大小。在 Twig 中,您可以使用 {% for <element> in <array> %} 来模拟 foreach 循环,以遍历数组。让我们在 views/books.twig 中使用它:

{% extends 'layout.twig' %}

{% block title %}
    Books
{% endblock %}

{% block content %}
<table>
    <thead>
        <th>Title</th>
        <th>Author</th>
        <th></th>
    </thead>
{% for book in books %}
    <tr>
        <td>{{ book.title }}</td>
        <td>{{ book.author }}</td>
        <td><a href="/book/{{ book.id }}">View</a></td>
    </tr>
{% endfor %}
</table>
{% endblock %}

我们还可以在 Twig 模板中使用条件,其作用与 PHP 中的条件相同。语法是 {% if <布尔表达式> %}。让我们用它来决定是否要在页面上显示前一个和/或后一个链接。在内容块末尾添加以下代码:

{% if currentPage != 1 %}
    <a href="/books/{{ currentPage - 1 }}">Previous</a>
{% endif %}
{% if not lastPage %}
    <a href="/books/{{ currentPage + 1 }}">Next</a>
{% endif %}

本模板最后要说明的是,在使用 {{ }} 打印内容时,我们并不局限于只使用变量。我们可以添加任何能返回值的有效 Twig 表达式,就像我们在使用 {{ currentPage + 1 }} 时所做的那样。

sales 视图

我们已经向您展示了使用模板所需的一切,现在只需完成所有模板的添加。下一个是显示特定用户销售清单的模板。用以下代码更新你的 index.php 文件:

$saleModel = new SaleModel(Db::getInstance());
$sales = $saleModel->getByUser(1);

$params = ['sales' => $sales];
echo $twig->loadTemplate('sales.twig')->render($params);

该视图的模板将与列出书籍的模板非常相似:一个用数组内容填充的表格。以下是 views/sales.twig 的内容:

{% extends 'layout.twig' %}

{% block title %}
    My sales
{% endblock %}

{% block content %}
<table>
    <thead>
        <th>Id</th>
        <th>Date</th>
    </thead>
{% for sale in sales %}
    <tr>
        <td>{{ sale.id}}</td>
        <td>{{ sale.date }}</td>
        <td><a href="/sales/{{ sale.id }}">View</a></td>
    </tr>
{% endfor %}
</table>
{% endblock %}

另一个与销售相关的视图是我们要显示特定销售的所有内容。同样,该销售视图将与书籍列表类似,因为我们将列出与该销售相关的书籍。强制渲染该模板的方法如下:

$saleModel = new SaleModel(Db::getInstance());
$sale = $saleModel->get(1);

$params = ['sale' => $sale];
echo $twig->loadTemplate('sale.twig')->render($params);

Twig 模板应放置在 views/sale.twig 中:

{% extends 'layout.twig' %}

{% block title %}
    Sale {{ sale.id }}
{% endblock %}

{% block content %}
<table>
    <thead>
        <th>Title</th>
        <th>Author</th>
        <th>Amount</th>
        <th>Price</th>
        <th></th>
    </thead>
    {% for book in sale.books %}
        <tr>
            <td>{{ book.title }}</td>
            <td>{{ book.author }}</td>
            <td>{{ book.stock }}</td>
            <td>{{ (book.price * book.stock)|number_format(2) }} €</td>
            <td><a href="/book/{{ book.id }}">View</a></td>
        </tr>
    {% endfor %}
</table>
{% endblock %}

错误模板

我们应该添加一个非常简单的模板,在应用程序出错时显示给用户,而不是显示 PHP 错误信息。这个模板只需要使用 errorMessage 变量,它可以如下所示。保存为 views/error.twig

{% extends 'layout.twig' %}

{% block title %}
    Error
{% endblock %}

{% block content %}
    <h2>Error: {{ errorMessage }}</h2>
{% endblock %}

请注意,即使错误页面也从布局中扩展出来,因为我们希望用户在发生这种情况时能够执行其他操作。

登录模板

最后一个模板是允许用户登录的模板。这个模板与其他模板有些不同,因为它将用于两种不同的情况。在第一种情况下,用户第一次访问登录视图,因此我们需要显示表单。在第二种情况下,用户已经尝试过登录,但在登录时出现了错误,即未找到电子邮件地址。在这种情况下,我们将在模板中添加一个额外的变量 errorMessage,并添加一个条件,只有在定义了该变量时才显示其内容。您可以使用操作符 is defined 进行检查。添加以下模板作为 views/login.twig

{% extends 'layout.twig' %}

{% block title %}
    Login
{% endblock %}

{% block content %}
    {% if errorMessage is defined %}
        <strong>{{ errorMessage }}</strong>
    {% endif %}
    <form action="/login" method="post">
        <label>Email</label>
        <input type="text" name="email">
        <input type="submit">
    </form>
{% endblock %}