PHP 零基础初学者手册(三)
八、显示博客条目
条目编辑器即将问世。您可以使用它来创建新的博客条目,这些条目将保存在数据库中。您可能正在慢慢了解模型-视图-控制器(MVC)范式,但是毫无疑问,您需要更多的实践来熟练使用它。
在本章中,你将学习如何在你的index.php页面上显示所有的博客条目。在这个过程中,你会重温到目前为止你所看到或学到的东西。我还将介绍以下新主题:
- 制作新的前端控制器
- 更新您的表数据网关:向您的
Blog_Entry_Table类添加一个方法 - 遍历数据库表中的数据集
- 创建显示博客条目的模型、视图和控制器
创建公共博客首页
在本书的后面,您将学习如何将管理模块隐藏在登录名后面。你不希望每个人都能在你的博客上写新的博客条目,是吗?当你有时间的时候,admin.php将只为授权用户保留。您博客的公众形象将是index.php。创造一张大众脸。创建一个新文件index.php,如下:
<?php
//complete code for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "models/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "PHP/MySQL blog demo example";
$pageData->addCSS("css/blog.css");
$dbInfo = "mysql:host=localhost;dbname=simple_blog";
$dbUser = "root";
$dbPassword = "";
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$pageData->content .= "<h1>All is good</h1>";
$page = include_once "views/page.php";
echo $page;
保存index.php文件。请记住,在您尝试通过本地主机加载任何内容之前,MySQL 和 Apache 应该已经运行。你可以通过 XAMPP 的控制面板启动 MySQL 和 Apache。一旦 MySQL 和 Apache 开始运行,您就可以在浏览器中加载 http://localhost/blog/index . PHP,亲自检查目前为止一切正常。你应该期待在你的浏览器中看到“一切都好”。如果您的数据库凭据无效,您将在浏览器中看到一个异常。
创建博客控制器
默认情况下,您希望博客条目显示在索引页面中。你可以从最小的步骤开始,创建一个超级简单的博客控制器。在controllers文件夹中创建一个新文件blog.php:
<?php
//complete code for controllers/blog.php
return "<h1>blog entries coming soon!</h1>";
准备好初步的博客控制器后,您可以从index.php加载它来测试控制器是否已加载。更新index.php:
//partial code for index.php
//changes begin here
//comment out initial test message
//$pageData->content .= "<h1>All is good</h1>";
//include blog controller
$pageData->content .=include_once "controllers/blog.php";
//no changes below here
$page = include_once "views/page.php";
echo $page;
保存博客控制器和您的index.php。在浏览器中重新加载 http://localhost/blog/index . PHP。您应该会看到如下输出:
blog entries coming soon!
当您在浏览器中看到该输出时,您知道您的index.php(您的前端控制器)加载了您的博客控制器。
获取所有博客条目的数据
您希望能够显示在数据库中找到的所有博客条目的列表。如何显示title、entry_text的前 150 个字符,以及每个博客条目的“阅读更多”链接?
因为您决心坚持使用 MVC 方法编写干净的代码,所以您已经知道您将需要一个模型、一个视图和一个控制器。你已经有了一个基本的博客控制器。您将逐步改进现有的博客控制器。您还有一个Blog_Entry_Table类,它提供了对blog_entry数据库表的单点访问。这将是你的模型,虽然它需要更新一点,给你你想要的。您还没有列出所有条目的视图。
首先从blog_entry表中获取正确的数据。在models/Blog_Entry_Table.class.php中声明一个新方法,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//declare a new method inside the Blog_Entry_Table class
public function getAllEntries () {
$sql = "SELECT entry_id, title,
SUBSTRING(entry_text, 1, 150) AS intro
FROM blog_entry";
$statement = $this->db->prepare( $sql );
try {
$statement->execute();
} catch ( Exception $e ) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
return $statement;
}
Caution
在编写这段代码时要小心。你不能把它放在Blog_Entry_Table.class.php文件的任何地方。您应该将它写在限定类定义的两个花括号之间——在类定义内部。
也许您想知道在类定义中编写新代码意味着什么?在下面的代码示例中,可以看到在一个类定义中声明了两个方法:
//class definition starts here
class Something {
//all methods must be declared inside the class definition
public function someMethod () {
//
}
public function someOtherMethod () {
//
}
} //class definition ends here
getAllEntries()方法将返回一个PDOStatement对象,通过它你可以访问所有的博客条目,一次一个。SQL 语句涉及一些您以前没有见过的 SQL,所以最初理解起来可能有点棘手。让我们一步一步来。
使用 SQL 子字符串子句
首先要注意的是,SELECT语句从blog_entry表中选择了三列— entry_id、title和entry_text。但是您并没有选择entry_text列中的所有内容。您只选择了前 150 个字符。你可以用一个SUBSTRING()函数来实现。MySQL SUBSTRING()函数的一般语法如下:
SUBSTRING( string, start position, length )
SUBSTRING()返回字符串的一部分:子串。string 参数指示要返回哪个字符串的一部分。start position 参数指示子字符串从哪个位置开始。length参数指定要返回的子字符串的长度。在SELECT语句中,您实际上是从在entry_text字段中找到的字符串中选择前 150 个字符。
使用 SQL 别名
使用 SQL,您可以使用别名来重命名表或列。在用于选择所有博客条目的 SQL 中,您已经将子字符串重命名为intro。重命名该列并不是绝对必要的。如果没有AS子句,您仍然有一个从三列返回数据的SELECT语句。返回的数据集可能如下表所示:
| 条目 id | 标题 | SUBSTRING(entry_text,1,150) |
|---|---|---|
| one | 我的第一篇文章 | 废话。。。 |
| Two | 我的第二个条目 | 废话连篇。。。 |
我想你会同意第三列有一个奇怪的名字。代码中奇怪的名字是不好的,因为代码变得难以阅读和理解。通过使用AS关键字重命名返回的列,可以得到一个更容易阅读和理解的表,如下所示:
| 条目 id | 标题 | 介绍 |
|---|---|---|
| one | 我的第一篇文章 | 废话。。。 |
| Two | 我的第二个条目 | 废话连篇。。。 |
测试您的模型
用代码开发东西时,寻找小步骤是很重要的。如果你在两次测试之间只写一点点代码,那么早期发现编码错误就更容易了。做一点测试,看看getAllEntries()方法是否返回正确的数据。将从博客控制器使用Blog_Entry_Table对象,因为博客控制器将在为浏览器生成输出的过程中加载博客模型和博客视图。因此,博客控制器是编写测试的逻辑位置。下面是一些测试来自controllers/blog.php的getAllEntries()的代码:
<?php
//complete code for controllers/blog.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
//$entries is the PDOStatement returned from getAllEntries
$entries = $entryTable->getAllEntries();
//fetch data from the first row as a StdClass object
$oneEntry = $entries->fetchObject();
//print the object
$test = print_r( $oneEntry, true );
//return the printed object to index to see it in browser
return "<pre>$test</pre>";
使用 print_r()检查对象
前面的代码使用一个 PHP 函数print_r()来检查一个对象。您已经使用print_r()检查了两个超级全局数组$_POST和$_FILES。您可以使用相同的功能来检查对象。如果您保存controllers/blog.php并在浏览器中加载 http://localhost/blog/index . PHP,您应该会得到类似如下的输出,除非您可能在title和intro中保存了其他内容:
stdClass Object (
[entry_id] => 1
[title] => Testing title
[intro] => test
)
仔细观察输出,可以看到打印的$oneEntry是一个StdClass对象,具有三个属性:entry_id、title和intro。更大的问题是这是否是你想要的。测试成功与否?
测试成功了!$oneEntry变量应该保存一个具有这三种属性的StdClass对象。您看到的是一个表示一行数据的 PHP 对象;你在看你的博客入口模型。$oneEntry变量应该代表通过 PDO 从 MySQL 接收的一行数据。这三个属性是由来自getAllEntries()方法的SELECT语句创建的:
//the SQL statement used in Blog_Entry_Table->getAllEntries()
SELECT entry_id, title,
SUBSTRING(entry_text, 1, 150) AS intro
FROM blog_entry
所有 SQL SELECT语句都将返回一个新的临时表,该表包含在SELECT子句中指定的列。返回的表将用在FROM子句中指定的表中找到的数据填充。
getAllEntries()方法将查询您的数据库并返回一个PDOStatement对象。每次调用PDOStatement的fetchObject()方法,PDOStatement都会返回一个代表一行数据的StdClass对象。
即使是绝对的初学者也很容易理解,循序渐进地开发是明智的。显而易见,在更少的代码行中更容易发现错误。同样容易理解的是,如果一段代码有一个未检测到的错误,后续代码可能会以更多的错误结束,因为任何后续代码都必须与有原始错误的代码协作。
结果是代码在多个地方有多个错误。结果是隐藏底层代码错误的代码错误。结果就是调试困难!这就是编码错误变成 bug 的原因:当你围绕一个微小的、未被发现的错误编写代码时。
唯一的对策是分步编写代码,并在每一步之间测试代码。问题是,对于初学者来说,测试他们的代码是否有效是很困难的。初学者并不总是清楚地意识到他们的代码在做什么或者应该做什么。初学者经常在不知道会发生什么的情况下试验他们的代码——这就是初学者的感受!您预测 PHP 行为和制定小测试的能力只会通过经验和反思来提高。
Note
有一种开发方法叫做测试驱动开发。它基于为每个代码单元和单独的测试单元编写正式的测试。对于初学者来说,这几乎不是一个话题,但是知道测试驱动开发的存在对你可能是有用的。
我将继续向您展示如何创建小型、非正式的测试,以及如何在各步骤之间编写尽可能少的代码。
为所有博客条目准备视图
从控制器准备、加载和测试模型之后,就该创建视图了。这个视图应该列出所有的博客条目。博客条目的数量可能会发生变化,因此为特定数量的条目创建一个视图并不是一个好主意。
无论在数据库中找到多少博客条目,视图都会自动改变以适应这些条目。您需要用一个循环来迭代博客条目。创建一个新文件views/list-entries-html.php,并让它遍历条目,如下所示:
<?php
//complete code for views/list-entries-html.php
$entriesFound = isset( $entries );
if ( $entriesFound === false ) {
trigger_error( 'views/list-entries-html.php needs $entries' );
}
//create a <ul> element
$entriesHTML = "<ul id='blog-entries'>";
//loop through all $entries from the database
//remember each one row temporarily as $entry
//$entry will be a StdClass object with entry_id, title and intro
while ( $entry = $entries->fetchObject() ) {
$href = "index.php?page=blog&id=$entry->entry_id";
//create an <li> for each of the entries
$entriesHTML .= "<li>
<h2>$entry->title</h2>
<div>$entry->intro
<p><a href='$href'>Read more</a></p>
</div>
</li>";
}
//end the <ul>
$entriesHTML .= "</ul>";
return $entriesHTML;
在您的应用中实际使用它之前,只需花一分钟来看看这个奇妙的while循环。无论在blog_entry数据库表中找到多少行,它都会动态地创建<li>元素。如果您的数据库中有一个博客条目,就会有一个<li>。如果你的数据库中有十个博客条目,那么就会有十个<li>元素。
注意关于$entry应该有哪些属性的评论。很容易忘记哪些属性应该是可用的,所以写一个关于它的注释会很有帮助。
查找视图和模型
最后一步是将视图与模型中的数据联系起来。这不需要太多代码,但是会在浏览器中产生完全不同的输出。更新 controllers/blog.php 中的代码:
<?php
//complete code for controllers/blog.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
$entries = $entryTable->getAllEntries();
//code changes start here
//test completed - delete of comment out test code
//$oneEntry = $entries->fetchObject();
//$test = print_r($entryTable, true );
//load the view
$blogOutput = include_once "views/list-entries-html.php";
return $blogOutput;
就是这样!在您的浏览器中加载 http://localhost/blog/index . PHP,您应该会看到每个博客条目都有一个带有单独的<li>元素的<ul>。你可以访问 http://localhost/blog/admin . PHP?page=editor,创建一个新条目,然后在浏览器中重新加载 http://localhost/blog/index . PHP,以查看列出的新创建的博客条目。你的博客系统开始看起来像一个真正的博客了。
点击阅读更多很有诱惑力吧?不就是想点一下看个博客条目吗?嗯,此时点击阅读更多不会有太大影响。您还没有编写代码来显示单个博客条目的所有内容,所以当您单击时,什么也不会改变。
回应用户请求
您希望当用户单击“阅读更多”时,能够显示一个博客条目的所有内容。你必须通过主键选择单个的博客条目。它已经在代码中可用;你只需要找到它。
当用户点击阅读更多时,你的代码的哪一部分应该响应:模型、视图还是控制器?控制器!控制器是用来处理用户交互的。点击链接是一种用户交互。所以,你应该在你的博客控制器上工作来处理用户点击阅读更多。下面是controllers/blog.php中的一个小代码变化,它将输出被点击的博客条目的主键:
<?php
//complete code for controllers/blog.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
//new code starts here
$isEntryClicked = isset( $_GET['id'] );
if ($isEntryClicked ) {
//show one entry . . . soon
$entryId = $_GET['id'];
$blogOutput = "will soon show entry with entry_id = $entryId";
} else {
//list all entries
$entries = $entryTable->getAllEntries();
$blogOutput = include_once "views/list-entries-html.php";
}
//end of changes
return $blogOutput;
在浏览器中加载 http://localhost/blog/index . PHP,看看有什么变化。默认情况下,您仍然会看到所有条目的列表,但是如果您在第一个条目上单击 Read more,您会看到不同的输出,如下所示:
will soon show entry with entry_id = 1
entry_id是博客条目的主键。既然您的代码已经知道了被点击的博客条目的主键,那么显示它将是一件几乎微不足道的任务。在解决这个问题之前,我认为你应该花一点时间来反思:为什么当你点击阅读更多时,你可以在$_GET['id']中找到entry_id?
Read more 链接是用一个有点特殊的href创建的。如果您在浏览器中单击这样的链接,您可以在浏览器的地址栏中看到请求的 URL。大概是这样的:
http://localhost/blog/index.php?page=blog&id=1
注意,这里有两个编码的 URL 变量:一个叫做page,另一个叫做id。这两个 URL 变量用一个&(与号)字符分隔。一个 URL 可以保存多个 URL 变量,只要每个 URL 变量用&字符分隔。
您可能会发现,id URL 变量保存了被点击条目的entry_id属性。这就是你如何找到被点击条目的主键;它被编码在 URL 中。但是它是如何被编码的呢?答案就在views/list-entries-html.php里。额外查看一下<a>元素的href:
//partial code for views/list-entries-html.php
//no code changes – please just read the code again
while ( $entry = $entries->fetchObject() ) {
$href = "index.php?page=blog``&
$entriesHTML .= "<li>
<h2>$entry->title</h2>
<div>$entry->intro
<p><a href='$href'>read more</a></p>
</div>
</li>";
}
就在那里,显而易见。URL 变量id从相应条目的entry_id属性中获取其值。看着&可能会有点奇怪。
没什么大不了的。一个&字符被称为一个&符号,&是一个表示&符号字符的 HTML 字符实体。HTML 字符实体是代表特殊字符的短代码。
Note
你可以在 www.w3schools.com/html/html_entities.asp 阅读更多关于 HTML 实体的内容。
获取条目数据
是时候解决显示条目的小问题了。为此,您从blog_entry表中获取数据。您已经有了一个提供对blog_entry表的访问的类。您可以继续使用它,这样您就有了对表的单点访问。你可以在models/Blog_Entry_Table.class.php中声明一个新方法。
博客控制器中已经有了entry_id。因此,您可以声明一个方法,该方法将一个entry_id作为参数,并返回一个包含相应博客条目所有内容的StdClass对象,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//declare a new method inside the class code block
//do not change any existing methods
public function getEntry( $id ) {
$sql = "SELECT entry_id, title, entry_text, date_created
FROM blog_entry
WHERE entry_id = ?";
$statement = $this->db->prepare( $sql );
$data = array( $id );
try{
$statement->execute( $data );
} catch (Exception $e) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage );
}
$model = $statement->fetchObject();
return $model;
}
您可以看到这个新方法与Blog_Entry_Table类中的其他方法非常相似。首先,声明一个 SQL 字符串。接下来,使用prepare()方法将 SQL 字符串转换为PDOStatement对象,将try()转换为execute()语句。最后,从返回的 MySQL 表中获取第一行数据,并将其作为一个StdClass对象返回。
请注意预准备语句的使用。当您使用从浏览器接收的输入创建 SQL 语句时,请记住始终使用准备好的语句。$id来自一个 URL 变量,因此它应该被视为不安全的。恶意黑客可能会试图利用它并尝试 SQL 注入攻击。准备好的语句会阻止这种尝试。
您正在使用带有未命名占位符(由?字符表示)的预准备语句。要用实际值替换占位符,需要创建一个值数组,并将该数组传递给execute()方法。
您已经看到了使用预准备语句的这种方式——您用它来构建条目编辑器。随时查阅第七章刷新记忆,加深对概念的理解。
创建博客视图
要显示一个条目,您需要一个视图,一个提供 HTML 结构并将其与条目数据合并的视图。创建一个新文件views/entry-html.php,如下所示:
<?php
//complete source code for views/entry-html.php
//check if required data is available
$entryDataFound = isset( $entryData );
if ( $entryDataFound === false ) {
trigger_error('views/entry-html.php needs an $entryData object');
}
//properties available in $entry: entry_id, title, entry_text, date_created
return "<article>
<h1>$entryData->title</h1>
<div class='date'>$entryData->date_created</div>
$entryData->entry_text
</article>";
前面的代码重复了自开发投票以来您已经见过几次的方法。本质很基本:用预定义的 HTML 结构合并存储在StdClass对象中的一些数据。视图需要一个保存在变量$entryData中的StdClass对象。所以,前几行代码检查了$entryData的可用性。如果没有找到,代码将触发一个自定义错误,因此很容易找到并纠正错误。
显示条目
你已经有了模型;你看得见风景。最后一步是更新您的博客控制器。它负责从模型中获取条目数据,与条目视图共享,并将结果 HTML 字符串返回给index.php,并在那里显示。您的控制器离完成只有两行代码:
<?php
//complete code for controllers/blog.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
$isEntryClicked = isset( $_GET['id'] );
if ($isEntryClicked ) {
$entryId = $_GET['id'];
//new code begins here
$entryData = $entryTable->getEntry( $entryId );
$blogOutput = include_once "views/entry-html.php";
//end of code changes
} else {
$entries = $entryTable->getAllEntries();
$blogOutput = include_once "views/list-entries-html.php";
}
return $blogOutput;
通过加载 http://localhost/blog/index . PHP 来测试你的进度?页面=浏览器中的博客。单击 Read more,您应该可以在浏览器中看到所单击博客条目的完整内容。
你有一个功能性博客,为普通用户提供了一个index.php和一个admin.php,通过它你可以创建新的博客条目。现在是庆祝你进步的好时机。自从第一章以来,你已经走了很长的路。你知道一些关于 PHP 面向对象编程的知识。你甚至有一些设计模式的经验。你知道如何使用数据库。你不再是一个绝对的初学者。
代码味道:重复代码
你能闻到吗?你的代码散发出一股难闻的气味。这是每个程序员都知道的经典代码味道之一。随着您对代码的熟练程度的提高,这是您应该学会避免的事情之一。
Note
在 http://en.wikipedia.org/wiki/Code_smell 找到一长串典型的代码气味。
重复代码是指相同或相似的代码出现在代码中的多个位置。你已经知道去哪里找气味了吗?就在models/Blog_Entry_Table.class.php里。这里有一个例子:
//partial code for models/Blog_Entry_Table.class.php
public function getEntry( $id ){
$sql = "SELECT entry_id, title, entry_text, date_created
FROM blog_entry
WHERE entry_id = ?";
$statement = $this->db->prepare( $sql );
$data = array( $id );
try {
$statement->execute( $data );
} catch (Exception $e){
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
$model = $statement->fetchObject();
return $model;
}
在Blog_Entry_Table类中还有一些其他的方法。他们都把prepare()一个PDOStatement和try()送到execute()那。因此,在所有三种方法中,您可以找到五到七行几乎相同的代码。那就糟了!
用卷毛保持干爽
重复代码是不好的,原因有很多。一个是你简单地使用了多余的行。你的代码太长了。更长的代码更糟糕,因为更多的代码总是意味着更多的错误。更少的代码意味着更少的错误。重复代码不好的另一个原因是,您可能会修改自己编写的代码。总有一天,你会想要做出一些改变。如果您有相同或非常相似的代码分布在十个不同的方法中,您将不得不在十个不同的方法中进行相同或非常相似的代码更改。如果您将代码保存在一个单独的方法中,您将只需要更改一个方法中的代码,它将自动影响其他十个方法。
这实际上只是根据卷毛定律进行编码的另一种情况,或者至少是卷毛定律的一种变体。卷毛最初的定律是:做一件事。这个特殊的变体也许应该是:做一件事一次。对此还有另一个极客的说法:保持干燥。DRY 是首字母缩写,意思是“不要重复自己。”
用 Curly 重构
重构是在不改变代码功能的情况下改变代码的过程。这对编码人员来说是件大事。当你意识到项目需求已经超出了代码架构,或者,换句话说,当代码架构没有以一种漂亮的方式支持你的项目需要的特性时,你应该重构你的代码。
是时候重构Blog_Entry_Table类了,所以变得更加干巴巴。让我们首先将准备 SQL 语句的代码封装到一个单独的方法中。在models/Blog_Entry_Table.class.php中声明一个新方法,如下所示:
//Partial code for models/Blog_Entry_Table.class.php
//declare a new method in the Blog_Entry_Table class
//$sql argument must be an SQL string
//$data must be an array of dynamic data to use in the SQL
public function makeStatement ( $sql, $data) {
//create a PDOStatement object
$statement = $this->db->prepare( $sql );
try{
//use the dynamic data and run the query
$statement->execute( $data );
} catch (Exception $e) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
//return the PDOStatement object
return $statement;
}
声明了新方法后,您可以尝试重构一个现有方法来使用新方法。您可以从最近声明的方法开始,即getEntry()方法。
//Partial code for models/Blog_Entry_Table.class.php
//edit existing method getEntry
public function getEntry( $id ){
$sql = "SELECT entry_id, title, entry_text, date_created
FROM blog_entry
WHERE entry_id = ?";
$data = array($id);
//call the new DRY method
$statement = $this->makeStatement($sql, $data);
$model = $statement->fetchObject();
return $model;
}
让我们测试一下新方法是否如预期的那样工作。在浏览器中导航到 http://localhost/blog/index . PHP,然后单击阅读更多以查看一个博客条目。如果您可以在浏览器中看到一个条目,您就知道重构的getEntry()方法起作用了。
看看使用新的makeStatement()方法如何让你的getEntry()方法变得更短一点?您可以通过其他方法,用一个漂亮的干巴巴的解决方案替换可怕的重复代码。
有一点语法细节需要注意。看看当一个方法调用同一个类中声明的另一个方法时,如何需要$this关键字?这与使用$this来访问一个属性并没有什么不同。在这两种情况下,$this都是一个对象对自身的指称。这是用面向对象的方式说“我的”
让我们继续重构,继续讨论saveEntry()方法:
//Partial code for models/Blog_Entry_Table.class.php
//edit existing method saveEntry
public function saveEntry ( $title, $entry ) {
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( ?, ?)";
$formData = array($title, $entry);
//changes start here
//$this is the object's way of saying 'my'
//so $this->makeStatement calls makeStatement of Blog_Entry_Table
$entryStatement = $this->makeStatement( $entrySQL, $formData );
//end of changes
}
你可以相信它会完美地工作,但是为了绝对确定,你应该测试一下。加载 http://localhost/blog/admin . PHP?page=editor,并测试您仍然可以使用条目编辑器创建新的博客条目。
重构是真正快乐的工作,不是吗?你在代码中发现了一个丑陋的角落,然后把它变得美丽。你真的应该重构getAllEntries()。在models/Blog_Entry_Table.class.php中重写现有函数 g etAllEntries():
//Partial code for models/Blog_Entry_Table.class.php
//edit existing method getAllEntries
public function getAllEntries () {
$sql = "SELECT entry_id, title, SUBSTRING(entry_text, 1, 150) AS intro FROM blog_entry";
$statement = $this->makeStatement($sql);
return $statement;
}
等等,这里有个问题。你看到了吗?你用一个参数调用makeStatement(),但它需要两个参数。到目前为止,你用两个参数来调用它。第二个参数是要在 SQL 字符串中使用的数据数组。但是在这种情况下,您没有想要在 SQL 字符串中使用的数据。你没有第二个论点要传递。
有时候想用一个参数调用makeStatement(),有时候想用两个参数调用。您希望第二个参数是可选的。幸运的是,PHP 有一个非常简单的方法使参数可选。您可以简单地用默认值声明参数,如果没有传递任何内容,将使用该值。以下是如何做到这一点:
//Partial code for models/Blog_Entry_Table.class.php
//edit existing method makeStatement
//change code: declare a default value of NULL for the $data argument
public function makeStatement ( $sql,``$data = NULL
//end of code changes
$statement = $this->db->prepare( $sql );
try{
$statement->execute( $data );
} catch (Exception $e){
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage );
}
return $statement;
}
在前面的代码中,参数$data获得默认值NULL。所以,如果没有第二个参数就调用了makeStatement(),那么创建的PDOStatement对象将使用NULL执行。这意味着没有动态值会替换预准备语句中的占位符。这正是您想要的,因为 SQL 中没有该语句的占位符。
Note
你可以参考 www.w3schools.com/php/php_functions.asp 来多了解一点关于带默认值的函数参数。
在用两个参数调用makeStatement()的其他情况下,第二个参数将用于用实际值替换 SQL 占位符。在代码中使用可选参数是一个非常强大的概念。当您将几乎重复的代码封装到一个单独的方法中时,就像您刚才所做的那样,这通常会带来一个干净的解决方案。
当您测试完代码后,您可以相信它能够正常工作。将浏览器导航到 http://localhost/blog/index . PHP,确认所有条目都像以前一样列出。记住:重构就是在不改变代码功能的情况下改变代码。因此,当代码的行为与重构前完全一样时,测试就成功了。重构的唯一目的是让代码更容易被编码者使用。
使用私有访问修饰符
makeStatement()方法是Blog_Entry_Table类的脆弱成员。它只能在内部调用,并且只能由其他Blog_Entry_Table方法调用。它当然不应该在类外被调用。makeStatement()可以理解为其他Blog_Entry_Table方法使用的子方法。
现在,从外部调用它是可能的。事实上,可以从代码中的任何地方调用它。这意味着makeStatement()方法很容易被错误地使用,如果你或你的开发伙伴忘记了只能在内部调用它。正因为如此,makeStatement()几乎是一个即将发生的错误。
这很容易补救。你可以简单地使用private访问修饰符,makeStatement()只能在内部调用。没有其他代码可以调用它。下面是如何使用private访问修饰符:
//Partial code for models/Blog_Entry_Table.class.php
//edit existing method makeStatement
//code change: make it private
private function makeStatement ( $sql, $data = NULL ){
//end of code changes
$statement = $this->db->prepare( $sql );
try {
$statement->execute( $data );
} catch (Exception $e) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
return $statement;
}
有了这个小小的改变,你的Blog_Entry_Table类有了很大的提高。改进的地方是现在无法从外部调用makeStatement()。只有一个Blog_Entry_Table对象可以调用该方法。这意味着你或你的程序员同事更难以错误的方式使用Blog_Entry_Table。
你之前可能注意到了private这个关键词?我已经将它用于$db属性,没有解释它。无法从外部访问private对象属性。根据经验,您应该默认声明对象属性private。只有在特别需要时,才使用不同的访问修饰符。这样,只有对象本身可以操纵它的属性。
记住:单一责任原则,也称为卷毛定律,适用于班级。一个类应该有一个单一的目的,它的所有属性和行为都应该严格符合这个目的。有一个职责的类比有许多职责的类简单,简单的类比复杂的类更容易使用。通过使用private访问修饰符隐藏一些属性和方法,您可以呈现一个更简单、更易于使用的公共接口。所以,根据经验,默认情况下使用private,需要时使用public。
Note
还有第三个访问修饰符:protected。在 PHP 中,它类似于private,只不过它可以通过继承与子类共享。你可以在 www.killerphp.com/tutorials/object-oriented-php/ 找到一个不错的教程,涵盖了继承、访问修饰符和其他中央 OOP 主题。
您可能遇到过一些使用public对象属性的例子——每次您创建一个依赖于对象属性的视图。您一直在使用具有public属性的StdClass对象。您已经使用这样的对象来表示数据库表中的一行数据。
摘要
在这一章中,你已经创建了你的博客的公众形象。在这个过程中,您看到了更多的 SQL,并且有了一些额外的机会来理解 MVC。
您已经看到了如何重用您自己的代码。Blog_Entry_Table现在由admin.php和index.php使用。Blog_Entry_Table是一个表数据网关,它提供了从 PHP 代码到blog_entry数据库表的单点访问。
您还看到了重构如何消除代码味道,以及如何使用私有访问修饰符和可选参数来保持代码干爽。
九、删除和更新条目
事情将会是这样的:一个章节将会以某种方式改善你的博客的公众形象,下一个章节将会集中在改善秘密博客管理模块上,他们将会像那样保持交替。本章着重于改进秘密博客管理模块。
让我们继续沿着对象和类的模型-视图-控制器(MVC)路径。管理模块的第二次迭代将向您展示如何通过条目编辑器更新和删除现有条目。在改进条目管理器的过程中,您将学习如何编写小型的非正式代码测试。这些测试旨在强调开发博客的实验过程,而不仅仅是向你展示一个博客的完整代码。如果您将测试集成到开发过程中,您将提高整体代码质量并减少调试时间。
在这一章中,你还将学习如何创建 HTML 表单,将变化传达给用户。此外,您还将重温 JavaScript 渐进增强的思想。阅读本章可能会稍微改变你的视角:你不再仅仅关注你的代码是如何工作的。你将开始关注如何使用代码来设计一个为你的用户工作的系统。
创建管理链接的模型
看看你现有的编辑器在 http://localhost/blog/admin . PHP?page =编辑器。您可以清楚地看到,已经有按钮可用于保存或删除现有条目。还可以看到少了点什么。您应该单击哪里将现有条目加载到条目编辑器中,以便编辑或删除它?
我相信你能想出许多聪明的方法来完成这项任务。我建议采用一种与您刚刚在博客上所做的非常相似的方法:我建议您向管理员显示所有条目的列表。单击一个条目应该会将其加载到条目编辑器中。我采用这种方法的主要动机是它与你已经为博客条目所做的相似。再做一遍会给你一个很好的学习循环。根据我当老师的经验,重复是必不可少的——尤其是对初学者来说。
另一个优点是在Blog_Entry_Table类中已经有了一个getAllEntries()方法。您可以重用现有的方法。所以,你已经处理了模型。为了确保您可以从控制器访问条目数据,您可以编写一点代码来测试这个假设。在你的代码编辑器中打开controllers/admin/entries.php,然后完全重写,如下所示:
<?php
//complete code for controller/admin/entries.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
//get a PDOStatement object to get access to all entries
$allEntries = $entryTable->getAllEntries();
//test that you can get the first row as a StdClass object
$oneEntry = $allEntries->fetchObject();
//prepare test output
$testOutput = print_r( $oneEntry, true );
//return test output to front controller, to admin.php
return "<pre>$testOutput</pre>";
如果一切正常,当您加载 http://localhost/blog/admin . PHP 时,应该会看到一个条目的StdClass表示。page =浏览器中的条目。输出应该如下所示:
stdClass Object (
[entry_id] => 1
[title] => Testing title
[intro] => bla bla
)
这个小测试证实了条目控制器确实可以访问条目数据。您可以在浏览器中看到第一个条目的内容。
您可以从前面的输出中学到一些东西。您可以看到您的$testOutput变量保存了一个类型为StdClass的对象。你可以看到它有三个属性,叫做entry_id、title和intro。您甚至可以看到这三个属性的值。
你必须学会理解 PHP 的行为。当您构建自己的 web 应用时,理解您使用的 PHP 代码尤为重要。这是一个好机会。您可以看看产生这个输出的 PHP 代码。看看您是否能够通过代码工作,并理解创建前面的输出所涉及的每个小过程。这里有三个问题可以指导你:
- 输出如何从
controllers/admin/entries.php到达admin.php,在那里你的浏览器可以看到它? - 为什么有三个属性?为什么不是一个或四个或其他数字?
fetchObject()方法是做什么的?
花点时间研究这些问题,找到自己的答案。一旦你真正理解了这些问题的答案,你就能更好地开始开发你自己的 PHP/MySQL 项目。
显示管理链接
随着模型的尝试、测试和理解,您可以开始处理视图了。你需要的是一个可点击的博客条目标题列表。这意味着您必须遍历数据库中找到的所有条目。
你可以采取一种和你在博客上采取的方法非常相似的方法。您可以使用一个while循环,通过一个PDOStatement对象遍历数据库记录。数据库表中的每一行数据都将由一个单独的<li>元素表示。
您可以将单个博客标题包装在<a>元素中,以创建一个可点击的条目列表。在views/admin/entries-html.php中创建一个新文件,如下:
<?php
//complete code for views/admin/entries-html.php
if( isset( $allEntries ) === false ) {
trigger_error('views/admin/entries-html.php needs $allEntries');
}
$entriesAsHTML = "<ul>";
while ( $entry = $allEntries->fetchObject() ) {
//notice two URL variables are encoded in the href
$href = "admin.php?page=editor&id=$entry->entry_id";
$entriesAsHTML .= "<li><a href='$href'>$entry->title</a></li>";
}
$entriesAsHTML .= "</ul>";
return $entriesAsHTML;
仔细看看生成的href值。点击一个条目会请求一个类似admin.php?page=editor&id=2的 URL。这样,编辑器控制器将可以访问被点击条目的entry_id。
可以看到这段代码和views/list-entries-html.php中的非常相似。实际上,这也许太相似了。如果您要重构博客代码,这将是重构的一个候选。有人可能会说你没有违反卷毛定律,但你确实进入了一个灰色地带。您可以重构代码,使用相同的视图文件以稍微不同的方式列出所有条目。另一方面,这种解决方案会导致代码更加复杂。我喜欢让我的观点尽可能简单。因此,我建议您保持这段代码不变,因为它是可行的,并且不会过于复杂。
我想让你明白,组织你的代码没有正确或错误的解决方案。你可以用许多不同的方法来写一个任务的解决方案。您决定如何组织您的代码取决于您如何考虑您的代码。在前一种情况下,我必须在简短、抽象的代码或重复、简单的代码之间做出选择。比起较长的代码,我更喜欢较短的代码,但比起抽象代码,我也更喜欢简单的代码。在这个例子中,我决定支持简单代码而不是短代码。
要显示新视图,您必须更新 entries 控制器,以便它加载视图。现在,controllers/admin/entries.php中的代码输出一个StdClass对象。您这样做是为了测试相关的模型代码是否如预期的那样工作。您可以完全删除 entries 控制器中的测试代码,以便它返回您新创建的视图。更新controllers/admin/entries.php中的代码,如下所示:
<?php
//complete code for controller/admin/entries.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
$allEntries = $entryTable->getAllEntries();
$entriesAsHTML = include_once "views/admin/entries-html.php";
return $entriesAsHTML;
您现在可以测试您的代码了。加载 http://localhost/blog/admin . PHP?page=entries,您应该会看到一个格式良好的可点击博客条目标题列表。如果单击标题,将显示空条目编辑器。您可以更改这一点,这样条目编辑器将加载编辑器内显示的被点击的博客条目的内容。
用要编辑的条目填充表单
有时,条目编辑器表单应该显示空白字段,以便您可以创建新条目。在其他时候,编辑器应该显示一个现有的条目,以便可以对其进行编辑。用户应该点击一个博客标题,将它加载到编辑器中。
单击这样的博客标题会将条目的entry_id作为 URL 变量编码到 HTTP 请求中。因此,如果条目的entry_id可以作为 URL 变量使用,那么您应该将相应的条目加载到编辑器中。如果没有找到这样的 URL 变量,您应该显示一个空白编辑器。
您可以通过在编辑器视图中添加一些占位符来实现这一点。如果视图找到条目的数据,它应该显示它;否则,它应该显示空的编辑器字段。下面是更新后的views/admin/editor-html.php:
<?php
//complete code for views/admin/editor-html.php
//new code added here
//check if required data is available
$entryDataFound = isset( $entryData );
if( $entryDataFound === false ){
//default values for an empty editor
$entryData = new StdClass();
$entryData->entry_id = 0;
$entryData->title = "";
$entryData->entry_text = "";
}
//changes in existing code below
//notice object properties used in <input> and <textarea>
return "
<form method='post' action='admin.php?page=editor' id='editor'>
<input type='hidden' name='id' value='$entryData->entry_id' />
<fieldset>
<legend>New Entry Submission</legend>
<label>Title</label>
<input type='text' name='title' maxlength='150'
value='$entryData->title' />
<label>Entry</label>
<textarea name='entry'>$entryData->entry_text</textarea>
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
</fieldset>
</fieldset>
</form>";
这里要注意的主要原则是使用对象属性作为内容占位符。例如:PHP 在$entryData->entry_text中找到的任何数据都将显示在<teaxtarea>元素中。如果$entryData->entry_text是一个空字符串,那么<textarea>将为空。另一方面,如果$entryData->entry_text持有来自数据库的数据,则<textarea>将显示来自数据库的内容。
您可以在编辑器视图中看到一个新的<input>类型。有一个隐藏的输入。用户看不到隐藏的输入。您可以使用它来存储正确处理提交的表单输入所需的值。该隐藏输入将存储当前显示的条目的entry_id,如果编辑器字段为空,则为 0。
用现有条目填充空编辑器的最后一步是获取被点击条目的内容。您已经在您的Blog_Entry_Table类中声明了一个getEntry()方法。您可以在编辑器控制器中使用它。更新controllers/admin/editor.php中的代码,如下所示:
//partial code for controllers/admin/editor.php
//add this code near the end of the script
//in my example this is line 21
//introduce a new variable: get entry id from URL
$entryRequested = isset( $_GET['id'] );
//create a new if-statement
if ( $entryRequested ) {
$id = $_GET['id'];
//load model of existing entry
$entryData = $entryTable->getEntry( $id );
$entryData->entry_id = $id;
}
//no new code below
$editorOutput = include_once "views/admin/editor-html.php";
return $editorOutput;
您可以从您的数据库中获取条目数据,因为您已经将它的entry_id作为一个 URL 变量。因为您有一个entry_id,所以您可以获得该特定博客条目的所有内容。你自己看吧。保存工作,将浏览器指向 http://localhost/blog/admin . PHP?页面=条目。单击一个标题可以看到编辑器中填充了被单击条目的title和entry_text。
编辑器还不太完善。您可以在编辑器中看到任何现有条目,但不能保存任何更改。如果您单击 Save 按钮,您可以看到一个新条目将被插入,即使您试图编辑一个现有的条目。您的编辑器还不能编辑现有条目,也不能删除现有条目。删除非常容易,所以让我们先实现它。
处理条目删除
显然,应该可以删除条目。这是一个非常简单的操作,也是一个很好的起点。您将需要对编辑器的模型和控制器进行一些更改。
该模型应该有一些代码来从数据库中实际删除条目数据。该视图已经有一个删除按钮,所以这里不需要做任何更改。必须更新控制器,以便它在用户单击删除时做出反应。
从数据库中删除条目
要从数据库的blog_entry表中删除一行数据,您将需要 SQL。您还需要PDO为您的 SQL 准备一个PDOStatement。执行PDOStatement应删除已识别的条目。花一分钟思考一下。您应该在哪里编写删除条目的代码?
要删除一个条目,您需要 PHP 操作blog_entry数据库表。您已经有了一个表数据网关对象,您的Blog_Entry_Table对象。表数据网关的目的是提供对给定表的单点访问。每当您想要访问该表时,应该使用相关表数据网关对象。因此,删除blog_entry表中条目的 PHP 代码属于您的Blog_Entry_Table。您可以通过在models/Blog_Entry_Table.class.php中声明一个新方法来实现它,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//declare a new method inside the Blog_Entry_Table class
public function deleteEntry ( $id ) {
$sql = "DELETE FROM blog_entry WHERE entry_id = ?";
$data = array( $id );
$statement = $this->makeStatement( $sql, $data );
}
从数据库中删除数据是最后的操作;无法撤销!重要的是,千万不要不小心删除了不该删除的内容。幸运的是,您的数据库表设计得当:每条记录都有一个主键。这意味着如果你有一个条目的主键,每一条记录都可以被唯一地标识。由此可见,您可以通过entry_id安全地删除blog_entry,因为您可以信任要删除的正确条目。这里不会出现数据意外丢失的情况!
响应删除请求
编辑器模型准备好删除条目后,是时候用代码更新控制器了,以确定是否单击了 delete 按钮。如果单击了 Delete 按钮,控制器应该调用模型来删除相关条目。如何知道删除按钮是否被点击了?看看编辑器表单的 HTML:
//partial code for views/admin/editor-html.php
//two buttons, one name, different values
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
</fieldset>
单击编辑器表单中的任何提交按钮都会对一个名为action的 URL 变量进行编码。如果您点击了保存按钮,那么action的值为save,如果您点击了删除按钮,那么delete的值为。URL 变量action的值可以告诉你用户点击了哪个按钮。更新controllers/admin/editor.php中的代码,如下所示:
<?php
//complete code for controllers/admin/editor.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
$editorSubmitted = isset( $_POST['action'] );
if ( $editorSubmitted ) {
$buttonClicked = $_POST['action'];
$insertNewEntry = ( $buttonClicked === 'save' );
// new code: was "delete" button clicked
$deleteEntry = ( $buttonClicked === 'delete' );
//new code: get the entry id from the hidden input in editor form
$id = $_POST['id'];
if ( $insertNewEntry ) {
$title = $_POST['title'];
$entry = $_POST['entry'];
$entryTable->saveEntry( $title, $entry );
//new code here
} else if ( $deleteEntry ) {
$entryTable->deleteEntry( $id );
}
//end of new code. No changes below
}
$entryRequested = isset( $_GET['id'] );
if ( $entryRequested ) {
$id = $_GET['id'];
$entryData = $entryTable->getEntry( $id );
$entryData->entry_id = $id;
}
$editorOutput = include_once "views/admin/editor-html.php";
return $editorOutput;
测试你的作品!加载 http://localhost/blog/admin . PHP?page =浏览器中的条目;单击一个条目将其加载到编辑器中;并删除该条目。单击 Delete 按钮应该会删除条目并重新加载空编辑器。确认所选条目实际上已被删除。
有一个小细节我想提请你注意。entry_id可以在两个不同的地方找到。在代码的一部分,你在$_POST['id']中寻找entry_id;在代码的另一部分,你在$_GET['id']中寻找。有点奇怪的是,他们持有相同的价值观,但服务于不同的目的。也许考虑一下这些 URL 变量是在哪里编码的会有所帮助。
每当用户点击 http://localhost/blog/admin . PHP 上列出的博客标题时,$_GET['id']就会被编码。页面=条目。因此,$_GET['id']表示用户希望在条目编辑器中看到的博客条目的entry_id。
每当一个条目被加载到条目编辑器中时,$_POST['id']就会被编码。它表示用户刚刚在编辑器中看到的条目的entry_id。因此,$_GET['id']表示要加载的条目,而$_POST['id']表示已经加载的条目。
准备模型以更新数据库中的条目
您有一个可以创建新条目和删除现有条目的编辑器。下一步是更新您的编辑器代码,以便您可以更新现有的条目。更新数据库中的现有条目肯定是模型的工作。您可以向您的Blog_Entry_Table类添加一个updateEntry()方法,如下所示:
//Partial code for models/Blog_Entry_Table.class.php
//declare new method
public function updateEntry ( $id, $title, $entry) {
$sql = "UPDATE blog_entry
SET title = ?,
entry_text = ?
WHERE entry_id = ?";
$data = array( $title, $entry, $id );
$statement = $this->makeStatement( $sql, $data) ;
return $statement;
}
还记得民意测验中的 SQL update 语句吗?他们又来了!在前面的代码中不应该出现意外。下一个任务是在适当的时候调用新方法,即当用户点击 Save 时调用updateEntry(),同时一个现有的条目被加载到条目编辑器中。
管制员:我应该插入还是更新?
当用户单击 Save 时,应该在数据库中插入或更新显示的条目。采取哪种操作取决于条目编辑器表单中显示的条目。还记得编辑器视图中的隐藏输入吗?它存储当前显示条目的entry_id,如果编辑器字段为空,则为 0。您可以使用它来检查管理员用户是否试图在blog_entry表中插入一个新行或者更新一个现有的行。
当单击 Save 按钮时,您可以从隐藏的输入中获取值。如果编辑器为空,隐藏输入的值为 0。这意味着管理员用户刚刚创建了一个新条目。您的代码应该在blog_entry中插入一个新行。如果它包含任何其他整数,您应该用相应的entry_id更新blog_entry。处理用户交互的代码属于控制器。是时候修改一下controllers/admin/editor.php中if语句的一些代码了,如下所示:
//partial code for controllers/admin/editor.php
//this is line 6 in my script
$editorSubmitted = isset( $_POST['action'] );
if ( $editorSubmitted ) {
$buttonClicked = $_POST['action'];
//new code begins here
$save = ( $buttonClicked === 'save' );
$id = $_POST['id'];
//id id = 0 the editor was empty
//so user tries to save a new entry
$insertNewEntry = ( $save and $id === '0' );
//comment out or delete the line below
//$insertNewEntry = ( $buttonClicked === 'save' );
$deleteEntry = ($buttonClicked === 'delete');
//if $insertNewEntry is false you know that entry_id was NOT 0
//That happens when an existing entry was displayed in editor
//in other words: user tries to save an existing entry
$updateEntry = ( $save and $insertNewEntry === false );
//get title and entry data from editor form
$title = $_POST['title'];
$entry = $_POST['entry'];
if ( $insertNewEntry ) {
$entryTable->saveEntry( $title, $entry );
//new code below
} else if ( $updateEntry ){
$entryTable->updateEntry( $id, $title, $entry );
//end of code changes
} else if ( $deleteEntry ) {
$entryTable->deleteEntry( $id );
}
}
请记住,用户可能会在以下两种不同的情况下单击保存按钮:
- 用户想要保存新条目。
- 用户希望保存现有条目中的一些更改。
您的代码必须能够区分这两种用户操作。这就是为什么当显示空编辑器时,名为entry_id的隐藏输入的值为 0。因此,当用户点击 Save 按钮,并且entry_id为 0 时,用户实际上是在尝试插入一个新条目。看一下前面的代码,注意它是如何在代码中表达的。慢慢读下面的代码,让意思深入理解。不要急于完成学习过程的这一步:
//code fragments from controllers/admin/editor.php – make no changes
//$save becomes TRUE if the button with the value of 'save' was clicked
$save = ( $buttonClicked === 'save' );
//and later in the same script...
//$insertNewEntry becomes TRUE only if $save is TRUE and $id is 0\. Both conditions must be TRUE
$insertNewEntry = ( $save and $id === '0' );
记住这一点也很好,如果编辑器中显示了一个现有的条目,隐藏的entry_id将具有一个不同于 0 的值。因此,如果用户点击 Save 并且entry_id不为 0,那么用户实际上是在尝试更新一个现有的条目。一旦您的代码确定了用户请求的动作,调用saveEntry()或updateEntry()是一件很简单的事情。你可以在if-else if语句的代码中看到这一点。测试你的工作进度。您应该能够将现有条目加载到编辑器中并对其进行更改。您还应该能够创建一个新条目。
传达变更
条目编辑器“静默地”进行任何更改,它不会通知用户条目是否已保存。您可以改进编辑器,以便它向用户提供反馈。可以想象,有许多可能的方法。我建议您在保存新条目或现有条目时显示一条消息条目已保存。您可以更改代码,以便将更改清楚地传达给用户,并继续在编辑器中显示保存的条目。这些改进将需要模型、视图和控制器中的代码更改。
步骤 1:更新模型
PHP 实际上不能继续显示保存的条目,因为整个条目编辑器是随着每个新的 HTTP 请求从头开始生成的。不可能只改变 PHP 生成的 HTML 页面的一小部分。PHP 将改变一切或者什么都不改变。
Note
嗯,也许不是完全不可能。如果您将 PHP 和 JavaScript 与 AJAX 结合使用,您可以做到这一点,但这超出了本书的范围。
但总会有别的办法。您可以给用户这样的印象,即一个条目继续被加载到编辑器中。您可以简单地立即重新加载它。为了能够在编辑器中重新加载保存的条目,您需要它的entry_id。您已经知道任何更新条目的entry_id,但是您不知道刚刚插入数据库的新条目的entry_id。更新models/Blog_Entry_Table.class.php中的saveEntry()方法,使其返回已保存条目的entry_id,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//edit existing method
public function saveEntry ( $title, $entry ) {
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( ?, ?)";
$formData = array($title, $entry);
$entryStatement = $this->makeStatement( $entrySQL, $formData );
//new code below
//return the entry_id of the saved entry
return $this->db->lastInsertId();
}
注意lastInsertId()方法。这是一个标准的PDO方法,通常非常方便。它做了您所期望的事情:它返回最近插入的行的 id。它所要求的只是用一个自动递增的主键来创建这个表。
步骤 2:更新控制器
既然saveEntry()方法返回了一个entry_id,你也必须在控制器中修改一些代码,以记住返回的entry_id。在controllers/admin/editor.php上稍加改动就可以完成。您只需声明一个存储返回值的变量,如下所示:
//partial code for controllers/admin/editor.php
//this is line 16 in my script
//update existing if-statement
if ( $insertNewEntry ) {
//introduce a variable to hold the id of a saved entry
$savedEntryId = $entryTable->saveEntry( $title, $entry );
} else if ( $updateEntry ){
$entryTable->updateEntry( $id, $title, $entry );
//in case an entry was updated
//overwrite the variable with the id of the updated entry
$savedEntryId = $id;
} else if ( $deleteEntry ) {
$entryTable->deleteEntry( $id );
}
Note
这个项目正变得越来越复杂,脚本也越来越长。您可能越来越难准确地知道在哪里实现代码更改。如果你遇到困难,你可以从本书的伙伴网站 www.apress.com 下载特定章节的完整源代码。
实现了前面的代码更改后,编辑器控制器现在知道了刚刚通过条目编辑器表单提交的条目的entry_id。如果 PHP 可以找到一个名为$savedEntryID的变量,您就知道一个条目刚刚被保存或更新。
如果 PHP 找到了$savedEntryID,您应该显示一条消息告诉用户条目已经保存。您希望编辑器显示创建或更新的条目。因此,您必须获得一个StdClass对象,并带有条目数据来呈现它。但不仅如此:您希望显示一条确认消息,表明条目是否被保存或更新。你可以在控制器中,在视图加载之前在controllers/admin/editor.php中完成。
//partial code for controllers/admin/editor.php
//update existing if-statement
$entryRequested = isset( $_GET['id'] );
if ( $entryRequested ) {
$id = $_GET['id'];
$entryData = $entryTable->getEntry( $id );
$entryData->entry_id = $id;
//new code: show no message when entry is loaded initially
$entryData->message = "";
}
//new code below: an entry was saved or updated
$entrySaved = isset( $savedEntryId );
if ( $entrySaved ) {
$entryData = $entryTable->getEntry( $savedEntryId );
//display a confirmation message
$entryData->message = "Entry was saved";
}
//end of new code
$editorOutput = include_once "views/admin/editor-html.php";
return $editorOutput;
此时,您的条目编辑器应该重新加载一个保存或更新的博客条目。你可以很容易地测试它。加载 http://localhost/blog/admin . PHP?page =浏览器中的条目。单击条目标题,将条目加载到条目编辑器表单中。稍微修改一下条目,然后单击 Save。您应该看到表单被重新加载并继续显示条目。
与以前相比,这是一个改进,以前单击 Save 会导致一个空的条目编辑器表单。试着把自己想象成一个普通用户。从这个角度来看,您可能会同意,从系统获得清晰的反馈会更好,这表明条目确实被保存了。等一下。反馈信息呢?我没看到!你就快成功了。一条反馈消息保存在 PHP 内存中,名为$entryData->message。这将是一个小任务,以更新视图和显示反馈信息。
步骤 3:更新视图
此时,您从模型中获得了一个新的或更新的条目的entry_id。您的控制器确定条目是否刚刚被保存或更新,并添加适当的确认消息。最后一步是更新视图,以便确认消息与条目一起显示。更新views/admin/editor-html.php中的代码,如下所示:
<?php
//complete code for views/admin/editor-html.php
$entryDataFound = isset( $entryData );
if( $entryDataFound === false ){
//default values for an empty editor
$entryData = new StdClass();
$entryData->entry_id = 0;
$entryData->title = "";
$entryData->entry_text = "";
//notice $entryData->message is blank when the editor is empty
$entryData->message = "";
}
//notice new code below: $entryData->message is embedded
return "
<form method='post' action='admin.php?page=editor' id='editor'>
<input type='hidden' name='id' value='$entryData->entry_id'/>
<fieldset>
<legend>New Entry Submission</legend>
<label>Title</label>
<input type='text' name='title' maxlength='150' value='$entryData->title' />
<label>Entry</label>
<textarea name='entry'>$entryData->entry_text</textarea>
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
<p id='editor-message'>$entryData->message</p>
</fieldset>
</fieldset>
</form>";
通过在浏览器中加载编辑器来测试您的工作。如果您创建一个新条目并保存它,您应该看到编辑器消息条目已保存。太棒了!您的编辑器提供了清晰的反馈,您知道您的新条目已被保存。您还可以尝试加载一个现有条目,进行一些编辑上的更改并保存。同样,您应该看到条目已保存。
从用户的角度来看,您的条目编辑器现在有了很大的改进。传达变化几乎和实际做出变化一样重要。
坚持一个头衔
我们慢慢开始像关注功能一样关注可用性。既然你已经在条目编辑器上工作了,我想指出一个可用性缺陷。不需要指定标题就可以创建一个新的博客条目!出于演示的目的,我已经在这里做了(见图 9-1 )。问题是您使用博客条目标题来列出管理模块中的所有条目。
图 9-1。
A title-less blog entry listed in the admin module
问题是没有标题的博客条目不能被点击,因此,这样的条目不会被加载到条目编辑器表单中。这有点不幸,但也许不是大问题,因为创建的博客条目将显示给访问index.php的普通用户。
如果你真的想编辑博客条目,你可以通过 phpMyAdmin。你可能会说这是可用性问题,而不是功能问题。从功能上来说,编辑条目是可能的,但是如果所有的博客条目都可以通过条目编辑器来编辑,那么对用户来说会更方便、更容易。
一个解决方案是改变你的条目编辑器,所以它坚持必须为博客条目声明一个标题。您可以简单地在标题的<input>元素上添加一个required属性。这样做,如果没有声明标题,就不可能提交条目编辑器表单。更新views/admin/editor-html.php中的一行代码,如下:
//partial code for views/admin/editor-html.php
//notice the added required attribute
<input type='text' name='title' maxlength='150' value='$entryData->title'``required
在您的代码中实现这一小小的更改,并尝试保存一个没有标题的新条目。当您尝试保存时,您将看到表单未被提交。你得到的只是一个小小的警告,如图 9-2 所示。
图 9-2。
Trying to create a blog entry without a title
这个解决方案在大多数现代浏览器中都可以很好地工作,但是在撰写本文时,有一些值得注意的例外。它不能在 Safari 中运行,也不能在许多移动浏览器中运行。
Note
使用 r equired属性进行客户端表单验证是 HTML5 的新功能。在 http://caniuse.com/form-validation 可以看到目前哪些浏览器支持这样的表单验证。
通过渐进式增强提高编辑器的可用性
用一个简单的 HTML 属性来修复可用性缺陷是很棒的,因为它很容易实现。不幸的是,并不是所有的浏览器都支持required属性。但是这也很好,因为这是一个锻炼 JavaScript 的机会。
属性可以用来阻止使用 Chrome、Firefox、Opera 和 Internet Explorer 更新版本的用户提交表单。苹果的 Safari 浏览器不会阻止表单提交,即使必填字段没有填写。一个可能的解决方案是使用 JavaScript 来检测是否支持required属性。如果不是,您可以使用 JavaScript 来防止不完整的表单提交。问题是当 JavaScript 询问 Safari 是否支持required时,Safari 会声称支持。但事实上,Safari 并不真正支持required属性。
您可以考虑使用 JavaScript 来检测访问浏览器的名称。这可以通过 JavaScript 的navigator.userAgent来实现。可惜 Chrome 的navigator.userAgent字符串里有 Safari 这个词。因此,您的 JavaScript 很容易将 Chrome 误认为 Safari。更糟糕的是,用户代理字符串很容易被欺骗。所以,用户代理字符串不是很可靠。
我提出一个不同的解决方案:为所有现代浏览器提供一个 JavaScript 解决方案。继续使用完全符合 HTML5 的浏览器的required属性,禁用 JavaScript。接受禁用 JavaScript 的 Safari 用户可以创建没有标题的博客条目。
首先,你要确保你的 JavaScript 只在现代浏览器中运行。你不希望古老的浏览器被我们现代的 JavaScript 卡住。你可以使用你已经在第五章中试过的方法。为 JavaScript 文件创建一个新文件夹。你可以称之为js。在js/editor.js中创建新的 JavaScript 文件。
//complete code for js/editor.js
function init(){
console.log('your browser understands DOMContentLoaded');
}
document.addEventListener("DOMContentLoaded", init, false);
嵌入您的外部 JavaScript
前面的脚本使用了DOMContentLoaded事件对旧浏览器隐藏您的 JavaScript。但是现在还不能测试您的 JavaScript 代码。您的浏览器必须首先加载 JavaScript 文件。为此,您可以在代码编辑器中打开admin.php,并使用您在第五章中创建的Page_Data->addScript()方法嵌入 JavaScript 文件,如下所示:
//partial code for admin.php
include_once "models/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "PHP/MySQL blog demo";
$pageData->addCSS("css/blog.css");
//new code: add the Javascript file here
$pageData->addScript("js/editor.js");
//no changes below
现在,您可以保存和测试进度。打开您的浏览器及其 JavaScript 控制台。如果你用的是 Chrome,你可以用 Cmd+Alt+J 打开控制台,如果你用的是 Mac,或者用 Ctrl+Alt+J 打开。
一旦浏览器的 JavaScript 控制台打开,就可以将浏览器导航到 http://localhost/blog/admin . PHP?页面=条目。注意控制台中的小输出,如图 9-3 所示。它确认您的浏览器运行 JavaScript 代码。
图 9-3。
A console message in Chrome
如果标题为空,则显示警告
您希望系统在允许表单提交之前告诉用户输入标题。因此,您需要一个 HTML 元素来向用户输出消息。您可以在网上找到的许多聚合填充将动态地创建这样的 HTML 元素并设置其样式。我想保持 JavaScript 非常简单,所以我不想用 JavaScript 创建 HTML 元素。我建议采用一种更简单但不太优雅的方法,将一个空的 HTML 元素硬编码到视图中,如下所示:
//partial code for views/admin/editor-html.php
//notice the <p id='title-warning'> element added
<form method='post' action='admin.php?page=editor' id='editor'>
<input type='hidden' name='id' value='$entryData->entry_id' />
<fieldset>
<legend>New Entry Submission</legend>
<label>Title</label>
<input type='text' name='title' maxlength='150' value='$entryData->title' required/>
<p id='title-warning'></p>
<label>Entry</label>
您可以使用 JavaScript 来检测条目编辑器表单何时被提交。如果标题为空,您可以阻止表单提交并显示一条错误消息。只有标题不为空时,才应提交表单。以下是如何用 JavaScript 表达的:
//Complete code for js/editor.js
//declare new function
function checkTitle (event) {
var title = document.querySelector("input[name='title']");
var warning = document.querySelector("form #title-warning");
//if title is empty...
if (title.value === "") {
//preventDefault, ie don't submit the form
event.preventDefault();
//display a warning
warning.innerHTML = "*You must write a title for the entry";
}
}
//edit existing function
function init(){
var editorForm = document.querySelector("form#editor");
editorForm.addEventListener("submit", checkTitle, false);
}
document.addEventListener("DOMContentLoaded", init, false);
前面的代码可以工作,但是完全符合 HTML5 的浏览器永远不会执行您的checkTitle()函数,因为required属性会导致浏览器显示一个标准警告。因此,Chrome 等浏览器会显示标准警告,而 Safari 会显示 JavaScripted 警告。这真的不是什么大问题,但是不同浏览器的标准警告看起来不同。如果你在 Chrome 和 Firefox 中测试你的博客条目编辑器,你就可以自己看到了。如果您希望您的编辑器在多个浏览器上看起来相似,您应该编写 JavaScript 来抑制标准浏览器警告,而显示您的 JavaScript 警告。这很容易做到,只需对js/editor.js中声明的init()函数做一点小小的改动,如下所示:
//partial code for js/editor.js
//edit existing function
function init(){
var editorForm = document.querySelector("form#editor");
var title = document.querySelector("input[name='title']");
//this will prevent standard browser treatment of the required attribute
title.required = false;
editorForm.addEventListener("submit", checkTitle, false);
}
就这样!如果用户使用现代浏览器,您的代码将依赖于使用 JavaScript 的客户端验证。你可以在 Safari 中看到它的运行,如图 9-4 。使用完全符合 HTML5 的浏览器和禁用 JavaScript 的用户将使用 HTML5 required属性接受客户端验证。禁用 JavaScript 的 Safari 用户运气不好:他们将能够创建一个没有标题的博客条目。运行不支持required属性和不支持现代 JavaScript 的遗留浏览器的用户将不得不接受他们可以创建没有标题的博客条目。拥有这种过时技术的用户将获得不太理想的体验,但至少他们可以创建博客条目。
图 9-4。
Testing editor in Safari with JavaScript enabled. A user cannot submit an entry without a title
其他可用性缺陷
在这一点上,编辑器还有其他的可用性缺陷。一个是管理员必须知道一点 HTML 来以任何方式格式化条目。此外,这也是一个很大的缺点,管理员可能会发现使用图像作为博客条目的一部分相当麻烦。
似乎这还不够,指示条目是否已保存的确认消息有时会有点误导。如果您创建一个新条目并保存它,您将看到条目已保存的确认。如果您继续进一步编辑该条目,确认消息仍会显示该条目已保存,即使不再是这样。
这不太好。一般情况下,你不希望误导信息!您将在第十一章中继续提高博客条目编辑器的可用性。
编码挑战:修复可用性缺陷
我希望你注意到一个可用性缺陷,你可以尝试用 PHP 来弥补。当您将一个现有的博客条目加载到编辑器中时,<legend>元素的值将是 New Entry Submission。那是误导<legend>。当你创建一个新的博客条目时,它很合适,但是当你编辑一个现有的条目时,它就不好了!
可以用 PHP 改。一如既往,解决这个问题有许多方法。一种方法是认为问题只与视图有关。这意味着你只需要修改views/admin/editor-html.php中的代码。
如果你查看你的代码,你会发现你已经有了一个$entryData->message。你用它在不同的时间显示不同的信息。您可以采用类似的方法,声明一个$entryData->legend。您可以用新的$entryData->legend替换硬编码的<legend>值。更大的问题是给$entryData->legend分配一个合适的值。当您将现有条目加载到编辑器中时,$entryData->legend的值可能是编辑条目。当您加载一个空编辑器时,该值可能是新条目提交。挑战在于你要让它发生。
摘要
在本章中,您已经大大改进了条目编辑器。它现在可以删除或编辑现有条目,并提供一些客户端验证来增强用户体验。回到第五章,你已经了解了渐进式改进的概念,这是使用 JavaScript 的一种常见方法。在本章中,您已经看到了另一个常见的 JavaScript 任务:使用 JavaScript polyfill 来修复浏览器之间的不一致。
就代码而言,这一章主要是重复已经讨论过的原则。代码变得越来越复杂,尤其是编辑器控制器。尽管复杂性增加了,但您仍然会看到以前涵盖的原则。
但是有些事情已经发生了根本性的变化:你已经开始使用代码来设计用户体验。你不再仅仅关心让代码工作的基本问题。你开始更多地从用户的角度关注它是如何工作的。你正在编写代码来设计与用户交流的系统。这是一个你将在后续章节中进一步探讨的话题。
十、通过用户评论和搜索改进你的博客
这一章回到你博客的公众形象。此时,博客访问者可以看到您写的所有博客条目的列表,并可以单击某个条目来阅读全部内容。
现代 web 应用最重要的特性之一是允许用户通过评论系统进行交互。几乎所有现存的博客都允许其读者对条目发表评论。这增加了用户和博客作者的体验,使每个人都能够继续作者通过他的帖子发起的对话。本章向你展示了如何给博客添加评论系统。
这将是一个机会,让你加强对已经学过的一些东西的掌握,并掌握另外一些技能。在本章中,您将
- 设计用户评论表单
- 处理复杂的视图
- 开发一个控制器脚本来处理用户与表单的交互
- 为用户注释创建新的数据库表
- 使用外键将评论与博客条目相关联
- 使用继承来避免冗余代码
- 编写一个类来提供对注释表的访问
构建和显示评论条目表单
任何评论系统的基本部分是用户界面。您需要一个允许用户为博客条目撰写评论的表单。表单是一个视图。你还需要一个相应的模型和一个控制器。你必须从某个地方开始。在views/comment-form-html.php中创建一个新文件,如下:
<?php
//complete code for views/comment-form-html.php
$idIsFound = isset($entryId);
if( $idIsFound === false ) {
trigger_error('views/comments-html.php needs an $entryId');
}
return "
<form action='index.php?page=blog&id=$entryId' method='post' id='comment-form'>
<input type='hidden' name='entry-id' value='$entryId' />
<label>Your name</label>
<input type='text' name='user-name' />
<label>Your comment</label>
<textarea name='new-comment'></textarea>
<input type='submit' value='post!' />
</form>";
要显示注释表单,您需要一个注释控制器。它在早期阶段的工作只是加载视图并返回 HTML 以显示评论表单。
<?php
//complete code for controllers/comments.php
$comments = include_once "views/comment-form-html.php";
return $comments;
到目前为止,代码简短扼要,非常像前面的代码示例。您可能会注意到,评论表单还不会出现在浏览器的任何地方。注释控制器加载注释视图。但是谁应该加载评论控制器并实际显示评论表单呢?
综合观点
仅当显示完整条目时,才应显示注释表。所以,显示条目的页面也应该显示一个评论表单:这是一个由其他视图组成的复杂视图。图 10-1 显示了组合视图的简单解决方案。找出部件的层次结构,并从“主控制器”加载“次控制器”
图 10-1。
Constructing complex views
您已经在自己制作的前端控制器中完成了这项工作。以admin.php中的代码为例。它加载一个模型和一个视图来制作 HTML5 页面。生成 HTML5 页面是admin.php主要关心的问题。但是根据不同的条件,admin.php会进一步加载编辑器控制器或列表条目控制器,每个控制器都会返回一些要嵌入到生成的页面中的内容。所以,admin.php是主控制器,后续加载的控制器是次控制器。
当前的任务是显示一个博客条目和一个 HTML 表单,以便用户可以对条目进行评论。在这种情况下,显而易见,主要控制器是博客控制器。评论表单仅在博客条目的上下文中有意义。博客控制器加载博客条目。博客控制器也应该加载评论控制器。在controllers/blog.php中,你可以这样表达:
<?php
//complete code for controllers/blog.php
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
if ( $isEntryClicked ) {
$entryId = $_GET['id'];
$entryData = $entryTable->getEntry( $entryId );
$blogOutput = include_once "views/entry-html.php";
//new code here: load the comments
$blogOutput .=include_once "controllers/comments.php";
//no other changes
} else {
$entries = $entryTable->getAllEntries();
$blogOutput = include_once "views/list-entries-html.php";
}
return $blogOutput;
现在你可以开始测试你的进步了!导航至http://localhost/blog/index.php并点击阅读更多以查看显示的一个条目。在博客条目的最后,您应该会看到显示的评论表单。你现在还不能提交任何新的评论,这并不奇怪。可以看到表单完全没有样式。这不是一个美丽的景象。您可以根据自己的喜好设计评论表格。这里有一个小小的 CSS 让你开始。我已经将这些新的 CSS 规则添加到我的样式表中。
/*partial code for css/blog.css*/
form#comment-form{
margin-top:2em;
padding-top: 0.7em;
border-top:1px solid grey;
}
form#comment-form label, form#comment-form input[type='submit']{
padding-top:0.7em;
display:block;
}
在数据库中创建注释表
在开始处理注释之前,您需要有一个存储注释的地方。在simple_blog数据库中创建一个名为 comment 的表。您将使用它来存储关于评论的所有信息。您必须在该表中存储几种不同类型的信息,如下所示:
comment_id:注释的唯一标识符。这是表的主键。您可以使用AUTO_INCREMENT属性,这样新的评论就会被自动分配一个惟一的 id 号。entry_id:评论对应的博客条目的标识。该列是一个INT值。entry_id引用了另一个表中的主键。entry_id是一个所谓的外键。author:评论作者的名字。该列最多接受 75 个字符,属于VARCHAR类型。txt:实际的注释文本。我本应该将该列称为 text,但是 text 是一个保留的 SQL 关键字,所以我不能使用它。列的数据类型应该是TEXT。date:评论发布的日期,存储为TIME_STAMP。您可以为该列设置一个默认值:CURRENT_TIMESTAMP,当用户向表中添加新的注释时,它将为确切的日期和时间提供一个TIME_STAMP。
要创建这个表,在浏览器中导航到http://localhost/phpmyadmin,选择simple_blog数据库,并打开 SQL 选项卡。执行以下命令创建注释表:
CREATE TABLE comment (
comment_id INT NOT NULL AUTO_INCREMENT,
entry_id INT NOT NULL,
author VARCHAR( 75 ),
txt TEXT,
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (comment_id),
FOREIGN KEY (entry_id) REFERENCES blog_entry (entry_id)
)
评论是用户对一个特定博客条目的回应。因此,每个新评论必须与一个博客条目唯一关联。你的博客很快会显示评论,但是所有的评论不应该一直显示。当显示特定的博客条目时,应该只显示与该博客条目相关的评论。
由此可见,您的数据库必须以这样一种方式设计,以表示博客条目和评论之间的关系。数据库设计必须支持任何一个评论只能与一个博客条目相关。一个合理的解决方案是创建一个包含一列entry_id的注释表,如表 10-1 所示。这样,一个评论将知道它相关的条目的entry_id。
表 10-1。
Comment Rows Related to Specific Entries
| comment_id | 条目 id | 作者 | 文本文件(textfile) | 日期 |
|---|---|---|---|---|
| one | one | 托马斯 | […] | 2014-03-02 12:54:15 |
| Two | eight | 托马斯 | […] | 2014-03-02 13:25:41 |
| three | one | 布伦南 | […] | 2014-03-07 01:43:19 |
看一下表 10-1 中显示的填充注释表。看看任何一条评论是如何与特定条目的entry_id明确相关的?这样,每个评论都知道它与哪个博客条目相关。还要注意,一个博客条目可能有许多相关的评论。在前面的例子中,带有entry_id = 1的条目有两个注释。这种关系在关系数据库术语中称为一对多关系。
使用外键
在两个表之间建立关系时,使用外键约束是非常常见的。您可以看到在前面的 SQL 语句中是如何声明外键约束的。但这到底是为了什么?
外键约束就是约束,也就是说,它限制某些东西。当一个表字段用约束声明时,你不能随便插入任何东西。约束限制了字段可以接受的数据。
外键约束也是对外表中主键列的引用。在前面的例子中,注释表的entry_id是对blog_entry表的entry_id列的引用。
used 外键约束有助于维护数据的完整性,因为 comment 表只接受可以在blog_entry表中找到的带有entry_id的注释。换句话说,评论表将只接受与现有博客条目相关的评论。
现在您已经有了一个准备好接受评论的数据库表,每个博客条目都显示有一个表单,用于接受来自用户的新评论。在 MVC 术语中,您必须编写一个插入注释的模型并更新您的控制器,这样它才能响应表单提交。一般来说,控制器应该处理用户交互,而模型应该处理核心逻辑并操纵数据库。您可以开始编写模型或控制器的代码。从哪里开始并不重要。
构建 Comment_Table 类
从模型开始,从某个地方开始。您刚刚在数据库中创建了一个新表。您可能还记得,您已经使用了表数据网关设计模式来访问您的blog_entry表。
表数据网关提供了从 PHP 代码到一个数据库表的单点访问。让我们继续使用表数据网关模式,创建一个新的类来提供对注释表的单点访问。您可以将新类称为Comment_Table,这样它的名字就清楚地表明了它所代表的内容。
Note
表数据网关模式有一个简单的经验法则:每个表都应该有一个对应的表数据网关类。一桌一课!
如果你回想一下Blog_Entry_Table类,你会对你在新的类中需要什么有一个很好的想法。要访问数据库表,你需要一个 PDO 对象。每当你实例化一个Blog_Entry_Table时,你把一个 PDO 对象作为参数传递给构造函数。接收到的 PDO 对象存储在一个$db属性中,它似乎很好地完成了这项工作。您可以按原样重用这些代码。
拥有一个makeStatement()方法也很方便,就像在Blog_Entry_Table中一样。使用makeStatement()方法,您在Comment_Table中的代码可以保持整洁(不要重复自己),或者至少是整洁的,您很快就会看到。
为了快速轻松地开始,您可以简单地从Blog_Entry_Table类中复制相关代码,并将其粘贴到一个新的Comment_Table类中。一旦你复制了相同的代码,你就可以开始声明Comment_Table独有的新方法。在models文件夹中新建一个名为Comment_Table.class.php的文件:
<?php
//complete code for models/Comment_Table.class.php
class Comment_Table {
//code below can be copied from models/Blog_Entry_Table.class.php
private $db;
public function __construct ( $db ) {
$this->db = $db;
}
private function makeStatement ( $sql, $data = null ){
$statement = $this->db->prepare( $sql );
try{
$statement->execute( $data );
} catch (Exception $e) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
return $statement;
}
} //end of class
你记得 DRY 吗?两个不同的类有两个相同的方法和一个相同的属性是不是有点可惜?重复代码是公认的代码味,上面的代码肯定很臭!问题是你有两个在某些方面完全相同的类。
这是一个非常普遍的问题——甚至有一个名称。这类常见问题有很多解决方法。我想向您展示一个面向对象编程独有的解决方案。这个解决方案叫做继承。
通过继承保持干燥
使用继承,您可以创建一个单独的类定义,从而让代码在几个类之间共享。接下来,您可以创建单独的子类,在其中保存各个类的唯一代码,比如您的Comment_Table和Blog_Entry_Table。你可以在图 10-2 中看到这个想法。
图 10-2。
Subclasses inherit properties and methods from a parent class
图 10-2 展示了如何在一个父类中声明一些你想在多个类中共享的代码。该父母的所有孩子出生时都具有这些属性。在图 10-2 中,可以看到A_Child和Another_Child都有一个$sharedProperty和一个sharedMethod()。这些是从父类继承的。在图 10-2 中,还可以看到A_Child和Another_Child各有特殊的属性和方法。这些是在子类定义中声明的。比如只有A_Child有changeA()方法。
你可以用这个想法在Blog_Entry_Table和Comment_Table类之间共享$db和makeStatement()。你可以在图 10-3 中看到这样的架构。
图 10-3。
Using inheritance to make DRY table data gateway classes
Comment_Table和Blog_Entry_Table类都将带有从父类Table继承的$db属性和makeStatement()方法。$db和makeStatement()的代码在Table类中只需编写一次。在Comment_Table和Blog_Entry_Table中仍然可以访问$db和makeStatement(),因为它们都是同一个父对象的子对象。
是一种关系
在面向对象的术语中,父类和子类之间的关系称为 is-a 关系。一个Comment_Table就是一个Table。Table是一个通用的抽象概念,代表了数据库表的一般概念。Comment_Table是一个特定数据库表的表示。
物体之间关系的概念是你日常思维中用到的。咖啡是一种饮料。橙汁也是一种饮料。橙汁和咖啡有一些共同的特征,尽管它们明显不同。饮料是可消费液体的抽象概念。咖啡和橙汁是特殊种类的可消费液体。您可能会想出许多其他抽象概念及其具体实现的例子。面向对象编程借用了广泛使用的人类推理模式,并用它给计算机程序带来层次化的顺序。
在代码中使用继承
您可以创建如图 10-3 所示的解决方案。想要在子类之间共享的代码必须有公共的或受保护的访问修饰符。任何带有私有访问修饰符的属性或方法都不会通过继承来共享。下面是一个普通的Table类的样子。在models文件夹中创建一个名为Table.class.php的新文件:
<?php
//complete code for models/Table.class.php
class Table {
//notice protected, not private
protected $db;
public function __construct ( $db ) {
$this->db = $db;
}
//notice protected, not private
protected function makeStatement ( $sql, $data = null ){
$statement = $this->db->prepare( $sql );
try {
$statement->execute( $data );
} catch (Exception $e) {
$exceptionMessage = "<p>You tried to run this sql: $sql <p>
<p>Exception: $e</p>";
trigger_error($exceptionMessage);
}
return $statement;
}
}
protected 访问修饰符与您已经使用的 private 访问修饰符非常相似。不能从外部访问受保护的方法和属性。它们只能从类本身内部访问。但是如果在这里使用 private,makeStatement()方法和$db属性对子类不可用。
为了让这些代码对一个子类可用,比如Comment_Table,你必须在你的代码中包含Table类定义脚本,并使用关键字extends。下面是几乎完全重写的干Comment_Table应该是什么样子:
<?php
//complete code for models/Comment_Table.class.php
//include parent class definition
include_once "models/Table.class.php";
//extend current class from parent class
class Comment_Table extends Table{
//delete all previous code inside class
//it should be completely empty
}
看到Comment_Table类在这一点上仅仅是一个空代码块了吗?从代码中一点也看不出来,但是Comment_Table与生俱来就有一个makeStatement()方法和一个$db属性。你在Comment_Table课上看不到它们,但在这里可以看到。它们继承自Table类,因为使用了extends关键字;Comment_Table延伸了Table。因此,在Table中声明的所有公共和受保护的方法和属性在Comment_Table中都是直接可用的。一个Comment_Table就是一个Table。图 10-4 显示了代码架构。
图 10-4。
A Comment_Table is a special kind of table
向数据库中插入新的注释
您可以向Comment_Table添加一个方法,这样您就可以向数据库中插入新的注释。调用新方法saveComment():
<?php
//complete code for models/Comment_Table.class.php
include_once "models/Table.class.php";
class Comment_Table extends Table{
//declare a new method inside the Comment_Table class
public function saveComment ( $entryId, $author, $txt ) {
$sql = "INSERT INTO comment ( entry_id, author, txt)
VALUES (?, ?, ?)";
$data = array( $entryId, $author, $txt );
$statement = $this->makeStatement($sql, $data);
return $statement;
}
}
这段代码应该让我们明白什么是继承。看$this->makeStatement()怎么用在Comment_Table里?这是可能的,因为makeStatement()方法是从Table类继承的。Comment_Table是“天生”的,所有公共的和受保护的属性和方法都在Table类中声明。
测试 saveComment()方法
是时候测试一下Comment_Table类和它的saveComment()方法是否有效了。您可以硬编码一个初步的注释,并只是为了测试的目的而插入它。评论控制者应该负责与评论相关的用户交互,所以编辑controllers/comments.php:
<?php
//complete code for controllers/comments.php
//include class definition
include_once "models/Comment_Table.class.php";
//create a new object, pass it a PDO database connection object
$commentTable = new Comment_Table($db);
//insert a test comment for entry_id = 1
//assuming an entry_id of 1.
$commentTable->saveComment( 1, "me", "testing, testing" );
$comments = include_once "views/comment-form-html.php";
return $comments;
前面的测试代码假设您有一个entry_id = 1。如果没有,可以使用blog_entry数据库表中的另一个entry_id。当您编写完测试代码后,您可以在浏览器中导航到http://localhost/blog/index.php?page=blog并点击任何 Read more 链接来运行您的测试代码。代码应该在注释表中插入一个测试注释。为了查看你的代码是否有效,你必须加载http://localhost/phpmyadmin并浏览注释表。您应该会看到一行插入到您的注释表中,如图 10-5 所示。
图 10-5。
A row inserted into the comment table, as seen in phpMyAdmin
注意,comment_id和date字段值是自动创建的。从注释控制器接收entry_id、author和txt值,并通过saveComment()方法插入到Comment_Table对象中。
检索给定条目的所有注释
在数据库中有对entry_id = 1的评论真是太好了。但这不是一个特别有用的评论,只要博客访问者不能在他们的浏览器中看到它。要显示给定条目的所有评论,您必须获取与给定条目的entry_id相关联的所有评论的数据。您的Comment_Table应该是从 PHP 到评论数据库表的单点访问。在models/Comment_Table.class.php中声明一个新方法来获取特定entry_id的所有条目,如下所示:
//partial code for models/Comment_Table.class.php
//declare new method inside the Comment_Table class
public function getAllById ( $id ) {
$sql = "SELECT author, txt, date FROM comment
WHERE entry_id = ?
ORDER BY comment_id DESC";
$data = array($id);
$statement = $this->makeStatement($sql, $data);
return $statement;
}
花点时间阅读一下前面代码中使用的 SQL 语句。它将在SELECT author、txt和date列显示与特定entry_id相关的所有评论。请记住,entry_id被声明为外键。它是对blog_entry表主键的引用。通过entry_id,您可以明确地识别一个特定的blog_entry:您知道一个评论与哪个博客条目相关。
注释将按照DESC结束顺序中的comment_id值按时间顺序排序。换句话说,具有较高comment_id值的评论将在具有较低comment_id值的评论之前列出。comment_id列被声明为auto_incrementing,这意味着插入的第一个注释将自动获得值为 1 的comment_id。下一个注释将得到值为 2 的comment_id,依此类推。因此,最新的评论将具有最高的comment_id值。因此,上面的 SQL 语句首先列出新的注释,然后列出旧的注释。
再一次,您可以意识到makeStatement()方法是从Table类继承而来的。可以使用Comment_Table类里面的makeStatement()。实际上,您可能还记得makeStatement()是用受保护的访问修饰符声明的。这意味着makeStatement()方法只能从类内部调用。在你的系统中不可能从任何其他 PHP 脚本中调用makeStatement()。
测试 getAllById()
你已经知道了。你大概经历过很多次。每当您键入一些代码时,您可能会在代码中引入一个 bug—一个编程错误。唯一明智的做法是写几行代码,然后测试代码是否按预期运行。把虫子抓在摇篮里!
打开 comments 控制器,写一点代码来测试getAllById()方法是否正常工作。如果你用一个条目的entry_id调用getAllById(),你知道至少有一个相关的评论,你应该得到一个PDOStatement。调用PDOStatement的fetchObejct(),你应该得到一个StdClass对象,代表注释表中的一行数据。您可以重写controllers/comments.php来测试新的getAllById()方法,如下所示:
<?php
//complete code for controllers/comments.php
include_once "models/Comment_Table.class.php";
$commentTable = new Comment_Table($db);
//query database
$allComments = $commentTable->getAllById( $entryId );
//get first row as a StdClass object
$firstComment = $allComments->fetchObject();
$testOutput = print_r( $firstComment, true );
die( "<pre>$testOutput</pre>" );
//PHP dies before coming to these lines
$comments = include_once "views/comment-form-html.php";
return $comments;
浏览浏览器至http://localhost/blog/index.php?page=blog,点击阅读更多内容运行测试。如果一切顺利,您应该会看到如下所示的输出:
stdClass Object (
[author] => me
[txt] => testing, testing
[date] => 2014-03-03 10:29:33
)
如果您看到类似的输出,您已经确认getAllById()按预期工作。die()函数将杀死 PHP 进程,因此它将有效地阻止 PHP 进程。为了调试或测试,提前终止 PHP 脚本有时会很有用。
创建用于列出注释的视图
此时,你应该有一个确定的测试。您将需要一个显示所有评论的视图。在views/comments-html.php中创建一个新文件:
<?php
//complete code for views/comments-html.php
$commentsFound = isset( $allComments );
if($commentsFound === false){
trigger_error('views/comments-html.php needs $allComments' );
}
$allCommentsHTML = "<ul id='comments'>";
//iterate through all rows returned from database
while ($commentData = $allComments->fetchObject() ) {
//notice incremental concatenation operator .=
//it adds <li> elements to the <ul>
$allCommentsHTML .= "<li>
$commentData->author wrote:
<p>$commentData->txt</p>
</li>";
}
//notice incremental concatenation operator .=
//it helps close the <ul> element
$allCommentsHTML .= "</ul>";
return $allCommentsHTML;
查找视图和模型以显示注释
您可以在您的注释控制器中注释掉或者删除测试代码。显示注释的最后一步是加载视图,该视图将显示从数据库中检索到的所有注释:
<?php
//complete code for controllers/comments.php
include_once "models/Comment_Table.class.php";
$commentTable = new Comment_Table($db);
$comments = include_once "views/comment-form-html.php";
//new code starts here
$allComments = $commentTable->getAllById( $entryId );
//notice the incremental concatenation operator .=
$comments .=include_once "views/comments-html.php";
//no changes below
return $comments;
在浏览器中查看任何博客条目。您应该会看到博客条目,然后是评论表单,最后是与该博客条目相关的所有评论的列表。
通过注释表单插入注释
您有一个评论表单,评论会显示出来。你也证明了你的saveComment()方法是有效的。通过表单插入来自用户的新评论应该是一件小事。要从表单中获取输入,您需要知道表单使用什么 HTTP 方法,以及表单中使用了什么 name 属性。
对于 web 开发人员来说,使用 PHP 检索表单输入是一项非常常见的任务。这是你真的应该在这本书结束时彻底理解的东西。我可以假设你完全明白我的意思,并且你已经完全理解了这个话题,但是我宁愿给你一个机会来测试你自己的理解。看一下下面的代码,看看您是否能弄清楚注释表单使用了什么方法,以及哪些 name 属性用于输入和文本区域元素。
//partial code for views/comment-form-html.php
//make NO code changes
return "
<form action='index.php?page=blog&id=$entryId' method='post' id='comment-form'>
<input type='hidden' name='entry-id' value='$entryId' />
<label>Your name</label>
<input type='text' name='user-name' />
<label>Your comment</label>
<textarea name='new-comment'></textarea>
<input type='submit' value='post!' />
</form>";
这不是一个特别困难的挑战,是吗?表单方法是post。有一个名为user-name的<input>字段,一个名为entry-id的隐藏<input>,一个名为new-comment的<textarea>。了解了这一点,就很容易在注释控制器中编写一点 PHP 来插入来自用户的新注释,如下所示:
<?php
//complete code for controllers/comments.php
include_once "models/Comment_Table.class.php";
$commentTable = new Comment_Table($db);
//new code here
$newCommentSubmitted = isset( $_POST['new-comment'] );
if ( $newCommentSubmitted ) {
$whichEntry = $_POST['entry-id'];
$user = $_POST['user-name'];
$comment = $_POST['new-comment'];
$commentTable->saveComment( $whichEntry, $user, $comment );
}
//end of new code
$comments = include_once "views/comment-form-html.php";
$allComments = $commentTable->getAllById( $entryId );
$comments .=include_once "views/comments-html.php";
return $comments;
将浏览器指向任何博客条目,并通过表单提交新评论。您应该会看到提交的评论与您对该博客条目的任何其他评论一起列出。你的评论系统有效!
熟能生巧
自从你开始读这本书以来,你已经有了很大的进步。看看你的博客就知道了:和你在第一章写的“来自 PHP 的你好”相去甚远。最重要的发展是你的思想发生了变化。你现在知道一些 PHP 和 MySQL。在你真正熟悉你所学的东西之前,你还需要许多小时的经验。
在没有我的指导下尝试应用一些学到的经验如何?您可以更改Blog_Entry_Table类,使其从Table继承,以实践继承。这与你对Comment_Table类所做的非常相似。
或者您可以向用户提供反馈,让他们知道系统已经注册了一个新提交的评论。这将非常类似于您在条目管理器中提供的确认消息。
下面是您可以完成的另一项任务:当用户阅读没有评论的博客条目时,您可以显示如下消息:
Be the first to comment this article
你可以在风景里做。您应该让 PHP 计算从数据库返回了多少行注释。如果返回了 0 行,您知道博客条目没有评论,您应该输出一条类似前面的消息。
现在你有了一个评论系统,用户可以偶然发现一个稍微复杂的系统行为。假设一个用户阅读了一个博客条目并发表了评论。之后,用户想要返回到所有博客条目的列表。所以,她点击了浏览器的后退按钮。但是等等!这不会把用户带回索引。如果每个博客条目都有一个<a>链接回index.php不是很好吗?你能实现这样的链接吗?
你可能会发现写自己的代码会减慢你的速度。这是意料之中的事。但是,只有当你开始编写自己的代码时,你才能学会编写自己的代码——你最好尽早开始。
搜索条目
你的博客系统已经取得了很大的进步。您可能已经通过条目编辑器添加了一些条目。访问你的博客的人可能会寻找你曾经写过的一些特别的东西,但他们可能不记得你是在哪个条目写的了。你需要给他们一个选项来搜索条目。
您应该显示一个搜索表单,以便访问者可以输入搜索文本。您应该使用任何输入的搜索文本在数据库中执行搜索,并返回与输入的搜索词匹配的任何条目。
你能看到三个责任吗?您需要一个视图来显示搜索表单,另一个视图来显示搜索结果。您需要一个模型来执行数据库搜索并返回结果。您将需要一个控制器来响应用户交互。如果表单被提交,控制器应该显示搜索结果;如果没有,显示搜索表单。
搜索视图
从一小步开始总是一个好主意。您可以为搜索视图创建 HTML 表单。这没什么稀奇的。在views/search-form-html.php中创建一个新文件:
<?php
//complete code for views/search-form-html.php
return "<aside id='search-bar'>
<form method='post' action='index.php?page=search'>
<input type='search' name='search-term' />
<input type='submit' value='search'>
</form>
</aside>";
视图将显示一个 HTML 搜索表单。您可能不熟悉 input 元素上使用的搜索类型属性。搜索字段只是一种特殊的单行文本字段。搜索字段将记住用户以前的搜索词,并向用户提供一个下拉列表,建议用户以前的搜索词。
并非所有浏览器都支持搜索类型。但是任何不支持它的浏览器都会默认一个基本的<input type='text'>,所以搜索表单仍然可以工作,即使一个浏览器不支持该搜索类型。
Note
要检查哪个浏览器支持哪些 HTML5 元素,请咨询 http://caniuse.com 。
要显示搜索表单,您应该考虑希望何时显示它。在每个页面视图上显示搜索表单会很好。为了显示搜索表单而不考虑其他显示的内容,您可以从前端控制器index.php加载它。在index.php的末尾附近添加一行代码:
//partial code for index.php
//new code: include the search view before the blog controller
$pageData->content .=include_once "views/search-form-html.php";
//end of new code
$pageData->content .=include_once "controllers/blog.php";
$page = include_once "views/page.php";
echo $page;
通过在浏览器中加载http://localhost/blog/index.php来测试您的进度。您应该会看到搜索表单显示在所有博客条目列表的前面。如果你点击阅读更多,你应该看到一个特定的博客条目。请注意,搜索表单仍会显示。
响应用户搜索
显示搜索表单时,很容易编写一个搜索词并提交它。尝试一下,您将在浏览器中看不到任何结果——没有检测到搜索表单提交。最终,您会希望显示搜索结果列表。您可以从初步的搜索控制器开始。在controllers/search.php中创建一个新文件:
<?php
//complete code for controllers/search.php
return "You just searched for something";
执行搜索后,您希望从索引中加载搜索控制器。如果没有执行搜索,index.php应该加载博客控制器。注意搜索表单的action属性:
//partial code from views/search-form-html.php, don't change anything
<form method='post' action='index.php?page=search'>
每当用户提交搜索表单时,一个名为page且值为search的 URL 变量将被编码为请求的一部分。因此,当page的值为search时,您的 web 应用应该显示搜索结果。从index.php这将很容易实现:
//partial code for index.php
//new code starts here, in line 17 in my index.php
$pageRequested = isset( $_GET['page'] );
//default controller is blog
$controller = "blog";
if ( $pageRequested ) {
//if user submitted the search form
if ( $_GET['page'] === "search" ) {
//load the search by overwriting default controller
$controller = "search";
}
}
$pageData->content .=include_once "views/search-form-html.php";
//comment out or delete this line
//$pageData->content .=include_once "controllers/blog.php";
$pageData->content .=include_once "controllers/$controller.php";
//end of changes
$page = include_once "views/page.php";
echo $page;
就是这样。您的前端控制器将只显示博客或搜索页面。只有在执行搜索后,才会显示搜索页面。通过将浏览器导航到http://localhost/blog/index.php来测试进度。您应该会看到博客条目列表。现在做一些搜索。您应该会看到来自搜索控制器的一条短消息。它说,“你刚刚搜索了一些东西”,这基本上确认了搜索控制器被加载。
搜索模型
您有一个搜索表单视图,还有一个初步的搜索控制器。是时候研究一个搜索模型了,这样您就可以执行实际的搜索了。要执行搜索,您必须查询您的blog_entry数据库表。您已经有了一个Blog_Entry_Table类来提供对该表的单点访问。明智的做法是给Blog_Entry_Table添加另一个方法。在models/Blog_Entry_Table.class.php里做理智的事,如下:
//Declare new method in Blog_Entry_Table class
public function searchEntry ( $searchTerm ) {
$sql = "SELECT entry_id, title FROM blog_entry
WHERE title LIKE ?
OR entry_text LIKE ?";
$data = array( "%$searchTerm%", "%$searchTerm%" );
$statement = $this->makeStatement($sql, $data);
return $statement;
}
也许这个$data数组需要一些解释。当两个项目完全相同时,创建一个包含两个独立项目的数组是不是很奇怪?那么,SQL 中未命名占位符的数量必须与您执行的数组中的项数完全匹配。因为 SQL 中有两个占位符,所以您需要一个包含两个值的数组用于搜索。在前面的示例中,您的代码将在两个不同的表列中搜索相同的搜索词。
Note
答?表示准备好的 SQL 语句中未命名的占位符。
因为您搜索两列,所以在 SQL 语句中需要两个占位符。因为您在两列中搜索相同的搜索词,所以这两个占位符应该替换为相同的值。这就是为什么$data数组的长度应该是 2,即使这两项是相同的。
使用相似条件进行搜索
上面的 SQL 语句演示了一个我在本书前面的例子中没有用到的 SQL 关键字:LIKE。让我们看一个稍微简单一点的例子,然后逐步使用前面使用的语法:
SELECT entry_id, title FROM blog_entry WHERE title LIKE 'test'
该查询将返回一个结果集,其中包含任何blog_entry行的entry_id和title属性,以及title属性恰好为test。因此,标题为“这是一个测试”的行不会是结果的一部分。
这样的查询非常清楚地说明了LIKE是如何工作的。但作为搜索,用处不大。为了进行更有用的搜索,您可以在LIKE条件中添加一个通配符。
SELECT entry_id, title FROM blog_entry WHERE title LIKE 'test%'
%字符代表通配符。通配符代表任何东西。因此,该查询将返回标题以“test”开头,后跟任何内容的所有行的结果集。将返回一个标题为“测试是否有效”的行。标题为“这是一个测试”的行不会被返回。当然,您可以找出两个通配符如何极大地改进查询。
SELECT entry_id, title FROM blog_entry WHERE title LIKE '%test%'
这样的查询将返回一个标题为“测试是否有效”的行和一个标题为“这是一个测试”的行因此,如果您只想搜索标题中带有单词 test 的条目,前面的查询将是完美的。您可以轻松扩大搜索范围,也可以在entry_text列中查找匹配项:
SELECT entry_id, title FROM blog_entry WHERE title LIKE '%test%'
OR entry_text LIKE '%test%'
在搜索单词 test 时,这非常有用。但是你可以有把握地假设你的博客访问者会搜索其他单词或短语。因此,您需要创建一个用空占位符准备的 SQL 语句。PHP 可以检索访问者的搜索词,并将其插入占位符所在的位置。下面是用于搜索 is 的最后一条 SQL 语句:
SELECT entry_id, title FROM blog_entry WHERE title LIKE '%?%'
OR entry_text LIKE '%?%'
这个最终的 SQL 语句将为任何行返回entry_id和title,其中title或entry_text与用户提交的搜索词相匹配。
试验模型
用小步骤编码,中间穿插非正式测试!你可以在你的控制器中使用print_r()来测试你的searchEntry()方法是否有效。您可以对一个您知道应该返回结果的术语进行硬编码搜索。我知道我在博客条目中使用了单词 test,所以我对 test 进行了硬编码搜索,并期望得到一个结果。为此,我更新controllers/search.php,如下:
<?php
//complete code for controllers/search.php
//load model
include_once "models/Blog_Entry_Table.class.php";
$blogTable = new Blog_Entry_Table( $db );
//get PDOStatement object from model
$searchData = $blogTable->searchEntry( "test" );
//get first row from result set
$firstResult = $searchData->fetchObject();
//inspect first row
$searchOutput = print_r($firstResult, true);
$searchForm = include_once "views/search-form-html.php";
$searchOutput .= $searchForm;
//display all output on index.php
return $searchOutput;
保存您的代码并在浏览器中加载http://localhost/blog/index.php。现在执行一些搜索—搜索什么并不重要。当您提交搜索表单时,您应该会看到以下内容:一个打印的对象。它看起来会像下面这样:
stdClass Object ( [entry_id] => 3 [title] => This is a test )
搜索结果视图
您刚才看到的输出是一个搜索结果的表示。它证实了searchEntry()方法是有效的。为了以用户可能喜欢的方式显示搜索结果,您需要一个搜索视图。您必须在返回的数据周围包装一些 HTML。在views/search-result-html.php中创建一个新文件:
<?php
//complete code for views/search-results-html.php
$searchDataFound = isset( $searchData );
if( $searchDataFound === false ){
trigger_error('views/search-results-html.php needs $searchData');
}
$searchHTML = "<section id='search'> <p>
You searched for <em>$searchTerm</em></p><ul>";
while ( $searchRow = $searchData->fetchObject() ){
$href = "index.php?page=blog&id=$searchRow->entry_id";
$searchHTML .= "<li><a href='$href'>$searchRow->title</li>";
}
$searchHTML .= "</ul></section>";
return $searchHTML;
前面的代码假设存在一个$searchData变量。如果没有找到,将触发错误。如果找到了$searchData变量,代码将使用while语句遍历结果集。while循环将为每个匹配搜索的blog_entry创建一个<li>元素。
从控制器加载搜索结果视图
要在浏览器中显示搜索结果,您必须加载搜索结果视图。将一个视图与一个模型联系起来是一个控制器的任务。更新您的搜索控制器,更新您在controllers/search.php中的代码:
<?php
//complete code for controllers/search.php
include_once "models/Blog_Entry_Table.class.php";
$blogTable = new Blog_Entry_Table( $db );
$searchOutput = "";
if ( isset($_POST['search-term']) ){
$searchTerm = $_POST['search-term'];
$searchData = $blogTable->searchEntry( $searchTerm ) ;
$searchOutput = include_once "views/search-results-html.php";
}
//delete all the code your wrote for testing the searchEntry method
return $searchOutput;
就这样。您的前端控制器将只显示博客或搜索页面。搜索页面将显示搜索结果,即使没有匹配项。
练习:改进搜索
你注意到搜索中的一个小问题了吗?尝试搜索一个你绝对知道在数据库中没有匹配的术语。我试图搜索“#€%,显然,没有匹配。我查看了生成的 HTML 源代码,以下是我的发现:
<section id='search'>
<p>You searched for <em>"#€%</em></p>
<ul></ul>
</section>
那永远不会是有效的 HTML,也不是特别用户友好的。代码的一个小变化可以将一个单独的<li>元素附加到<ul>中。也许没有条目与您的搜索匹配。你能改变吗?您可能需要知道,如果没有结果集,一个PDOStatement将保存值FALSE。
摘要
你在这一章中涵盖了很多内容,包括学习和改进你的博客。博客访问者可能会注意到评论系统。就互动交流而言,评论系统是一个游戏改变者。突然间,你不仅仅是向全世界发布你的想法。有了评论系统,你就邀请了你和你的读者之间的双向交流。
我很高兴有机会向您展示面向对象编程的经典特征之一:继承。这是保持干燥的一个非常聪明的方法,只要你不过度使用它。
编写长继承链是可能的。你可以创建一个狗类,它是狼的孩子,是犬科动物的孩子,是四足动物的孩子,是哺乳动物的孩子。但是经验表明,浅继承关系更可取。长的遗传链会导致依赖性问题,因为狗依赖于狼的存在,而狼又依赖于犬科动物,犬科动物又依赖于四足动物。保持你的继承链短,你会没事的。
在这一章中,您将直接面对数据库设计中的外键以及使用通配符搜索数据库表。这两个都是重要的话题,作为一名 web 开发人员,你在未来一定会再次遇到。