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 %}