PHP-零基础初学者手册-全-
PHP 零基础初学者手册(全)
一、设置 PHP 开发环境
构建一个可工作的开发环境可能是令人生畏的,尤其是对于绝对的初学者来说。为了跟进本书中的项目,您需要访问 Apache、PHP 和 MySQL 的有效安装,最好是在您的本地机器上。出于速度和安全性的考虑,总是希望在本地进行测试。这样做既保护了你正在进行的工作不受开放互联网的影响,又减少了上传文件到 FTP 服务器和等待页面重新加载所花费的时间。
为什么需要 Apache、MySQL 和 PHP
PHP 是一种强大的脚本语言,可以在任何安装了 PHP 的计算机的命令行中独立运行。然而,单靠 PHP 不足以构建动态网站。要在网站上使用 PHP,您需要一个可以处理 PHP 脚本的服务器。Apache 是一个免费的 web 服务器,一旦安装在计算机上,就允许开发人员在本地测试 PHP 脚本;这使得它成为您的本地开发环境的无价之宝。
此外,用 PHP 开发的网站通常依赖于存储在数据库中的信息,因此可以快速方便地修改这些信息。这是 PHP 站点和 HTML 站点的一个显著区别。这就是关系数据库管理系统(如 MySQL)发挥作用的地方。这本书的例子依赖于 MySQL。我选择这个数据库是因为 PHP 提供了对它的原生支持,还因为 MySQL 是一个免费的开源项目。
Note
最终用户可以免费获得一个开源项目,并附带创建该软件所需的代码。用户可以自由地检查、修改和改进代码,尽管有某些附加条件。开源倡议列出了定义开源软件的十个关键条款。您可以在 www.opensource.org/docs/osd
查看这份名单。
PHP 是一种通用脚本语言,最初是由拉斯马斯·勒德尔夫在 1995 年构思的。Lerdorf 创建 PHP 是为了满足为万维网创建页面时对处理数据的简单方法的需求。
Note
PHP 的诞生源于拉斯马斯·勒德尔夫想要创建一个脚本来记录他的在线简历被访问的次数。由于他创作的剧本广受欢迎,勒多夫继续开发这种语言。随着时间的推移,其他开发人员也加入到他的行列中来开发软件。今天,PHP 是互联网上最流行的脚本语言之一。
PHP 最初代表个人主页,并作为一个免费的开源项目发布。随着时间的推移,这种语言被修改以满足用户的需求。1997 年,PHP 被重新命名为 PHP:超文本预处理器,就像现在所知道的那样。在我写这篇文章的时候,PHP 5.5.7 是当前的稳定版本。许多服务器上仍在使用 PHP 的旧版本。
PHP 如何工作
PHP 一般用作服务器端脚本语言;它特别适合创建动态网页。脚本语言的特点是集成了对数据库接口的支持,如 MySQL,这使它成为构建各种 web 应用的首选,从简单的个人网站到复杂的企业级应用。
页面加载时,浏览器会解析 HTML。浏览器根本无法处理 PHP。PHP 由服务文档的机器处理(这个机器被称为服务器)。在文档被发送到访问者的浏览器之前,文档中的所有 PHP 代码都由服务器处理。因为 PHP 是由服务器处理的,所以它是一种服务器端脚本语言。
使用 PHP,您可以创建动态网页——可以根据条件变化的网页。例如:当我登录到我的脸书帐户,我看到我的内容。当您登录您的脸书帐户时,您可以看到您的内容。我们将加载相同的资源( www.facebook.com
),但是我们将被动态地提供不同的内容。这对于 HTML web 文档来说是不可能的,因为它们是静态的,也就是说它们不能改变。每个用户都会看到完全相同的 HTML 页面。本书的其余部分探索了一些你可以用动态网页实现的事情。
PHP 是一种解释型语言,这是 PHP 程序员的另一大优势。许多编程语言要求在运行文件之前将文件编译成机器代码,这是一个非常耗时的过程。绕过编译的需要意味着您能够更快地编辑和测试代码。
因为 PHP 是服务器端语言,所以运行 PHP 脚本需要服务器。在本地机器上开发 PHP 项目意味着在本地机器上安装一个服务器。本书中的例子依靠 Apache Web 服务器来交付您的网页。
Apache 及其功能
Apache 是 web 上最流行的 Web 服务器软件;它托管了当今几乎一半的网站。Apache 是一个开源项目,可以在几乎所有可用的操作系统上运行。Apache 是一个社区驱动的项目,许多开发人员为它的进展做出了贡献。Apache 的开源根源也意味着该软件可以免费获得,这可能大大有助于 Apache 相对于其竞争对手(包括微软的 IIS 和谷歌的 GWS 等)的压倒性人气。
在 Apache HTTP Server 项目网站( http://httpd.apache.org
)上,Apache HTTP Server 被描述为“为包括 UNIX 和 Windows NT 在内的现代操作系统开发和维护开源 HTTP 服务器的努力。这个项目的目标是提供一个安全、高效、可扩展的服务器,提供与当前 HTTP 标准同步的 HTTP 服务。”
与所有 web 服务器一样,Apache 接受 HTTP 请求并提供 HTTP 响应。万维网是建立在 web 服务器上的,您访问的每个网站都展示了 Web 服务器的功能。我已经提到过,虽然 HTML 可以由 web 浏览器处理,但是服务器端脚本语言(如 PHP)必须由 web 服务器处理。由于 Apache 非常受欢迎,所以在本书中它被用于测试目的。
用 MySQL 存储信息
MySQL 是一个关系数据库管理系统(RDBMS)。本质上,这意味着 MySQL 允许用户在基于表的结构中存储信息,使用行和列来组织不同的数据。还有许多其他的关系数据库管理系统。本书中的例子依靠 MySQL 来存储您将在 PHP 脚本中使用的信息,从博客条目到管理员信息。这种方法有很大的优势,我们将详细探讨。
Note
Blog 是 weblog 的缩写,是个人或企业制作的在线日志。
安装 PHP、Apache 和 MySQL
对于新程序员来说,最大的障碍之一是起步。在编写第一行 PHP 代码之前,您必须下载 Apache 和 PHP,通常还有 MySQL,然后阅读那些充满您可能还不理解的技术术语的安装说明。这种经历会让许多开发人员感到不自信,怀疑他们是否正确安装了所需的软件。
就我自己而言,这个障碍让我几个月都没有学习编程,尽管我非常想超越普通的 ole HTML。在成功运行我的第一个 PHP 命令之前,我不止一次,而是三次尝试在我的本地机器上安装 PHP,但都没有成功。
幸运的是,开发社区已经对新手开发人员的挫折做出了回应,提供了几个选项,无论您是为 Windows、Mac 还是 Linux 机器创建应用,都可以免去设置开发环境的所有痛苦。这些选项包括设置 Apache、MySQL 和 PHP 安装的一体化解决方案。
最常见的一体化解决方案是一个名为 XAMPP ( www.apachefriends.org/en/xampp.html
)的程序,它将 Apache、MySQL、PHP 和其他一些有用的工具整合到一个简单的安装程序中。XAMPP 是免费的,可用于 Windows、Mac 和 Linux。本书假设您将使用它作为您的开发环境。
Note
默认情况下,大多数 Linux 发行版都附带了 LAMP 栈(特定于 Linux 的软件,功能类似于 XAMPP)。默认情况下,某些版本的 Mac OS X 也会安装 PHP 和 Apache。
安装 XAMPP
足够的背景。现在,您可以在开发机器上安装 XAMPP 了。这个过程大约需要五分钟,而且完全无痛。
第一步:下载 XAMPP
你的首要任务是获得一份 XAMPP 软件。前往 XAMPP 网站( www.apachefriends.org/en/xampp.html
)下载最新版本(发布时为 1.8.3)。
步骤 2:打开安装程序,按照说明进行操作
下载 XAMPP 后,找到新下载的安装程序并运行它。你应该会看到一个类似于图 1-1 所示的屏幕。
Note
本书中使用的所有截图都是在运行 Mac OS X 10.6.8 的电脑上拍摄的。如果您使用不同的操作系统,您的安装可能会略有不同。用于 Windows 的 XAMPP 提供了额外的选项,例如安装 Apache、MySQL 和 Filezilla(一种 FTP 服务器)作为服务的能力。这是不必要的,会消耗计算机资源,即使它们没有被使用,所以最好关闭这些服务。此外,Windows 用户应该保留c:\xampp
安装目录,以便更容易地理解本书的示例。
图 1-1。
The introductory screen for the XAMPP installer on Mac OS X
点击下一步按钮进入下一个屏幕(见图 1-2 ),在这里您可以选择要安装的组件。使用默认选择即可。XAMPP 安装程序将引导您完成安装过程。图 1-3 至 1-5 显示了剩余的步骤。
图 1-5。
When you’re ready to install, click Next
图 1-4。
You don’t have to learn more about BitNami at this point
图 1-3。
XAMPP installation directory
图 1-2。
Select components to install
安装需要一两分钟才能完成,此时安装人员会显示最后一个屏幕(见图 1-6 ),确认安装成功。
图 1-6。
Installation is complete
步骤 3:测试 XAMPP 以确保正确安装
到目前为止,您已经使用 XAMPP 向导安装了 Apache、PHP 和 MySQL。下一步是激活 Apache,这样就可以写一些 PHP 了。
打开 XAMPP 控制面板
您可以通过导航到新安装的 XAMPP 文件夹并打开 XAMPP 管理器来激活刚安装的应用(参见图 1-7 )。
Note
打开 XAMPP 控制面板时,可能会提示您输入密码。这对服务本身没有影响,也不应该影响本书所涉及的项目。
在开发机器上激活 Apache、PHP 和 MySQL 就像在 XAMPP 管理器中点击 Apache 旁边的 Start 按钮一样简单。系统可能会提示您确认是否允许服务器在您的计算机上运行,并且可能会要求您输入系统密码。这样做之后,状态应该表明 Apache 正在运行,如图 1-7 所示。
图 1-7。
The XAMPP manager shows that the local Apache Web Server is running Note
在 XAMPP 有一个 FTP(文件传输协议)选项。FTP 提供了一种在网络间移动文件的方法。本书中的例子不需要这个选项,所以没有必要在 XAMPP 控制面板中激活它。前几章甚至不需要 MySQL 数据库。
如果 Apache 没有运行呢?
有时,XAMPP Apache 服务器不运行,即使你试图启动它。最常见的问题是它与使用您电脑上相同端口的其他服务冲突。检查您是否运行了 Skype 或 Messenger 或其他类似的网络服务。完全关闭 Skype,如果你幸运的话,你的 Apache 还能运行。
如果它仍然不运行,你可以求助于互联网。XAMPP 在线社区非常有用,大多数安装问题已经在位于 https://community.apachefriends.org/f/viewforum.php?f=34
的 Apache Friends 论坛中得到解决。您也可以在 http://stackoverflow.com/
转而搜索或询问。
验证 Apache 和 PHP 正在运行
检查 Apache 是否在您的开发机器上正常运行是一件简单的事情。只需打开浏览器,进入以下地址:http://localhost。如果一切正常,你将被重定向到http://localhost/xampp/splash.php
(见图 1-8 )。
图 1-8。
Check in your browser that your Apache Web Server is running
如果这个屏幕加载,您已经成功地在开发机器上安装了 Apache 和 PHP!地址 http://localhost 是您正在使用的当前电脑的别名。使用 XAMPP 时,在浏览器中导航到 http://localhost 会告诉服务器打开 web 根目录。这是包含在 XAMPP 安装目录中的htdocs
文件夹。使用服务器访问本地计算机上的 web 根目录的另一种方法是导航到 IP 地址(分配给连接到计算机网络的任何设备的数字标识符),它充当所有 HTTP 服务器的“主”地址:http://127.0.0.1。
选择 PHP 编辑器
您的开发机器现在正在运行 PHP 编程所需的所有程序。下一步是决定如何编写脚本。PHP 脚本是基于文本的,所以您有无数的选择,从简单的Notepad.exe
和文本编辑程序到高度专业化的集成开发环境(ide)。
大多数有经验的 PHP 开发人员都使用 IDE,因为它有很多好处。许多初学者在使用 IDE 时会有一些困难,也许是因为 IDE 有太多的特性,初学者会感到困惑。
您可能可以使用任何您用来编写 HTML 和 CSS 的程序来编写 PHP 代码。一个好的编辑应该有一些特点。
- 语法突出显示:这是识别编程语言中某些单词的能力,例如变量、控制结构和各种其他特殊文本。这个特殊的文本被突出显示或以其他方式区分,以便更容易地扫描您的代码。
- 内置函数引用:当您输入函数或对象方法的名称时,该特性会显示可用的参数、声明该函数的文件、该函数功能的简短描述以及参数和返回值的更深入的细分。事实证明,在处理大型库时,这个特性是非常宝贵的,它可以省去您查阅 PHP 手册来检查函数的参数顺序或可接受的参数的麻烦。
- 自动完成特性:该特性将可用的 PHP 关键字添加到一个下拉列表中,允许您快速、轻松地从列表中选择想要的关键字,省去了您每次记忆和键入关键字的努力。说到工作效率,每一秒都很重要,这个特性是节省时间的好方法。
- 代码折叠:这个特性允许您折叠代码片段,使您的工作空间整洁,代码易于导航。
- 自动缩进:自动缩进你写的代码,保持一致。这种缩进的代码对于人类读者来说更容易阅读,因为缩进表明了代码块之间的关系。
- 内置 ftp:当你想在万维网上发布你的项目时,你需要 ftp 把你的 PHP 文件上传到一个在线的 web 服务器上。您可以使用独立的 ftp 程序,但是如果它内置在您的 IDE 中,您只需单击一下就可以上传整个项目。
您有许多好的 ide 和编辑器可供选择。NetBeans 和 Eclipse PDT 都是优秀的免费 ide。如果您想习惯专业开发人员经常使用的工具,请尝试一种或两种方法。初学者可能会发现从简单的编辑器开始更容易。我真的很喜欢 Komodo Edit:它和任何编辑器一样容易使用,并且它提供了刚刚列出的开箱即用的大多数功能,包括 PHP 和许多其他语言的优秀自动完成功能。下面是刚才提到的三个 PHP 编辑器的下载链接:
- 从
https://netbeans.org/downloads/
获取 NetBeans。 - 从
http://projects.eclipse.org/projects/tools.pdt
获取 Eclipse PDT。 - 从
www.activestate.com/komodo-edit/downloads
获取科莫多编辑。
我将使用科莫多编辑这本书的例子。使用任何其他编辑器都应该不难理解这些示例。如果您决定使用 ide,您将必须查阅在线文档,以了解如何在您选择的 IDE 中建立新项目。
创建你的第一个 PHP 文件
一切都设置好并正常运行后,是时候冒险编写您的第一个 PHP 脚本了。作为一种服务器端脚本语言,PHP 需要 Apache 之类的 web 服务器才能运行。您已经在本地计算机上安装了 Apache,所以您的系统已经准备好了。
Apache 将解释保存在名为htdocs
的文件夹中的任何 PHP 文件。你可以在XAMPP/xamppfiles/htdocs
的 XAMPP 装置中找到它。
您很快就会创建许多 PHP 文件,所以保持它们的有序是个好主意。在htdocs
里面新建一个文件夹,命名为ch1
。
现在打开 Komodo Edit,或者任何你决定使用的编辑器或 IDE。从科莫多编辑,您可以选择文件➤新➤新文件从主菜单。在新文件中,编写以下内容:
<?php
echo "Hello from PHP";
在 Komodo Edit 中,选择文件➤保存以查看文件保存对话框(图 1-9 )。将新文件另存为test.php
,在htdocs/ch1
中;将格式设置为所有文件;然后单击保存。
图 1-9。
The Save As dialog box from Komodo Edit
运行您的第一个 PHP 脚本
下一步是让 Apache 处理您的 PHP 脚本。如果您通过浏览器请求脚本,这将自动发生。因此,打开一个 web 浏览器,导航到 http://localhost/ch1/test.php,您会惊奇地在浏览器中看到 php 生成的输出(图 1-10 )。您已经成功地创建并执行了您的第一个 PHP 脚本!
图 1-10。
Seeing the output from test.php
in the Chrome web browser
摘要
在这一章中,你学习了一些关于 PHP、MySQL 和 Apache 的知识。您发现了它们是什么,以及它们在动态网站的开发中扮演什么角色。通过安装 XAMPP 和科莫多编辑软件,你还学会了在本地计算机上安装一个功能齐全的开发环境的简单快捷的方法。
在下一章中,您将学习 PHP 的一个小而有效的子集,包括变量、对象和一些本地语言结构和语句。几乎你学到的所有东西都将在你的新开发环境中进行测试,所以保持 XAMPP 的 Apache 服务器开放和运行。
二、理解 PHP 语言基础
到目前为止,您已经绕过了创建开发环境的旧的、繁琐的方法,现在可以开始编写代码了。
但是你从哪里开始呢?在这一章中,我将介绍开始使用 PHP 创建强大的动态 web 应用需要遵循的步骤。你还将开始发展创建博客所需的基本技能。此外,您将学习如何完成几项任务,包括如何执行以下操作:
- 在网页中嵌入 PHP
- 将数据作为输出发送到浏览器
- 在代码中添加注释
- 使用变量
- 处理 PHP 错误
- 创建 HTML5 模板
- 使用对象
- 连接字符串
- 用
$_GET
超级全局变量访问 URL 变量 - 声明类定义
- 嵌入动态 CSS
在这一章结束时,你将会看到一些基本的 PHP 允许你创建、存储、操作和输出数据。您将使用这些技能开发一个个人投资组合网站的基本版本。
Note
本章讨论了 PHP 语言的基本方面,但不是全部细节。为了澄清,更多的例子,或者为了概念强化,访问 PHP 手册的 www.php.net/manual/en/
并在显示“在函数列表中搜索 _ _ _ _ _ _ _ _ _ 的字段中搜索函数或者,您可以通过导航到 http://php.net/function_name
来访问许多 PHP 函数的信息。不要忘记阅读评论,因为你的许多程序员同事在他们的评论中提供了见解、技巧,甚至附加功能。
嵌入 PHP 脚本
在第一章中,当我谈到 Apache 和 web 服务器时,我提到了服务器如何在将文件发送到浏览器之前处理 PHP 文件。但是您可能会好奇服务器是如何知道在哪里寻找 PHP 的。
默认情况下,服务器只在以扩展名.php
结尾的文件中寻找 PHP。但是一个.php
文件可能包含不属于 PHP 脚本的元素,搜索整个文件寻找潜在的脚本是令人困惑和耗费资源的。为了解决这个问题,所有的 PHP 脚本都需要包含 PHP 分隔符。要开始一个 PHP 脚本,您需要包含开始分隔符<?php
并开始编码。最后,只需在脚本末尾添加?>
即可。这些分隔符之外的任何内容都将被视为 HTML 或纯文本。
您可以在实践中看到这一点。首先在/xampp/htdocs/
中创建一个新文件夹ch2
。接下来,用 Komodo Edit 创建一个新文件test.php
。编写以下代码:
<p>Static Text</p>
<?php
echo "<p>This text was generated by PHP!</p>";
?>
<p>This text was not.</p>
保存文件,在浏览器中导航到 http://localhost/ch2/test.php,您应该在浏览器中看到以下输出:
Static Text
This text was generated by PHP!
This text was not.
如您所见,PHP 分隔符内的文本被作为脚本处理,但外部的文本被呈现为常规的 HTML。一个页面中可以包含多少 PHP 代码块没有限制,所以下面的代码片段完全有效:
<?php
echo "<p>This is some text.</p>";
?>
<p>Some of this text is static, <?php echo "but this sure isn't!"; ?></p>
<?php echo "<p>"; ?>
This text is enclosed in paragraph tags that were generated by PHP.
<?php echo "</p>"; ?>
前面的代码片段将以下内容输出到浏览器:
This is some text.
Some of this text is static, but this sure isn't!
This text is enclosed in paragraph tags that were generated by PHP.
如果你写了一个 PHP 脚本,除了 PHP 什么都没有,你不需要结束 PHP 分隔符。如果你要在文件中写一些不是 PHP 的东西,你只需要标记一个 PHP 代码块的结尾。
使用回声
额外看看前面代码示例中echo
的用法。PHP 的echo
是一个所谓的语言构造——PHP 的基本语法单位。echo
语句可能是从 PHP 向浏览器输出文本的最常见方法。这就是echo
所做的一切。它将输出发送到浏览器。
请注意,在前面的代码示例中,输出字符串用双引号分隔。第一个双引号表示字符串的开始。第二个双引号标记要输出的字符串的结尾。在 PHP 中,你必须分隔你在代码中使用的任何字符串。字符串分隔符告诉 PHP 一个字符串何时开始和结束,这是 PHP 需要知道的,以便处理您的代码。
Note
字符串是“文本”的一个怪词因为计算机不是人,它不会真的看到文本,更不会看到文字。他们看到字符串。
什么是变量?
变量是一个关键字或短语,用作存储在系统内存中的值的标识符。这很有用,因为它允许我们编写对变量值执行一组操作的程序,这意味着您可以简单地通过更改变量来更改程序的输出,而不是更改程序本身。
将值存储在变量中
在变量中存储一个值非常简单。在一行中,您可以声明一个新变量并为其赋值:
<?php
$myName = "Thomas";
$friendsName = "Brennan";
echo "<p>I am $myName and I have a friend called $friendsName.</p>";
如果您将前面几行输入到您的test.php
文件中,并将其加载到您的浏览器中,您应该会看到如下输出:
I am Thomas and I have a friend called Brennan.
也许您会注意到,前面的代码只包含 PHP。因此,没有必要用 PHP 分隔符来标记 PHP 代码块的结尾。喜欢的话可以在末尾加?>
;没什么区别。
变量是一个占位符
变量在编程中被广泛使用。这是一个你必须理解的基本概念。从前面的例子中可以学到重要的一课。当您阅读 PHP 代码时,您会看到变量名:
echo "<p>I am $myName and I have a friend called $friendsName.</p>";
您可以在浏览器中看到 PHP 的输出。您可以看到 PHP 用字符串值替换了变量名。比如看到$myName
,PHP 看到的是托马斯。看到$friendsName
,PHP 看到的是布伦南。
变量是特定值的占位符。PHP 甚至不会注意到这个变量;它看到存储在里面的值。打个比方,你可以把一个变量理解为一个容器,比如一个杯子。我的电脑旁边有一个杯子,我可以在里面放各种各样的东西:咖啡、一支铅笔或一些零钱。PHP 变量就是这样。PHP 看到的是包含的内容,而不是容器。
Note
用技术术语来说,PHP 变量是通过值传递的,而不是通过引用传递的。
有效的 PHP 变量名
在 PHP 中,所有变量都必须以美元符号字符($)开头。对于有效的变量名还有一些进一步的限制,但是如果您仅仅使用字母字符,您将不会遇到无效变量名的问题。因此,避免空白字符、数字和特殊字符,如!"#€%&/.
Note
你可以在变量名中使用数字,但不能在初始位置使用。因此,$1a
是一个无效的变量名,而$a1
是完全有效的。
显示 PHP 错误
在学习 PHP 的过程中,您肯定会犯一些错误。当你写了一些错误的 PHP 时,很容易认为你做了坏事。从某种意义上说,这当然是不好的。您可能更喜欢从一开始就编写完美的 PHP。
从另一个意义上说,错误是一件非常好的事情。许多这样的错误提供了一个学习的机会。如果你真正理解了错误的原因,你就不太可能重复它,即使你重复了,如果你理解了,你也可以很容易地纠正错误。
PHP 错误消息并不总是显示出来——这取决于您的开发环境是如何设置的。如果你在脚本的开头写了下面两行 PHP 代码,所有的错误信息都会显示出来。让我们产生一个错误:
<?php
//these two lines tell PHP to show errors in the browser
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
//here comes the error
echo "This string never ends;
你看到错误了吗?只有一个字符串分隔符。要编写有效的 PHP,必须用字符串分隔符将字符串括起来,例如双引号。在前面的例子中,缺少结束分隔符,所以 PHP 看不到输出结束的位置。如果运行该代码,您将在浏览器中看到一条错误消息,如下所示:
Parse error:``syntax error, unexpected $end, expecting T_VARIABLE or T_DOLLAR_OPEN_CURLY_BRACES or T_CURLY_OPEN in``/Applications/XAMPP/xamppfiles/htdocs/ch2/test.php``on line
错误消息是友好的,但并不总是像您希望的那样精确。当 PHP 无法处理您的代码时,就会触发一个错误。PHP 将对问题可能是什么做出有根据的猜测。在前面的例子中,PHP 在第 4 行遇到了“意外结束”。你的剧本里有一个“bug”。请通过添加缺少的双引号来调试脚本。
我建议你养成强制显示错误信息的习惯,并尝试阅读你遇到的所有错误信息。如果你遇到一个你不理解的错误信息,你可以在网上搜索解释。像 www.stackoverflow.com
这样的网站很可能会对你的错误信息给出解释。
用 PHP 创建 HTML5 页面
PHP 是创建动态 HTML 页面的绝佳语言。只需一点点 PHP,您就可以创建一个有效的 HTML5 页面,在内存中包含可变内容,并让 PHP 将创建的页面输出到浏览器。让我们为个人投资组合网站制作一个基本框架。在XAMPP/htdocs/ch2
中创建一个名为index.php
的新 PHP 文件:
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$title = "Test title";
$content = "<h1>Hello World</h1>";
$page = "
<!DOCTYPE html>
<html>
<head>
<title>$title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'/>
</head>
<body>
$content
</body>
</html>";
echo $page;
如果您在浏览器中保存并加载 http://localhost/ch2/index.php,您应该会看到一个格式良好的 HTML5 页面,带有一个标题和一个标题。检查 PHP 生成的 HTML 页面的源代码是一个好习惯。这样做,您应该会看到变量已经被 PHP 替换为相应的值。HTML 源代码应该如下所示:
<!DOCTYPE html>
<html>
<head>
<title>Test title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
</head>
<body>
Hello World</h1>
</body>
</html>
包括一个简单的页面模板
用 PHP 创建一个有效的 HTML5 页面是一个非常非常普通的任务。理解前面的代码应该没什么问题。让我们尝试以一种更容易在其他项目中重用的方式创建相同的输出。如果您可以在其他项目中重用您的代码,您就可以更快、更有效地开发解决方案。让我们将 HTML5 页面模板保存在一个单独的文件中。
在现有的 PHP 项目中创建一个名为templates
的新文件夹。在 templates 文件夹中创建一个名为page.php
的新 PHP 文件,如下所示:
<?php
return "<!DOCTYPE html>
<html>
<head>
<title>$title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'/>
</head>
<body>
$content
</body>
</html>";
返回值
PHP 中的return
语句非常有用。它只是停止脚本的执行。将返回紧跟在return
语句之后的任何值。在前面的示例中,将返回一个有效的 HTML5 页面。
包括模板
要使用索引中的模板,您必须将脚本加载到 PHP 的内存中。您可以用另一个 PHP 语句来做这件事:include_once
。更新您的index.php
文件,如下所示:
<?php
//complete code for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$title = "Test title";
$content = "<h1>Hello World</h1>";
//indicate the relative path to the file to include
$page = include_once "templates/page.php";
echo $page;
上述代码的输出将与您第一次创建页面时的输出相同。没有功能上的变化,但是在代码架构上有一些美学上的变化。可重用的页面模板现在保存在一个单独的文件中。需要时,模板包含在index.php
中。我们实际上是将代码的不同部分拆分到不同的文件中。结果是更多的代码可以在其他项目中重用。这个分离不同部分的过程也称为关注点分离。
注释您的代码
在代码中写注释对你的学习过程很有帮助。这样的注释应该提醒你代码做了什么以及为什么。用你自己的语言解释代码会加快你的学习过程。此外,如果您发现自己与一组开发人员一起工作,代码注释将帮助您有效地协作,因为您可以将注释作为代码注释写给共同开发人员。
块注释和单行注释
您可以在代码中编写注释。注释可以提醒你和其他阅读你的代码的人,代码的不同部分是做什么的。您必须清楚地界定注释,这样 PHP 就不会试图将注释解释为实际的产品代码。你应该知道用 PHP 编写代码注释的两种方式:块注释和单行注释。
<?php
//this is a single-line comment
/*
This is a comment block
It may span across
several lines
*/
避免命名冲突
你很快就会发现自己用数百行代码编写 PHP 项目。您将需要许多变量,每个变量都必须有唯一且有意义的名称。您必须避免命名冲突,如下例所示:
<?php
$title = "Welcome to my blog";
/*
hundreds lines of code later
*/
$title = "Web developer";
看到问题了吗?最初,名为$title
的变量用于指示 HTML 页面的<title>
元素的值。很久以后,在同一个系统中,一个名为$title
的变量被用来存储职位名称。具有这种变量名的系统易受攻击。使用该变量时,您可能会看到不需要的系统行为。更好的解决方案是清楚地指出$title
的上下文。一种方法是使用对象。
<?php
$pageData = new StdClass();
$pageData->title = "Welcome to my blog";
/*
hundreds lines of code later
*/
$jobData = new StdClass();
$jobData->title = "Web developer";
您可以通过使用 PHP 的 native StdClass
创建一个新的标准 PHP 对象。PHP 对象就像一个变量,它可以存储值。一个普通的 PHP 变量可以存储一个值。一个对象可以存储任意多的值。每个单独的值都可以存储为唯一的对象特性。
在前面的代码示例中,您可以看到两个不同的对象,每个对象都有一个 title 属性。应该清楚的是,$pageData->title
不同于$jobData->title
,即使两个属性都被命名为title
。
该对象提供了一个上下文,这将使您更容易在代码中的正确位置使用正确的标题。您可以使用对象将代码组织成有意义的单元。你可以说一个对象和它的属性很像一个文件夹和里面的文件。
Note
对象的意义远不止于此——远不止于此。在代码中使用对象是处理系统复杂性的事实标准,不会在代码中引入不必要的复杂性。在整本书中,你会学到更多关于用对象编程的知识。
对象运算符
通过提供清晰的上下文,对象可以用作属性的命名空间,以避免命名冲突。要从对象属性中获取值,您必须指定两件事:要获取哪个对象及其属性。为此,您使用 PHP 的对象操作符。一般语法如下所示:
$objectName->propertyName;
PHP 的对象操作符看起来像一个箭头。它表示您正在获取特定对象中的特定属性。
对页面数据使用 StdClass 对象
让我们用一个对象重构index.php
和页面模板,以防止烦人的命名冲突。以下是index.php
的一些变化:
<?php
//complete code for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$pageData = new stdClass();
$pageData->title = "New, object-oriented test title";
$pageData->content = "<h1>Hello from an object</h1>";
$page = include_once "templates/page.php";
echo $page;
您还必须更新templates/page.php
,以便它在正确的位置使用新创建的对象及其属性:
<?php
return "<!DOCTYPE html>
<html>
<head>
<title>``$pageData->title
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
</head>
<body>
$pageData->content
</body>
</html>";
保存文件并在浏览器中重新加载index.php
。期望在<title>
和<body>
元素中看到改变的值。
对象属性就像变量一样
普通 PHP 变量是值的简单占位符。对象显然更复杂,因为一个对象可以保存几个值。前面的StdClass
对象保存两个独立的值。每个值都存储在唯一的对象特性中。对象属性的行为类似于 PHP 变量。它们是值的简单占位符。一个对象可以有许多属性。每个属性可以包含一个值。在前面的示例中,您可以看到,为了获取值,您必须同时指定对象和属性。
PHP 属性可以理解为 cups。它们所包含的价值可以理解为杯子里的咖啡(或任何东西)。打个比方,你可以把一个对象看作一个托盘,把它的属性看作托盘上的几个杯子。要得到你的咖啡,你必须从正确的托盘中得到正确的杯子。
在前面的代码示例中,您可以看到在<title>
中使用了$pagedata
对象的title
属性,在<body>
元素中使用了$pageData
对象的content
属性。
页面视图
一个个人作品集网站可能会有几个不同的页面。也许一页是关于你的技能和教育背景,另一页是你工作实例的链接。
因为您正在创建一个动态网站,所以您不必创建两个完整的 HTML 页面。您可以使用页面模板显示两种不同的页面视图。页面视图是看起来像单个页面的东西。一个页面视图可能由几个较小的视图组成。你可以把一个页面视图想象成一个乐高房子,把一个视图想象成一个乐高积木:把较小的部分组合起来,就可以建造出更大的东西。
让我们将所有视图保存在一个文件夹中。在现有的项目文件夹中创建一个名为views
的新文件夹。创建一个新文件,views/skills.php
。
<?php
return "<h1>Skills and educational background</h1>
<p>Read all about my skills and my formal training</p>
";
这是完整的文件。这是一个非常小的观点。开发代码时,从小处着手通常是个好主意。任何潜在的错误都将更容易在更少的代码行中被发现。您需要在views/projects.php
中查看另一个小视图。
<?php
return "<h1>Projects I have worked on</h1>
<ul>
<li>Ahem, this will soon be updated</li>
</ul>";
制作动态网站导航
你必须在正确的时间展示正确的观点。您可以创建一个全局的、持久的站点导航,即,在网站的每个页面上都是相同的导航。因为 PHP 可以包含文件,所以您可以简单地将导航代码保存在一个文件中,并在每个需要它的脚本中包含它。一个明显的优点是你可以在一个文件中改变导航,这种改变会反映在每个站点页面上,不管有多少个页面。在views/navigation.php
中创建一个新文件。
<?php
return "
<nav>
<a href='index.php?page=skills'>My skills and background</a>
<a href='index.php?page=projects'>Some projects</a>
</nav>
";
注意,整个导航字符串用双引号分隔。使用单引号来分隔href
属性值,因此,不能在导航字符串中使用双引号。第三个双引号将触发 PHP 错误。所以你。
要查看索引页面上的导航,您必须从index.php
开始包含它。
<?php
//complete code for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$pageData = new stdClass();
//changes begin here
$pageData->title = "Thomas Blom Hansen: Portfolio site";
$pageData->content = include_once "views/navigation.php";
//end of changes
$page = include_once "templates/page.php";
echo $page;
保存并运行这段代码,您应该会看到一个带有导航的页面。现在还不要期望看到任何视图。
用 PHP 传递信息
传递数据是区分动态网页和静态网页的关键。通过根据用户的选择定制体验,您可以为网站增加全新的价值。可以通过 URL 变量将信息传递给 PHP。URL 变量就是在 URL 中声明的变量。您可以在导航中看到两个 URL 变量。仔细看看导航<a>
元素中的href
属性。
index.php?page=skills
index.php?page=projects
href
表示点击导航项将加载index.php
并编码一个名为page
的 URL 变量。如果你点击一个链接,名为page
的 URL 变量将得到一个值skills
。如果点击另一个链接,page
会得到一个值projects
。
PHP 可以访问 URL 变量并使用它们,例如,在正确的时间加载正确的页面视图。URL 变量是动态网站的生命线。
访问 URL 变量
要访问 URL 变量,可以使用$_GET
超全局数组。以下是您在index.php
中如何使用它:
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$pageData = new stdClass();
$pageData->title = "Thomas Blom Hansen: Portfolio site";
$pageData->content = include_once "views/navigation.php";
//changes begin here
$navigationIsClicked = isset($_GET['page']);
if ($navigationIsClicked ) {
$fileToLoad = $_GET['page'];
$pageData->content .= "<p>Will soon load $fileToLoad.php</p>";
}
//end of changes
$page = include_once "templates/page.php";
echo $page;
那是相当多的一口!PHP 可以通过$_GET
访问 URL 变量。要访问名为page
的 URL 变量的值,您需要编写$_GET['page']
。只有当用户点击了一个导航项目时,才会有一个名为page
的 URL 变量。
使用 isset()测试变量是否已设置
如果你试图使用一个不存在的变量,你将触发一个 PHP 错误。所以,在你试图访问一个变量之前,你必须确保这个变量已经设置好了。PHP 为此构建了一种语言。您已经看到了它的作用。
$navigationIsClicked = isset($_GET['page']);
如果括号内的变量被设置,isset()
函数将返回TRUE
。所以,如果用户点击了一个导航项,$navigationIsClicked
将会是TRUE
;如果不是,那就是FALSE
。
如果$navigationIsClicked
是TRUE
,那么声明一个名为$fileToLoad
的 PHP 变量,来存储名为page
的 URL 变量的值。接下来,向$pageData->content
属性添加一个字符串,以显示名为page
的 URL 变量的值。保存并运行代码。在浏览器中加载后,单击“我的技能”导航项目。这应该会在您的浏览器中产生以下输出:
Will soon load skills.php
如果您单击另一个导航项目,您可以看到输出的变化。您会看到,根据用户与站点的交互方式,输出会动态变化。
$_GET,一个超全局数组
PHP 可以通过一个叫做$_GET
的所谓超全局数组来访问 URL 变量。PHP 还有一些用于其他目的的超全局数组。使用$_GET
,您可以通过名称访问 URL 变量。在导航中,你有两个<a>
元素。单击其中任何一个都会为一个名为page
的 URL 变量编码一个唯一的值。
在图 2-1 中可以看到浏览器地址栏中的一个 URL 变量。注意 URL 变量page
的值在输出中是如何表示的?要用 PHP 获取 URL 变量的值,您需要编写
$_GET['the name of the url variable'];
图 2-1。
A URL variable in action
动态包含页面视图
动态站点导航即将完成。它工作得很好,除了当导航项目被点击时页面视图没有被加载。让我们通过更新index.php
中的代码来改变这一点,如下所示:
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$pageData = new stdClass();
$pageData->title = "Thomas Blom Hansen: Portfolio site";
$pageData->content = include_once "views/navigation.php";
$navigationIsClicked = isset($_GET['page']);
if ($navigationIsClicked ) {
$fileToLoad = $_GET['page'];
//change one line to load page views dynamically
$pageData->content .=include_once "views/$fileToLoad.php";
}
$page = include_once "templates/page.php";
echo $page;
保存更改并在浏览器中重新加载index.php
。当您单击一个导航项目时,您会看到输出发生了变化。将加载并显示相应的页面视图。是您的index.php
文件动态地改变了它的外观。名为page
的 URL 变量将决定加载哪个文件。
就是这样!这是一个基本的,动态的网站,具有持久的,全球性的导航。
串联
你注意到上面代码中的.=
了吗?它是 PHP 的增量连接运算符,和基本赋值运算符=
有一点不同。
这里有一个例子来说明不同之处:
<?php
$test = "<p>Hello ";
$test = "world</p>";
echo $test;
$concatenationTest = "<p>Hello ";
$concatenationTest .= "world</p>";
echo $concatenationTest;
上例的 HTML 源代码输出清楚地显示了赋值和增量连接之间的区别。
world</p>
<p>Hello world</p>
赋值操作符为变量赋值一个新的字符串值,并覆盖该过程中任何以前的字符串。因此,变量$test
中的初始"<p>Hello "
被覆盖。
增量连接操作符将$concatenationTest
中的现有字符串与一个新字符串合并。增量串联在现有字符串的末尾添加新字符串。
严格的命名约定
很高兴看到你的第一个动态网站工作,不是吗?它是可行的,并且依赖于严格的命名约定。导航项目为名为page
的 URL 变量编码不同的值。相应的页面视图文件必须同名,并保存在views
文件夹中。
超链接 | 可变 URL | 视图文件 |
---|---|---|
index.php?page =技能 | page =技能 | view/skills . PHP |
index.php?页面=项目 | 页面=项目 | views/projects.php |
显示默认页面
动态导航非常好用,但是它有一个缺点:当用户导航到index.php
时,没有显示默认的页面视图,在这种情况下,名为page
的 URL 变量没有值。在index.php
很容易改变。您只需稍微改变一下if
语句。
//partial code for index.php
if ($navigationIsClicked ) {
$fileToLoad = $_GET['page'];
} else {
$fileToLoad = "skills";
}
$pageData->content .=include_once "views/$fileToLoad.php";
显著的变化是$fileToLoad
从 URL 变量page
中获取它的值,如果设置了的话。如果没有设置,$fileToLoad
将有一个默认值skills
。一旦$fileToLoad
有了值,你就可以用它来加载用户请求的页面视图或者关于“我的技能”的默认页面视图
验证您的 HTML
生成 HTML 页面的过程有点抽象。如果在正确的时间显示正确的页面视图,很容易认为一切都是完美的。如果你看到了正确的动作,你的 PHP 脚本就会完美地工作。但这并不意味着你的 HTML 是完全有效的。动态网页应该符合 web 标准,就像静态 HTML 页面一样。您应该像验证任何其他 HTML 一样验证生成的 HTML。
Note
您可以在浏览器中加载一个动态页面,并通过浏览器查看生成的 HTML 源代码。当您看到生成的 HTML 源代码时,您可以选择它,复制它,并将其粘贴到在线 HTML 验证服务中。我一般用 http://validator.w3.org/#validate_by_input
。
用 CSS 设计网站样式
当所有页面视图的 HTML 都有效时,你就可以开始用 CSS 来设计你的站点了。您完全可以像通常样式化静态 HTML 站点那样做:为站点的视觉设计创建一个带有样式规则的外部样式表。为了对 portfolio 站点进行这样的操作,您可以在您的项目文件夹中创建一个名为css
的新文件夹。在css
文件夹中创建一个名为layout.css
的新文件:
nav {
background-color: #CCCCDE;
padding-top: 10px;
}
nav a{
display:inline-block;
text-decoration:none;
color: #000;
margin-left: 10px;
}
nav a:hover{text-decoration: underline;}
您可以更改或添加您喜欢的任何样式规则。前面的css
只是让你开始。您可能想要设计所有动态 HTML 页面的样式,那么为什么不将这个功能构建到页面模板中呢?您只需为指向外部样式表的<link>
元素添加一个新的占位符。让我们更新templates/page.php
:
<?php
return "<!DOCTYPE html>
<html>
<head>
<title>$pageData->title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
$pageData->css
</head>
<body>
$pageData->content
</body>
</html>";
注意,新属性被用作引用外部样式表的<link>
元素的占位符。要使用更新的页面模板,您必须更新index.php
并为新属性声明一个值:
<?php
//partial code listing for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
$pageData = new stdClass();
$pageData->title = "Thomas Blom Hansen: Portfolio site";
$pageData->content = include_once "views/navigation.php";
//one line of code added here
$pageData->css = "<link href='css/layout.css' rel='stylesheet' />";
保存文件并在浏览器中加载index.php
。期待看到你的风格规则生效。
声明 Page_Data 类
有时,使用内部的嵌入式样式表来补充外部样式表会非常有用。您可以很容易地用一个<style>
元素的占位符来更新页面模板。更新templates/page.php
。
<?php
return "<!DOCTYPE html>
<html>
<head>
<title>$pageData->title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
$pageData->css
$pageData->embeddedStyle
</head>
<body>
$pageData->content
</body>
</html>";
从index.php
声明一个属性值同样容易,但是让我们做一些不同的事情。问题是有时你不需要任何嵌入的<style>
元素,而有时你需要。
既然您的模板有了一个用于嵌入 CSS 的占位符,那么该属性必须始终有一个值。你不想浪费时间为一个多余的<style>
元素声明一个值,所以让我们做一个更智能的解决方案。让我们朝着面向对象编程迈出下一步,为页面数据创建一个自定义类。在您的项目文件夹中创建一个名为classes
的新文件夹。在classes
文件夹中创建一个名为Page_Data.class.php
的新文件。
<?php
class Page_Data {
public $title = "";
public $content = "";
public $css = "";
public $embeddedStyle = "";
}
就是这样—一个定制的类,为页面模板所需的属性预定义了空字符串值。关键字class
表示后面的名称是自定义类名。
PHP 类名可以以字母或下划线开头。习惯上以大写字母开头。如果类名是一个复合词,通常用下划线字符分隔各个单词,并以大写字母开始下一个单词,例如My_Custom_Class_Name
。
我通常将我的每个自定义 PHP 类定义保存在一个单独的文件中,其名称类似于类名。我也喜欢用可选的后缀.class.php
来结束文件名。因此,My_Custom_Class_Name
的文件将被称为My_Custom_Class_Name.class.php
。
在类名后面是一组花括号,用来为类定义划分代码块。看一下Page_Data
的代码块。在花括号内,使用关键字public
声明了四个属性。结果是Page_Data
类将会有四个默认属性,每个属性都声明了一个默认值。
类制造对象
您可以使用来自index.php
的新类定义。这将是一个微小的变化。更新index.php
如下:
//Partial code listing for index.php
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
//delete or comment out the previous object
//$pageData = new stdClass();
//no changes below this point
在浏览器中加载 http://localhost/ch2/index.php,以测试您的代码。您的网站应该完全像以前一样工作。你知道你对代码做了一些改动,但是这些改动普通用户是看不到的。您已经重构了代码。
要使用自定义的类,必须首先包含类定义。接下来,您必须使用new
关键字用类定义创建一个新对象。Page_Data
类使我们能够在页面模板中为嵌入的样式保留一个占位符,并且只要您需要一个带有嵌入的<style>
元素的页面,就只为该属性分配一个实际值。
用动态样式规则突出显示当前导航项目
您有一个页面模板和一个Page_Data
对象,它们准备好处理嵌入的样式。您通常希望将样式规则保存在外部样式表中。对于动态网站,这个约定仍然适用,但是因为您可以将样式嵌入到index.php
中,所以您可以很容易地使用动态样式。大多数时候,好的 ole 外部样式表可以很好地完成工作。但是在少数情况下,动态样式非常强大。您可以使用动态样式规则来突出显示当前导航项目。这真的很简单,一旦你明白了。更新index.php
如下:
<?php
//complete code listing for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "Thomas Blom Hansen: Portfolio site";
$pageData->content = include_once "views/navigation.php";
$pageData->css = "<link href='css/layout.css' rel='stylesheet' />";
$navigationIsClicked = isset($_GET['page']);
if ($navigationIsClicked ) {
$fileToLoad = $_GET['page'];
} else {
$fileToLoad = "skills";
}
$pageData->content .=include_once "views/$fileToLoad.php";
//new code below: dynamic style added below
$pageData->embeddedStyle = "
<style>
nav a[href *= '?page=$fileToLoad']{
padding:3px;
background-color:white;
border-top-left-radius:3px;
border-top-right-radius:3px;
}
</style>";
$page = include_once "templates/page.php";
echo $page;
保存您的工作并将索引加载到浏览器中。您应该会看到一个简单的选项卡式导航,当前导航项目清晰地突出显示。在下面的例子中,我点击了“一些项目”导航项。由于突出显示的导航选项卡,用户可以清楚地看到显示的是哪个页面。你可以在图 2-2 中看到我的例子。
你懂一些 PHP,所以你知道这一切的发生是因为名为page
的 URL 变量有一个值skills
。它可能还不是一个很好的视觉设计,但是它应该足够让你应用你所有的 CSS 技能来设计一个漂亮的用户界面。
图 2-2。
Current navigation item highlighted with a dynamic style rule
仔细看看动态 CSS
所使用的 CSS 属性选择器并不常用。让我们仔细看看。
nav a[href *= '?page=$fileToLoad']
首先,注意 PHP 变量$fileToLoad
。它是实际值的占位符。例如,当用户点击“一些项目”导航项时,$fileToLoad
的值为projects
,因为名为page
的 URL 变量的值为projects
。你可以在上图浏览器截图的地址栏看到。因此,当浏览器解释 CSS 时,它实际上会看到以下内容:
nav a[href *= '?page=projects']
选择器告诉浏览器类似“寻找一个<nav>
元素,在这个元素中你会找到一个<a>
元素,它的href
属性包含字符串?page=projects
假装你是浏览器。看看你的<nav>
元素。寻找一个具有包含?page=projects
的href
属性的<a>
元素。
您和浏览器将只找到一个这样的<a>
元素。浏览器将对这个<a>
元素应用一个特殊的样式规则,这个规则将突出显示这个元素。
摘要
现在您已经看到了如何使用一点基本的 PHP 来构建一个非常动态的站点。在这一点上,你的学习过程可能会受益于一些实验。
你可以尝试建立一个个人作品集网站。添加您认为合适的页面视图,并相应地更新您的导航。
你可以尝试创建一些更全面的页面视图。在这个过程中,你会逐渐适应动态站点结构,页面视图会返回到index.php
上显示。
你可以利用你现有的 CSS 技能为你的作品集开发一个一致的网站设计。在这个动态网站的新环境中使用你现有的 HTML 和 CSS 技能将是一个非常好的练习。这对你来说可能相对容易,因为作品集网站相当简单。在你工作的网站很简单的时候做这个练习是个好主意。你开发的动态网站很快就不再简单了。
当你觉得准备好了,翻页学习 HTML 表单、PHP 函数和条件语句,所有这些你都会在第三章中遇到。
三、表单管理
在第二章,我们建立了一个动态的个人作品集网站。在这个过程中,您看到了如何用<a>
元素对 URL 变量进行编码,以及如何使用$_GET
超全局变量来访问这些 URL 变量。传递数据是区分动态网页和静态网页的关键。通过根据用户的选择定制体验,您可以为网站增加全新的价值。
现在,您已经了解了一些 PHP 并编写了一个基本的动态站点,您已经准备好深入研究 URL 变量了。HTML <form>
元素通常用于创建允许用户与动态站点交互的界面。你必须学会如何处理这样的 HTML 表单。在本章中,您将学习以下内容:
- 什么是 HTML 表单以及如何创建它们
- 什么是超全局数组以及如何使用它们
- 如何使用
GET
方法在 HTML 表单中编码 URL 变量 - 如何使用
POST
方法在 HTML 表单中编码 URL 变量 - 如何编写一个动态 PHP 测试
- 何时使用
if-else
条件语句 - 什么是命名函数以及如何编写一个命名函数
- 一部美国西部电影能教会你什么是干净的代码
- 为什么代码真的是诗歌
什么是表格?
HTML 表单允许访问者与站点进行交互。图 3-1 显示了谷歌的搜索表单。当用户访问 www.google.com
,在文本输入字段中键入搜索词,并点击谷歌搜索时,谷歌执行搜索。
图 3-1。
Search form from www.google.com
你一定遇到过的另一种表单是登录表单,注册用户可以通过它登录并进入受限区域。当您登录您的脸书帐户、银行帐户或 Gmail 帐户时,您可能会看到此类表单。图 3-2 中的登录名来自脸书。
图 3-2。
Login form from www.facebook.com
最后一个熟悉的例子是星级评定系统。如果你在网上书店买过一本书,你可能会遇到一个星级系统。图 3-3 显示了亚马逊的星级评定表。
图 3-3。
Star rating form from www.amazon.com
如果你打算从事网页开发或网页设计,你肯定会从事开发和设计有用的、功能性的表单。因为 web 表单是系统和用户之间的接口,所以开发和设计 web 表单是非常重要的。
建立一个新的 PHP 项目
学习需要重复,所以让我们重复上一章学到的一些东西。在XAMPP/htdocs
文件夹中创建一个名为ch3
的新项目文件夹。在ch3
中,你需要来自之前项目的templates
和classes
文件夹的副本,以及里面的 PHP 脚本。创建一个名为views
的空文件夹。打开 Komodo Edit 并在ch3
中创建新的index.php
文件。请确保在保存文件时将格式设置为所有文件。图 3-4 说明了如何操作。
图 3-4。
Save a new file as index.php
with Komodo Edit
你需要在index.php
中安装一些 PHP 来做更多的事情。您可以从输出一个简单的 HTML 页面开始。注意,您应该重用classes/Page_Data.class.php
和templates/page.php
,而不需要修改任何一个脚本中的一行代码。当你知道了 PHP,你就不用多次解决同一个任务了。只需解决一次,并对其进行编码以便重用,如下所示:
<?php
//code listing for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "Building and processing HTML forms with PHP";
$pageData->content = "<nav>will soon show a navigation...</nav>";
$pageData->content .= "<div>...and a form here</div>";
$page = include_once "templates/page.php";
echo $page;
亲眼看看
为了检查您输入的内容是否正确,您可以保存index.php
并在浏览器中导航到 http://localhost/ch3/index.php。
will soon show a navigation...
...and a form here
没有禅师会拿棍子戳你,但我有几个问题要问你。你的答案将表明你到目前为止学到了什么。如有疑问,可查阅第二章进行解释。
include_once
是做什么的?$pageData->title
如何改变生成的 HTML 页面的<title>
?.=
是什么意思?它的专业名称是什么?- 当我们
echo $page
的时候会发生什么?
创建动态导航
您将创建两个不同的表单。您将需要一个站点导航来在这些表单之间导航。创建一个新文件ch3/views/navigation.php
,如下所示:
<?php
//code listing for views/navigation.php
return "
<nav>
<a href='index.php?page=search'>Search on bing</a>
<a href='index.php?page=quiz'>Dynamic quiz</a>
</nav>
";
就像在第二章中一样,你创建一个 PHP 脚本,简单地返回一小段 HTML 代码。在index.php
中,您将使用一些 PHP 将一些小的 HTML 片段拼接在一起,生成一个格式良好的动态 HTML 页面。以下,index.php
更新显示导航:
<?php
//code listing for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "Building and processing HTML forms with PHP";
//change this one line below only
$pageData->content = include_once "views/navigation.php";
$pageData->content .= "<div>...and a form here</div>";
$page = include_once "templates/page.php";
echo $page;
为表单创建页面视图
你可以遵循第二章中页面视图的命名约定,因为它似乎为动态网站提供了一个可靠的代码架构。这种组织和命名页面视图的方式可以给你一个构建动态站点的心理框架。当你有了内在的框架,你就会知道你需要哪些文件来开发你想要开发的网站。你不必每次创建一个新的站点都要重新发明一个好的动态代码架构。
上一节描述的导航包含指向名为“搜索”和“测验”的页面的链接。因此,我们必须在views
文件夹中创建两个新的 PHP 文件。
超链接 | 可变 url | 视图文件 |
---|---|---|
index.php?页面=搜索 | 页面=搜索 | views/search.php |
index.php?page =测验 | page=测验 | views/quick . PHP |
使用 Komodo Edit 创建两个新文件,如下所示:
<?php
//code listing for views/search.php
return "will soon show the search form";
<?php
//code listing for views/quiz.php
return "quiz will go here";
显示 index.php 的页面浏览量
为了让index.php
在被请求时显示这些页面视图,您必须额外编写几行代码,这些代码几乎与您在index.php
中为上一个项目编写的代码相同,如下所示:
<?php
//code listing for ch3/index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "Building and processing HTML forms with PHP";
$pageData->content = include_once "views/navigation.php";
//changes begin here
//$pageData->content .= "<div>...and a form here</div>";
$navigationIsClicked = isset($_GET['page']);
if ( $navigationIsClicked ) {
$fileToLoad = $_GET['page'];
} else {
$fileToLoad = "search";
}
$pageData->content .=include_once "views/$fileToLoad.php";
//no changes below
$page = include_once "templates/page.php";
echo $page;
这段代码实际上是告诉 PHP 加载站点访问者请求的任何视图。如果没有点击导航项目,我们将显示views/search.php
。您可以通过在浏览器中加载 http://localhost/ch3/index.php 来测试您的代码是否工作。
明智地使用时间:惯例和重用
你肯定已经注意到这个动态网站非常像第二章中的动态网站。我们重用了一些文件,比如Page_Data
类和页面模板。我们不能完全像在第二章中一样重用index.php
或navigation.php
,但是这个项目是按照相同的惯例构建的。
重用代码是一个好主意,因为这允许您更快地开发解决方案。如果您有在一个项目中工作的脚本,您可以非常信任它们在其他项目中做同样的事情。因此,代码重用减少了调试时间。
总会有你不容易重用的部分,比如导航。但是如果你养成了在不同的项目中以相同的方式创建动态导航的习惯,你将能够快速而轻松地开发新的动态导航。因此,当您不能按原样重用代码时,也许您可以重用那些支持您知道有效的代码的原则。
一个超级简单的搜索表单
HTML 表单是用<form>
元素创建的。还有许多专门为表单设计的 HTML 元素。也许最重要的是<input>
元素。让我们在views/search.php
中创建一个例子,如下:
<?php
return "
<form method='get' action='
http://www.bing.com/search
<input type='text' name='q' />
<input type='submit' value='search on bing' />
</form>
";
尝试您的搜索表单
保存您的工作,将浏览器指向 http://localhost/ch3/index.php 以查看表单。你应该会看到类似图 3-5 的东西。
图 3-5。
A simple search form completely unstyled
您可以在文本字段中键入一些搜索词,然后单击按钮。你的浏览器会加载bing.com
,Bing 会搜索你输入的任何内容。我输入了猫。完成搜索后,查看一下浏览器的地址栏。你会发现类似 http://www.bing.com/search?q=cats
的东西。
Tip
一个
当您单击提交按钮时,您的浏览器会请求一个新的 URL。这就像点击一个<a>
元素。当您单击一个<a>
元素时,您的浏览器将请求由其href
属性指示的资源。当您提交表单时,您的浏览器将请求由<form>
元素的action
属性指示的资源。
表单编码 URL 变量
<form>
元素的action
属性是 http://www.bing.com/search
,但是当您提交表单时,您的浏览器请求了 http://www.bing.com/search?q=cats
。不知何故,表单将一个名为q
的 URL 变量编码到请求的 URL 中,并将其值设置为cats
。这就是表单可以做的:它们可以在 HTTP 请求中编码 URL 变量。
正如你在前一章看到的,URL 变量可以用 PHP 访问。URL 变量对于动态网站来说是必不可少的。显然,如果设置了名为q
的 URL 变量, www.bing.com
将执行搜索。Bing 将寻找q
拥有的任何价值。
Name 属性声明 URL 变量的名称
理解 URL 变量q
是如何声明的很重要。理解了这一点,你就理解了形式的本质!
URL 变量q
之所以得名,是因为<form>
有一个<input>
元素,其name
属性设置为 q。如果<input>
元素嵌套在<form>
中,你可以推断出具有name
属性的<input>
元素将声明一个 URL 变量。这条规则也适用于表单中常用的其他 HTML 元素的一小部分。在本书中,你会看到更多这样的与表单相关的元素。
元素和一些常见的类型
您是否注意到<input type='text' />
显示为单行文本字段,而<input type='submit'/>
显示为提交按钮?输入type
属性有许多可能的值。在这本书里,你会看到一些输入类型。一旦你能使用这些,学习如何使用剩下的输入类型应该没有问题。
Note
HTML5 引入了不少新的<input>
类型,但并不是所有主流浏览器都实现了这些类型。这些新类型中有许多非常有用。在 http://caniuse.com/#search=input
看哪些浏览器实现了哪些功能。
了解方法属性
到目前为止,您只看到了可以在浏览器地址栏的 URL 中看到的 URL 变量。这种 URL 变量使用 HTTP 方法GET
进行编码。您已经使用这些变量创建了一个动态导航和一个可以在 www.bing.com
执行搜索的表单。
任何用GET
编码的 URL 变量都被限制为相对较少的字符。具体数字因浏览器而异,但有效的最大值似乎是 2000 个字符左右。因为 URL 中的变量是显而易见的,页面可以被书签标记和链接。因此,GET
变量非常适合站点导航。
命名 PHP 函数
也许 PHP 最强大的特性之一是能够在代码中定义和执行函数。函数是在脚本中声明的命名代码块,您可以在以后调用它。您将很快使用函数编写一个动态测验,但是让我们先来看看 PHP 中命名函数的基础知识:
function functionName () {
//function body
}
函数的基本语法
函数的基本格式要求您首先在函数名前面使用function
关键字来声明函数。函数名可以包含任何字母数字字符和下划线,但不能以数字开头。函数名后面必须跟一组括号和一个用花括号分隔的代码块。在ch3
中用 Komodo Edit 创建一个新的 PHP 文件。称它为test-functions.php
。声明一个命名函数,如下所示:
<?php
function p(){
echo "<p>This paragraph came from a function</p>";
}
如果您加载 http://localhost/CH3/test-functions . PHP,您将看不到任何输出。许多初学者希望看到前面代码的输出。但是函数并不总是像初学者假设的那样运行。在显式调用函数名之前,不会执行函数体内的代码。您可以在test-functions.php
中添加一个函数调用来执行代码,如下所示:
<?php
//function declaration
function p(){
echo "<p>This paragraph came from a function</p>";
}
//function call
p();
再次运行代码,您将在浏览器中看到预期的输出。函数的一个非常有趣的特性是它们可以非常容易地被重用。简单地调用一个函数两次,它就运行两次。开始吧。
<?php
//function declaration
function p(){
echo "<p>This paragraph came from a function</p>";
}
//function calls
p();
p();
你大概能猜对,代码会输出两个<p>
元素,每个元素都有相同的文本:This paragraph came from a function
。更重要的是,您可以看到函数声明和函数调用之间的区别。该示例有两个不同的函数调用。因为函数调用两次,所以会运行两次。
但这是一个极其丑陋的例子。功能非常不灵活。它只能做一件事,即输出那一个字符串。让我们把它变得更聪明一点。
<?php
//function declaration
function p(){
return "<p>This paragraph came from a function</p>";
}
//function calls
$output = p();
$output .= "<h1>Just some heading</h1>";
$output .=p();
echo $output;
现在这样好多了!显著的变化是函数不再有echo
。相反,它返回一个生成的<p>
。这是有后果的。为了让<p>
得到回应,你必须把echo
写在别的地方。在这个例子中,echo
现在出现在最后。
你可能会问为什么这更聪明?那很简单!既然函数没有echo
,如果需要的话,你可以在echo
它之前进一步操作输出。
实际上,这是一个很好的经验法则:不要直接从一个函数开始。使用return
语句要好得多。在代码中的一个地方有一个单独的echo
语句比把echo
语句分散在各处要好得多。
使用函数参数增加灵活性
你可能认为让一个函数总是返回一个内容完全相同的<p>
有点傻。当然,你说得很对。它不是很灵活。所以,让我们用一个函数参数来改进function p()
。
<?php
//function declaration
function p( $content ){
return "<p>$content</p>";
}
//function calls
$output = p( "I want this text in my first paragraph" );
$output .=p( "...and this in my second" );
echo $output;
注意,我在函数声明中的括号内声明了一个名为$content
的变量。这是一个函数参数。$content
用于存储返回的<p>
元素中使用的内容。但是$content
如何获得一个值呢?每次调用函数时都会发生这种情况。函数p()
被调用时使用的参数将被临时存储在$content
中。函数参数非常酷,因为它们允许你编写一个函数,这个函数可以用许多不同的值重用。在本书的后面,你将会看到更多带参数函数的例子。
你考虑过函数名p()
吗?我喜欢我的函数名有意义,因为这个函数总是返回一个<p>
元素,我想p()
会是一个很好的名字。你可以考虑使用另一个名字,比如returnPTag()
。好的函数名应该准确且有意义。无论如何,这是探索 PHP 函数可以做的一些事情的一个小弯路。接下来,让我们使用函数编写一个动态测验。
为测验创建表单
在views
文件夹中创建一个名为quiz-form.php
的新 PHP 文件。
<?php
//complete code for views/quiz-form.php
return "<form method='post' action='index.php?page=quiz'>
<p>Is it hard fun to learn PHP?</p>
<select name='answer'>
<option value='yes'>Yes, it is</option>
<option value='no'>No, not really</option>
</select>
<input type='submit' name='quiz-submitted' value='post' />
</form>";
显示测验表格
要显示测验表格,您必须更新views/quiz.php
中的代码,如下所示:
<?php
$output = include_once "views/quiz-form.php";
return $output;
保存这两个文件,将浏览器指向 http://localhost/ch3/index.php?page = quiz,看看你创造了什么。
使用
前面的表单使用了两个您可能不熟悉的 HTML 元素。当您希望用户在几个预定义的选项之间进行选择时,<select>
元素是一个很好的元素。选项通过嵌套<option>
元素来显示。其结构非常类似于常规的 HTML 列表,如<ul>
和相应的<li>
。
当用户选择一个选项时,新的 URL 变量将被编码到提交表单时发送的请求中。注意,URL 变量的名称将由<select>
元素的name
属性定义,其值由所选<option>
元素的value
属性定义。
POST 方法
您的第一个表单使用了GET
方法,但它不是唯一可能的 HTTP 方法。还有一种方法叫POST
。POST
方法没有定义最大字符数——事实上,POST
方法甚至不限于文本。当使用 HTTP POST
方法时,可以通过表单上传文件。
此外,HTTP POST
变量在 URL 中不是直接可见的。它们被隐藏起来发送。这使得 HTTP POST
成为必须处理大量内容和带有敏感信息的表单的完美候选。因为 HTTPOST
变量不是 URL 的一个组成部分,所以用户不能将依赖于 HTTP POST
变量的页面视图标记为书签。
使用$_POST 超级全局
PHP 有一个名为$_POST
的原生超级全局。它可以用来访问用POST
方法编码的 URL 变量。提交表单时,您可以使用它来处理表单。更新views/quiz.php
,如下:
<?php
//add a new variable and an if statement
$quizIsSubmitted = isset( $_POST['quiz-submitted'] );
if ( $quizIsSubmitted ){
$answer = $_POST['answer'];
$output = showQuizResponse( $answer );
} else {
$output = include_once "views/quiz-form.php";
}
//keep the return statement as it was
return $output;
//declare a new function
function showQuizResponse( $answer ){
$response = "<p>You clicked $answer</p>";
$response .= "<p>
<a href='index.php?page=quiz'>Try quiz again?</a>
</p>";
return $response;
}
可以加载 http://localhost/ch3/index.php?page =在您的浏览器中进行测验,查看代码的作用。它首先检查表单是否被提交。还记得表单有一个提交按钮吗?
<input type='submit' name='quiz-submitted' value='post' />
好吧,如果 PHP 可以找到用名称为quiz-submitted
的POST
方法编码的 URL 变量,您就知道表单已经提交了。如果表单已提交,您可以使用超级全球$_POST
获得所选答案。然后,答案作为参数传递给新函数showQuizResponse()
,该函数将简单地返回一个字符串来指示用户的答案,并显示一个<a>
元素来重新开始测验。
您制作的第一个表单的action
属性指向 www.bing.com
。提交表单时,测验表单应重新加载测验页面。加载测验的 URL 是index.php?page=quiz
,所以<form>
的action
属性准确地引用了那个资源。
$_POST 是一个数组
你已经知道$_GET
是一个超级全局数组。$_POST
是另一个超全局数组。但是什么是真正的数组呢?基本上,数组是一种可以保存多项的数据类型。每一项都存储在一个索引下。我想分享一个例子。如果您愿意,可以创建一个新的 PHP 文件并编写示例代码,但这并不是真正必要的。我把下面的代码保存在一个叫做test-assoc-array.php
的文件中:
<?php
//complete code for ch3/test-assoc-array.php
$my['name'] = "Thomas";
$my['year-of-birth'] = 1972;
$my['height'] = "193cm";
$out = "My name is " . $my['name'];
echo $out;
如果您在浏览器中运行 http://localhost/CH3/test-assoc-array . PHP,您会看到“我的名字是 Thomas”的输出。在前面的示例中,$my
是一个数组。您可以看到它保存了存储在同一个变量中的数据集合。为了从数组中获取数据,必须使用正确的索引。在前面的示例中,“Thomas”存储在索引[ 'name']
下。在命名索引下存储项目的数组称为关联数组。
检查数组中的所有项目通常会很方便。PHP 有这样一个功能。它叫做print_r()
。下面是使用它的一种方法:
<?php
//complete code for ch3/test-assoc-array.php
$my['name'] = "Thomas";
$my['year-of-birth'] = 1972;
$my['height'] = "193cm";
$out = "<pre>";
$out .=print_r($my, true);
$out .= "</pre>";
echo $out;
如果运行这段代码,可以看到$my
的每个索引及其对应的值。您将看到如下内容:
Array
(
[name] => Thomas
[year-of-birth] => 1972
[height] => 193cm
)
你看到的是 PHP 看到的数组。您会看到一个包含三个命名索引及其值的数组。数组在您的代码中非常有用,因为它们允许您将项目分组在一起。PHP 提供了$_GET
和$_POST
数组,让您可以访问用 http 方法GET
或POST
编码的所有数据。测验示例使用了POST
。我想让您检查一下$_POST
,这样您就可以亲眼看到表单提交后的效果。更新views/quiz.php
中if-else
语句的一些代码,如下所示:
<?php
//complete code for views/quiz.php
$quizIsSubmitted = isset( $_POST['quiz-submitted'] );
if ( $quizIsSubmitted ){
$answer = $_POST['answer'];
$output = showQuizResponse( $answer );
//inspect the $_POST superglobal array
$output .= "<pre>";
$output .= print_r($_POST, true);
$output .= "</pre>";
} else {
$output = include_once "views/quiz-form.php";
}
return $output;
function showQuizResponse( $answer ){
$response = "<p>You clicked $answer</p>";
$response .= "<p>
<a href='index.php?page=quiz'>Try quiz again?</a>
</p>";
return $response;
}
保存工作,将浏览器指向 http://localhost/ch3/index.php?page =测验。如果您提交测验表单,您可以看到$_POST
数组中的所有项目:
Array
(
[answer] => yes
[quiz-submitted] => post
)
您可以从输出中看到,我在提交表单之前选择了yes
,您还可以看到索引quiz-submitted
包含一个值post
。我真正希望您看到的是,当提交表单时,每个带有 name 属性的表单相关 HTML 元素在$_POST
中编码一个命名索引。在views/quiz-form.php
看一看。看看 Submit 按钮是如何对索引quiz-submitted
进行编码的,因为它有一个值为quiz-submitted
的 name 属性。<select>
元素编码answer
索引,因为它有一个值为answer
的 name 属性。PHP 通过$_POST
超全局数组提供对所有编码的测验表单数据的访问。
您将学习使用print_r()
来调试您的 PHP 代码。这个例子只是开胃菜。通常你不想在后台向用户展示传递给 PHP 的数据。您只需检查\(_POST,看看 PHP 看到了什么。现在您已经看到了,您可以注释掉检查\)_POST 的代码部分:
//partial code for views/quiz.php
//$output .= "<pre>";
//$output .=print_r($_POST, true);
//$output .= "</pre>";
如果和否则解释
测验依赖于用$_POST
编码的表单数据。它也依赖于if-else
语句。你已经看到过if-else
语句使用过几次。是时候得到关于这种条件语句的更详细的解释了。所有条件语句都遵循一定的模式。
if ( Boolean expression ) {
//code block
}
在任何条件语句的括号内,必须编写一个计算结果为TRUE
或FALSE
、0 或 1 的表达式。在计算机科学中,这样的表达式被称为布尔表达式。如果表达式计算结果为TRUE
,代码块将运行。如果表达式计算结果为FALSE
,代码块将不会运行。
通常,如果表达式是TRUE
就做一件事,如果表达式是FALSE
就做另一件事是很方便的。这可以通过一个else
代码块轻松实现。一般形式如下所示:
if ( boolean expression ) {
code block
} else {
another code block
}
如果表达式是TRUE
,第一个代码块将运行。第二个代码块如果是FALSE
就会运行。您可以在测验示例中看到它的工作情况。PHP 可以检查用户是否提交了表单。如果发生这种情况,PHP 代码将获得用户提供的答案并生成一个响应。如果没有,PHP 将简单地返回显示测验的 HTML。
评估测验回答
现在的测验回答提供的反馈不比鹦鹉提供的多。它只是重复用户选择的答案。有了 PHP,你可以做得更好。在编辑器中打开views/quiz.php
并更新showQuizResponse()
,如下所示:
function showQuizResponse( $answer ){
//changes begin here
$response = "<p>You clicked $answer";
if ( $answer === "yes" ){
$response .= " - I know exactly how you feel!";
}
$response .= "</p>";
//end of changes
$response .= "<p>
<a href='index.php?page=quiz'>Try quiz again?</a>
</p>";
return $response;
}
相同的比较运算符
在前面的代码示例之前,您没有见过任何===
。三个等号组成了 PHP 的相同比较运算符。它比较两个值是否相同。相同的比较运算符通常用于为if
语句制定条件。上一节示例中使用的条件实际上是指“如果用户的回答与‘是’相同。”"
Note
如果你在其他地方找到 PHP 代码示例,你会经常看到==
。双等号表示 PHP 的相等比较运算符。它几乎等同于相同的比较运算符。在大多数情况下,您可以互换使用这两者。在 http://php.net/manual/en/language.operators.comparison.php
了解更多。
卷毛定律:做一件事
你看过 1991 年的电影《都市滑头》吗?是的,那部由比利·克里斯托主演的感觉不错的西部喜剧。杰克·帕兰斯扮演卷毛,一个粗犷的老牛仔,他知道生活的秘密,并不情愿地与克里斯托的角色米奇分享:
- 卷毛:你知道生活的秘密是什么吗?
- (举起一根手指)
- 卷毛:这个!
- 米奇:你的手指?
- 一件事。只有一件事。你坚持这一点,其余的都不算什么。
- 米契:但是“一件事”是什么?
- 卷毛:(微笑)那是你必须去发现的。
我们可以放心,Curly 说的不是干净代码的原则。但是,顺便提一下,他提出了一个原则,我们可以用它来写干净的函数。每个函数都应该做一件事。只有一件事。
Note
Jeff Atwood 写了一篇关于将 Curly 定律应用于干净代码的有趣的博客文章。在 http://blog.codinghorror.com/curlys-law-do-one-thing/
念。
干净的代码是易于使用的代码。如果你的函数只做一件事,它们会很短。短代码通常比长代码更容易阅读和理解。如果你能阅读并理解你的代码,那么发现错误就变得容易多了——而你将会犯错误。如果你花了 50%的开发时间来追踪代码中的错误,不要感到惊讶。
在前面的测试示例中,您可以看到两个 clean 函数,每个函数只做一件事。一个函数显示测验;另一个函数显示一个响应。
有意义的名字
函数和变量名是任意的。你可以给他们打任何电话。在测验中,我们有以下内容:
if ( $quizIsSubmitted ){
$answer = $_POST['answer'];
$output = showQuizResponse( $answer );
} else {
$output = include_once "views/quiz-form.php";
}
我们可以重命名文件、函数和变量,而不会失去任何功能。例如,我们可以:
if ( $a ){
$c = d( $_POST['answer'] );
} else {
$c = include_once "views/e.php";
}
前面的代码很糟糕,因为这些名字根本没有表达意义。代码可以工作,但是很难阅读和理解。阅读这样的代码需要一个非常细心的读者。但是也有可能写出更糟糕的代码。您可以使用容易引起误解的名称。下面的代码仍然是测验示例,它仍然有效,但是变得很难阅读:
if ( $itIsLate ){
$output = goToSleep( $_POST['answer'] );
} else {
$output = include_once "views/coffee.php";
}
代码是诗歌
努力编写有表现力的、漂亮的代码。努力编写易读的代码。当你用代码开发新的解决方案时,你会花大量的时间阅读你自己的代码。代码就像诗歌。你写一遍,却读了很多遍。所以像写诗一样写你的代码:小心选择你的用词。
函数名和变量名应该是描述性的,准确的,不要过长。它们应该让你的代码更容易阅读和理解——而不是更难。通常,你会发现一个函数或变量无法用一个词来准确描述。我经常使用复合变量或函数名,比如$quizIsSubmitted
。我喜欢用骆驼大小写来写这样的名字:每个新单词都大写。Camel case 是一个相当常见的命名约定。我喜欢它,因为我发现 ?? 比 ?? 更容易阅读。
样式表单
第一次尝试设计表单样式时,可能会对不熟悉的元素名称感到困惑。但是您可以像处理任何其他 HTML 元素一样处理表单和大多数相关元素。通常可以完全避免使用id
和class
属性作为 CSS 钩子。使用 CSS 属性选择器,您的表单及其各种属性将为您提供大量的机会来选择您想要的元素。这里有一个让你开始的例子:
/*this selector will target the quiz form only*/
form[action='index.php?page=quiz']{
position:relative;
margin: 30px 10px;
}
/*select only <p> and <select> inside the quiz form*/
form[action='index.php?page=quiz'] p,
form[action='index.php?page=quiz'] select{
display:inline-block;
}
练习
锻炼你所学的是真正学习的好方法。下面是一些简单的练习,可以帮助你理解你所遇到的一些 PHP。其中一些练习可能看起来很简单。您已经编写了代码来解决更复杂的任务。但是从一本书上复制代码示例是一回事。从头开始编写自己的代码是完全不同的事情。
借此机会挑战自己。您可能会发现,通过编写自己的代码来解决简单的问题,您会学到很多东西——也许至少与通过本书中的示例学习一样多。
首先,您可以尝试创建一个外部样式表并将index.php
页面链接到该样式表。如果你忘了怎么做,参考第二章中的提示。
你也可以试着改进一下动态测验。改变views/quiz.php
并让它在用户选择 no 选项时输出有意义的响应怎么样?
您还可以编写另一个 HTML 表单,根据一个人的身高和体重来计算这个人的身体质量指数(身体质量指数)。计算身体质量指数的公式如下。您的任务是创建一个表单,用户可以在上面输入身高和体重,并编写一些 PHP 代码来计算基于输入的身体质量指数。
//metric
bmi = kg/ (2 * m)
//for UK and US readers
bmi = ( lb/(2 * in) ) * 703
最后但同样重要的是,您可以尝试编写一个将货币从一种货币转换为另一种货币的表单。如果你想让它变得更高级,你可以有一个<select>
元素,里面有一个可以转换的货币列表。
摘要
我们在这一章中涉及了很多内容。你已经学会了如何编写 HTML 表单。HTML 表单在提交时会对 URL 变量进行编码。URL 变量通过 HTTP 请求从浏览器传递到 web 服务器。您已经学习了如何使用GET
或POST
方法处理编码了 URL 变量的 HTTP 请求。您已经学会了用命名函数来组织代码。但最重要的是,您已经了解了 Curly 法则以及如何应用它来增强代码的美感。
四、使用图像上传构建动态图库
你知道如何制作一个简单的动态网站。你知道怎么写表格。你知道如何用$_GET
或$_POST
访问 URL 变量。我说是时候好好利用你的新知识了。让我们用一个允许用户上传新图片的表单来构建一个动态图片库。在这个过程中,你会学到不少东西。
- 建立一个动态网站。
- 编写命名函数。
- 使用
$_GET
和$_POST
超全局数组。 - 用一个
while
循环迭代。 - 使用 PHP 的原生
DirectoryIterator
类。 - 编写自定义对象方法。
- 用 PHP 的
$_FILES
超全局数组上传文件。 - 规划并编写一个易于上传文件的类。
建立动态网站
在XAMPP/htdocs
中为这一章创建一个名为ch4
的新项目文件夹。从第三章中复制templates
和classes
文件夹以及里面的 PHP 文件。创建新文件夹css
、views
和img
。
先决条件:一个有一些图像的文件夹
图片库应该有一些图片。此图像库将仅使用 JPEG 图像。为你的图库准备少量 JPEG 图片。将图像保存在img
文件夹中。
我们坚持使用与前几章相同的站点架构。这将使得重用以前项目中的代码变得更加容易。重用您自己的代码将帮助您更快地开发您的解决方案,这最终将使您成为更有价值的开发人员。
创建导航
该网站将有两个主要页面视图:一个用于显示图库,另一个用于显示允许用户上传新图像的表单。因为我们知道我们需要这两个页面视图,所以我们可以准备一个包含两个导航项的站点导航。在views
文件夹中创建一个新文件,并将其命名为navigation.php
。
<?php
return "
<nav>
<a href='index.php?page=gallery'>Gallery</a>
<a href='index.php?page=upload'>Upload new image</a>
</nav>
";
创建两个虚拟页面视图文件
在编写新代码时,从小处着手总是一个好主意。让我们准备两个单独的页面视图:一个用于图库,一个用于上传表单。每个页面视图将从单独的文件中生成和返回。因此,我们在views
文件夹中创建两个文件。
<?php
//complete source code for views/gallery.php
return "<h1>Image Gallery</h1>";
<?php
//complete source code for views/upload.php
return "<h1>Upload new images</h1>";
您会注意到 PHP 块没有结束分隔符,也就是说,代码中没有?>
。你可能还记得第一章中的内容,没有必要结束 PHP 代码块,除非你特别想在你的文件中写一些静态 HTML。只要你只写 PHP,你就不必结束你的 PHP 代码块。另一方面,如果您愿意,您可以用?>
结束 PHP 代码块。无论如何都没什么区别。
创建索引文件
每个网站都应该有一个索引文件。这将是用户唯一会请求的文件,因此,它就像是所有网站内容的大门。让我们创建一个index.php
文件,并显示一个链接到两个非常简单的页面视图的功能性动态导航。
<?php
//complete code for index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "classes/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "Dynamic image gallery";
$pageData->content = include_once "views/navigation.php";
$userClicked = isset($_GET['page']);
if ( $userClicked ) {
$fileToLoad = $_GET['page'];
} else {
$fileToLoad = "gallery";
}
$pageData->content .=include_once "views/$fileToLoad.php";
$page = include_once "templates/page.php";
echo $page;
测试时间到了
到目前为止,所有的代码都和我们在前几章所做的一样,所以它应该不会给你带来什么惊喜。总的来说,我们只有不到 20 行代码,但是这足以执行一个初始测试。当你在做代码的时候,建议你写一点代码,测试一下,然后再多写一点。
如果你经常测试你的进度,你就能在错误发生的初期就发现它们。在更少的代码行中更容易发现错误,所以让我们尽可能早地捕捉错误。
初学者可能很难知道什么时候测试以及会遇到什么情况。显然,不太了解 PHP 的初学者将很难预测一段 PHP 代码的行为。学会预测 PHP 代码的行为是很重要的。
学习这项技能的最好方法是发挥你的想象力:在你将http://localhost/ch4/
载入浏览器之前,试着猜测你会在那里看到什么。试着猜测你的网站在这一点上会有什么表现。
要执行测试,您必须打开您的 XAMPP 管理器并启动 Apache。Apache 运行后,您就可以将http://localhost/ch4/
加载到浏览器中了。一切如你所料吗?我希望你能看到一个非常基本的网站,动态导航显示两个链接的工作。如果你没有,你应该慢慢地阅读你的代码,寻找任何不合适的地方,例如,拼写错误的变量名。您还可以将您的代码与您以前使用动态导航的项目进行比较。
向一个页面添加两个样式表
在开发更大的网站时,使用多个样式表是很常见的。我们可以用如下代码从index.php
开始做:
$pageData->css = "<link href='css/layout.css' rel='stylesheet' />";
$pageData->css .= "<link href='css/navi.css' rel='stylesheet' />";
您可能还记得,$pageData
对象是在包含templates/page.php
之前在index.php
中创建的。$pageData->css
房产在templates/page.php
使用。本质上,templates/page.php
就像 HTML5 页面的骨架。通过提供在 HTML5 页面中使用的内容,$pageData
对象提供了骨骼的所有肌肉。骨骼和肌肉在index.php
处连接。
上面的代码会工作得很好。但是我想借此机会向您介绍另一个干净代码原则:保持干燥。
保持干燥
所有优秀的程序员都试图保持干燥。DRY 是“不要重复自己”的缩写。当你发现自己在写重复的代码时,你应该经常停下来想一想:有没有一种方法可以重新组织这段代码以避免重复?前面的例子重复了生成<link>
元素的代码。我在这里强调了重复的代码。
$pageData->css =``"<link href='``css/layout.css
$pageData->css .=``"<link href='``css/navi.css
重复是丑陋的!有更聪明的方法来解决这个代码问题。恰好有一种更聪明的方法涉及到一个非常强大的概念:对象方法。让我们实现一个添加样式表的方法。
用方法改进 Page_Data 类
对象方法就像函数一样。最大的区别是方法是在类定义中声明的,通常用于对象属性。这里有一个方法可以让Page_Data
类在添加样式表时避免重复:
<?php
//complete code listing for classes/Page_Data.class.php
class Page_Data {
public $title = "";
public $content = "";
public $css = "";
public $embeddedStyle = "";
//declare a new method
public function addCSS( $href ){
$this->css .= "<link href='$href' rel='stylesheet' />";
}
}
要添加样式表,您必须生成一个具有唯一的href
属性的<link>
元素。要添加另一个样式表,您必须用另一个唯一的href
属性生成另一个<link>
元素。这两个<link>
元素是相同的,除了href
属性。
对象方法addCSS()
利用了这一点。该方法需要一个表示href
属性的参数。每次调用该方法时,都会创建一个新的<link>
元素。所有创建的<link>
元素将一起存储在对象的$css
属性的一个字符串中。
是函数还是方法?
您可以看到方法是用关键字function
声明的。事实上,前面的方法看起来就像您已经编写的一个命名函数。功能和方法几乎相同。唯一的语法区别是方法是在类定义中声明的函数。除此之外,没有明显的区别。
这是什么?
当我提到我的个人财产时,我会用“我的”——比如我的头发、身高和体重。my 这个词是一个自我指称。当从类定义内部引用 PHP 对象属性时,必须使用关键字$this
:这是 PHP 对象用于自我引用的单词。所以,$this->css
是对象对其$css
属性的内部引用。
在前面的方法中,可以看到使用$this
关键字的必要性。没有它,您就不能引用$css
属性。如果你不能引用$css
属性,对象就不会记得创建的<link>
元素。
使用新方法
使用index.php
中的新方法将非常简单。在一个新的Page_Data
对象被创建之后和最终的echo
之前,你可以添加两行代码来添加两个样式表到index.php
页面。
$pageData->addCSS('css/layout.css');
$pageData->addCSS('css/navigation.css');
您需要两个单独的样式表来测试此时是否一切正常。让我们创建两个非常简单的样式表来测试一些东西。
/* code listing for css/layout.css */
h1{color:red;}
/* code listing for css/navigation.css*/
nav a{text-decoration: none; color:black}
nav a:hover{text-decoration: underline;}
现在保存您的文件并在浏览器中加载http://localhost/ch4/index.php
。如果一切正常,所有的<h1>
元素应该是红色的,导航项目应该是黑色的,并且没有下划线,直到你用鼠标悬停它们。这个设计肯定不好看,但是它展示了新的 object 方法的预期效果。
您只能使用声明的方法
PHP 只能按预期使用。您只能使用在类定义中实际声明的方法。在代码示例中,您声明了一个用于添加样式表的方法,因此现在可以添加样式表了。您不能向您的$pageData
对象添加铅笔。
//this would trigger an error
$pageData->addPencil( "2b" );
这似乎是一个非常明显的观察,但是重要的是要认识到,对象只有当你在类定义中声明方法时才具有你给它们的行为。如果您试图在一个对象上调用一个方法,而这个对象的类定义中没有声明这个方法,PHP 将显示一个错误消息。
准备显示图像的功能
在编辑器中打开views/gallery.php
文件,并声明一个简短的函数,该函数简单地返回一个 HTML 字符串,该字符串包含一个带有一个<li>
的<ul>
。
<?php
//complete source code for views/gallery.php
//function call
return showImages();
//function defintion
function showImages(){
$out = "<h1>Image Gallery</h1>";
$out .= "<ul id='images'>";
$out .= "<li>I will soon list all images";
$out .= "</ul>";
return $out;
}
看看函数showImages()
中的变量$out
如何使用增量连接,在几行代码中逐渐获得越来越多的内容。最后,当 HTML 字符串完成时,变量$out
被返回给调用者。调用者将是你的代码中调用函数showImages()
的地方,也就是在views/gallery.php
的开头。
在views/gallery.php
的开头可以看到一个return
语句。一旦 HTML 字符串从showImages()
返回,整个事情就返回到index.php
,因为views/gallery.php
是从index.php
开始包含的。本质上,生成的字符串被返回到index.php
,在那里它将被添加到$pageData
对象,与页面模板合并,并回显到浏览器。
循环
我希望函数、变量和增量连接慢慢开始对您有意义。是时候关注迭代了:在代码中重复一些东西。让我们从while
循环开始。while
只要条件为真,循环将重复相同的代码块。基本语法是
while ( $condition ) {
//repeat stuff here
}
一个while
循环在语法上非常类似于一个if
语句。如果条件成立true
,后续代码块中的代码将重复;它会循环。这里有一个简单的例子来说明这个概念:
<?php
$number = 1;
while ( $number < 5 ) {
echo "the while loop has concluded $number loops<br />";
$number = $number + 1;
}
如果您要运行这段代码,您将会在浏览器中看到如下四行内容:
the while loop has concluded 1 loops
the while loop has concluded 2 loops
the while loop has concluded 3 loops
the while loop has concluded 4 loops
请注意,代码块重复了四次。这是因为括号内声明的条件。它表示只要$number
小于 5,代码块就将重复。代码块每运行一次,$number
就加 1。当while
循环重复四次时,变量$number
的值为 5。因为 5 不小于 5,所以while
循环终止,第五行永远不会被回显。
循环非常适合多次重复相同的操作。PHP 中还有其他种类的循环结构。如果您从其他书籍或互联网上查找 PHP 代码,您可能会遇到它们。其他类型的循环都非常类似于while
循环,尽管语法不同。
使用目录运算符在文件夹中查找文件
我们可以使用一个while
循环为在img
文件夹中找到的每个JPEG
文件创建<img>
元素。但是while
循环不能自己查看文件夹。我们可以使用一个专门为查找文件夹而设计的本地 PHP 对象:它被称为DirectoryIterator
。
迭代只是“循环”的一个技术术语,就像目录是“文件夹”的一个技术名称一样所以,你可以从它的名字猜测出一个DirectoryIterator
可以遍历文件夹中的文件。这就是它所做的一切,而且做得非常好。这里有一个一般的例子:
$filesInFolder = new DirectoryIterator( $folder );
$numItemsInFolder = 0;
while ( $filesInFolder->valid() ) {
$numItemsInFolder = $numItemsInFolder + 1;
$filesInFolder->next();
}
echo "found $numItemsInFolder items in folder named $folder";
看到while
循环的条件了吗?你正在调用DirectoryIterator
对象的方法valid()
。如果DirectoryIterator
对象当前指向文件夹中的一个有效项目,它将返回 true。你大概能猜到方法next()
是做什么的。这将使DirectoryIterator
指向文件夹中的下一项。
因此,通过将一个while
循环与$filesInFolder->valid()
和$filesInFolder->next()
相结合,您可以构建一个循环,重复一个代码块,重复的次数与文件夹中的项目一样多。前面的代码将做到这一点。
显示所有图像
让我们在图库中实现一个类似的代码块。下面是我的最终版本:
//edit existing function
function showImages(){
$out = "<h1>Image Gallery</h1>";
$out .= "<ul id='images'>";
$folder = "img";
$filesInFolder = new DirectoryIterator( $folder);
while ( $filesInFolder->valid() ) {
$file = $filesInFolder->current();
$filename = $file->getFilename();
$src = "$folder/$filename";
$fileInfo = new Finfo( FILEINFO_MIME_TYPE );
$mimeType = $fileInfo->file( $src );
if ( $mimeType === 'image/jpeg' ) {
$out .= "<li><img src='$src' /></li>";
}
$filesInFolder->next();
}
$out .= "</ul>";
return $out;
}
如果保存并运行这段代码,您将会看到 PHP 生成了一个包含<img>
元素的列表,显示了文件夹中所有的 JPEG 图像。如果您的文件夹中有两张 JPEG 图像,您将在您的在线画廊中看到这两张图像,如果您有十张 JPEG 图像,您将看到十张图像。这一切都是因为函数showImages()
而动态发生的。
Note
used Finfo
对象在 PHP 5.3.0 中是默认启用的,但在某些 PHP 安装中可能被禁用。或者,您可以使用mime_content_type( $src )
来获取文件的 mime 类型,但是您应该知道mime_content_type()
已经被弃用了:您不能相信它将来会工作。您可以在该书的配套网站上使用mime_content_type
()找到源代码。
使用一点 PHP,您可以创建对您的客户更有吸引力的解决方案。想一想保持这个图片库的更新有多容易?您的客户只需在正确的文件夹中多放几张图片,图库就会更新。
创建表单视图
你可以写一些 CSS 让画廊更漂亮。我们将会谈到这一点,但首先,我想向您展示如何通过 HTML 表单向图库上传新图像。让我们从显示一个表单开始。从某种意义上说,表单就像站点导航:它是静态 HTML 的一部分,不需要修改。在views
文件夹中为其创建一个单独的文件。调用文件upload-form.php
,如下:
<?php
return "
Upload new jpg images</h1>
<form method='post' action='index.php?page=upload' enctype='multipart/form-data' >
<label>Find a jpg image to upload</label>
<input type='file' name='image-data' accept='image/jpeg'/>
<input type='submit' value='upload' name='new-image' />
</form>";
前面的一些应该看起来很熟悉。我们有一个带有method
和action
属性的 HTML 表单。但是这个表单和你之前写的表单有点不一样。
您注意到为表单声明的enctype
属性了吗?表单使用的默认编码不允许文件上载。我们必须明确声明这个特殊的表单应该使用multipart/form-data
作为content-type
,因为这是通过 HTTP 上传文件所必需的。
另一个显著的不同是新的输入属性type='file'
。它将创建一个文件上传控件,允许用户浏览他们自己的图像文件上传硬盘。还请注意同一个<input>
元素上的accept
属性。它实际上声明了唯一可以通过这个表单上传的文件是带有image/jpeg
的content-type
的文件。
声明一个accept
属性对最终用户非常有帮助。当它被声明时,它将缩小用户可以通过表单选择的文件的范围。帮助用户选择具有适当文件类型的文件。你应该知道accept
属性是不被老版本的浏览器支持的。所以,使用旧浏览器的用户不会得到accept
属性带来的额外好处。但这不会损害表单的基本功能:无论使用哪种浏览器,所有用户都可以选择上传一个文件。
Note
accept
属性可以用于任何互联网媒体类型。互联网媒体类型是识别文件类型的标准方式。在 http://en.wikipedia.org/wiki/Internet_media_type
查看更多互联网媒体类型。
显示用于上传图像的表单
要实际显示上传表单,您必须在适当的时候包含 HTML 片段。您希望当用户单击“上传新图像”导航项目时显示表单。因此,要显示表单,您必须更新views/upload.php
中的代码,如下所示:
<?php
//complete source code for views/upload.php
$output = include_once "views/upload-form.php";
return $output;
如果你保存你的工作并在你的浏览器中加载http://localhost/ch/index.php?page=upload
,你可以看到一个文件上传控件的样子,但是不要期望能够真正上传文件。
Note
文件上传控件在不同的浏览器上会有不同的呈现方式。通常,您会使用一些自定义 CSS 来设计 HTML 元素的外观,但是文件上传控件很难进行样式化。如果你愿意,可以在互联网上搜索解决方案,并准备在多种浏览器和浏览器版本中严格测试你的设计。
$ _ 文件
当您试图通过 HTML 表单上传文件时,可以通过名为$_FILES
的 PHP 超全局数组访问文件日期。在实际上传文件之前,让我们看看 PHP 看到了什么。您可以使用print_r()
来检查$_FILES
,就像您在上一章中使用它来检查$_POST
一样。更新views/upload.php
,如下:
<?php
//complete source code for views/upload.php
//$newImageSubmitted is TRUE if form was submitted, otherwise FALSE
$newImageSubmitted = isset( $_POST['new-image'] );
if ( $newImageSubmitted ) {
//this code runs if form was submitted
$output = upload();
} else {
//this runs if form was NOT submitted
$output = include_once "views/upload-form.php";
}
return $output;
//declare new function
function upload(){
$out = "<pre>";
$out .=print_r($_FILES, true);
$out .= "</pre>";
return $out;
}
在views/upload.php
中声明一个新函数,并在文件顶部添加一个条件语句。HTML <pre>
元素将保留文本格式,比如制表符。原生 PHP 函数print_r()
会输出一个数组,这样人眼就能读懂。
这足以让您测试您的上传表单。保存您的工作并在浏览器中运行。通过表单选择某个.jpg
文件,您应该会看到如下输出。
Array (
[image-data] => Array (
[name] => alberte-lea.jpg
[type] => image/jpeg
[tmp_name] => /Applications/XAMPP/xamppfiles/temp/phpYPcBjK
[error] => 0
[size] => 119090
)
)
从那个输出中,你可以扣除相当多。你可以看到$_FILES
是一个array
。在前面的示例中,$_FILES
有一个索引:image-data
。需要注意的是,它之所以被称为image-data
,是因为表单中的文件上传控件元素的name
属性被设置为image-data
。
在$_FILES['image-data']
里面,还有另外一个数组,有五个索引:name
、type
、tmp
_ name
、error
、size. name
、type
、size
索引应该是显而易见的,剩下的可能第一次碰到就有点晦涩难懂了。
tmp_name
上传表单时,其文件数据将临时存储在 web 服务器的内存中。PHP 可以通过$_FILES
['image-data']['tmp_name']
访问临时存储的文件数据。您必须访问文件数据才能将临时文件永久保存在服务器的文件系统上。
错误
上传文件时可能会出错。该图片库运行在安装了 XAMPP 的本地 web 服务器上。您可能遇到的最常见的问题可能是过于严格的文件权限设置。如果您遇到上传错误,您可以检查$_FILES
[ ['image-data']['error']
以获得相关的错误代码。可以在 www.php.net
查阅 PHP 手册。它可以帮助您理解遇到的任何错误代码的含义。在本书的后面,我将向您展示如何以编程方式处理上传错误。
用 PHP 上传文件
用 PHP 将文件上传到 web 服务器很简单。您只需访问临时文件数据并永久保存它。在此过程中,您必须指明要保存在哪个文件夹中,以及要另存为什么文件名。有一个本地 PHP 函数可以做到这一点。
move_uploaded_file( $fileData, $destination );
该函数有两个参数。第一个是$fileData
,应该保存有效的文件数据。第二个文件夹$destination
应该是一个现有的可写文件夹。函数move_uploaded_file()
返回一个布尔值。如果文件保存成功,它将返回TRUE
,如果出错,将返回FALSE
。
规划上传者课程
作为一名 PHP 开发人员,在您的一生中,您可能需要多次编写代码来上传文件。以这样一种方式编写一些用于上传的代码是一个好主意,这样您可以在以后的项目中轻松地重用它。对象很容易重用,所以计划一个可以重用的类,通过 PHP 对象上传文件。
用户模式
我喜欢使用简单的 UML 类图来规划类。你可以在图 4-1 中看到虚类的基本符号。
图 4-1。
Basic UML diagram
注意类名的命名约定:名字总是以大写字母开头。如果类名是一个复合词,用下划线分隔单词,第二个单词以大写字母开头。
Note
UML 是“统一建模语言”的缩写该语言为记录代码提供了标准语法。UML 不仅仅是这些类图。
上传者类别要求
您知道您将需要保存从表单接收的文件数据。因此,该类需要一个存储文件数据的属性和一个将其保存为文件的方法。您知道文件需要一个名称,所以这个类需要一个$filename
属性。最后,你知道文件必须保存在某个地方。您需要一个属性来记住保存文件的$destination
,并且您可以添加一个方法来指定保存位置。了解了这些需求,您就可以开始计划新的类定义了。你可以绘制一个 UML 类图,如图 4-2 所示。
图 4-2。
UML diagram of the Uploader
class
有了计划和 UML 类图,就很容易开始编写类定义。创建一个新文件classes/Uploader.class.php
,如下所示:
<?php
class Uploader {
private $filename;
private $fileData;
private $destination;
public function saveIn( $folder ) {
$this->destination = $folder;
}
public function save(){
//no code here yet
}
}
前面的代码声明了一个类,该类具有用花括号分隔的类名和类代码块。在该类中,声明了三个属性和两个方法。很容易看出前面的类框架是基于 UML 类图的。
从代码中可以看到,每当调用方法saveIn
时,属性destination
就会获得它的值。属性filename
和fileData
没有任何值。您可以从超级全局数组$_FILES
中获得filename
和fileData
值。如果每当一个新的Uploader
对象被创建时,它们就能得到值,那么它们的值就能反映出你想在那时上传的任何文件,这就太好了。
__
构造魔法()
碰巧的是,您可以声明一个只运行一次的方法,无论何时创建一个新的Uploader
对象。用面向对象的术语来说,这样的方法叫做构造函数。在 PHP 语法中,它被称为__construct
。这是一种所谓的神奇方法。请注意在方法名前有两个下划线字符。为Uploader
声明一个构造方法,这样每当创建一个新的Uploader
对象时,filename
和fileData
属性就可以从$_FILES
中获取它们的值。
<?php
//complete code for classes/Uploader.class.php
class Uploader {
private $filename;
private $fileData;
private $destination;
//declare a constructor method
public function __construct( $key ) {
$this->filename = $_FILES[$key]['name'];
$this->fileData = $_FILES[$key]['tmp_name'];
}
public function saveIn( $folder ) {
$this->destination = $folder;
}
public function save(){
//no code here yet
}
}
还记得您必须知道用于上传文件的<input type='file'>
元素的name
属性吗?您需要name
属性来访问$_FILES
中的所有文件数据。在前面的代码中,构造函数方法将一个$key
作为参数。$key
的值应该与name
属性的值相同。有了这些,构造函数方法就可以访问上传文件的数据,这些数据只驻留在服务器的内存中,直到被保存。
保存上传的文件
课程即将完成。您只需完成保存新文件的方法。当您在本地 web 服务器上工作时,在执行文件上载时可能会遇到一个常见问题:目标文件夹可能不可写。因为您可以预料到该特定错误,所以您可以在代码中为其做好准备,如下所示:
//partial code for classes/Uploader.class.php
//edit the save method in the Uploader class
public function save(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble ){
$name = "$this->destination/$this->filename";
$succes = move_uploaded_file( $this->fileData, $name );
} else {
trigger_error("cannot write to $this->destination");
$succes = false;
}
return $succes;
}
通读这段代码,您可能会猜到 PHP 将检查目标文件夹是否可写。如果不是,PHP 将触发一条错误消息,告诉您哪里出错了。换句话说,如果您遇到这个特定的上传错误,PHP 将显示一条错误消息。错误消息是好的;它们帮助您诊断代码中的错误。
使用 Uploader 类
您现在可以充分利用Uploader
类并上传一个文件。在views/upload.php
中不需要很多代码,因为大多数代码都是在Uploader
类中编写的。
//partial code for views/upload.php
//edit existing function in views/upload.php
function upload(){
include_once "classes/Uploader.class.php";
//image-data is the name attribute used in <input type='file' />
$uploader = new Uploader( "image-data" );
$uploader->saveIn("img");
$fileUploaded = $uploader->save();
if ( $fileUploaded ) {
$out = "new file uploaded";
} else {
$out = "something went wrong";
}
return $out;
}
这有多酷?你有一个完全动态的图片库,用户可以通过网站上传自己的图片。也许还不完全是这样,但是我希望你会同意你真的开始使用 PHP 来创造一些有趣和有用的东西。
什么会出错?
我在学习在本地 web 服务器上通过 PHP 上传文件的学生中看到的最常见的错误是,目标文件夹的文件权限设置过于严格。如果您遇到这个问题,Uploader
对象将触发一个 PHP 错误并通知您。解决方案很简单:更改目标文件夹的权限设置,这样每个人都有读/写权限。
另一个常见问题发生在 PHP 通过$_FILES
找不到任何文件数据的时候。如果在创建一个新的Uploader
对象时提供了一个错误的$key
,这种情况就会发生。创建新的Uploader
对象时,必须向Uploader
构造函数传递一个参数。该参数必须包含 HTML 表单中文件上传控件的name
属性。在前面例子中使用的<form>
中,有一个文件上传控件。
<input type='file' name='image-data' />
要上传通过那个<input>
元素接收的文件,您需要一个知道在哪里寻找比特流的Uploader
对象。当Uploader
对象被创建时,您必须将相关的name
属性值作为参数传递。在这种情况下,您必须使用字符串image-data
,如下所示:
$uploader = new Uploader("image-data");
单一责任原则
我希望你惊叹于Uploader
类定义的美丽。它的设计和编写只有一个目的:上传文件。它有三个属性和两个方法。属性是关于要上传的文件的,方法是关于上传文件的。
单一责任原则是面向对象编程中常用的原则。单一责任原则声明一个类应该为单一的目的而编写。该类的所有属性和方法都应该直接与这个目的相关。这个类应该只有一个改变的理由。
例如:Uploader
只有一个改变的理由。如果你想用它上传不同的文件,它会改变。单一责任原则是在代码中为之奋斗的美好理想。这又是真正的卷毛定律,只是这一次,应用于面向对象编程。
Note
你可以在 http://en.wikipedia.org/wiki/Single_responsibility_principle
了解更多关于单一责任原则的内容。
你已经直觉地知道单一责任原则是一个好主意。如果你买了一台多功能厨房实用机器,可以制作咖啡和冰淇淋,烤面包和炸香肠,你会认为它制作的咖啡相当糟糕。事实上,你可以相信它会很糟糕地完成所有的动作。如果你想要很棒的咖啡,你会给自己买一台只有一个目的的机器:咖啡机!
如果你欣赏好咖啡,你可能会想到一些煮不出好咖啡的咖啡机。你正在学习 PHP,这样你就可以计划和构建类来做一件伟大的事情。
摘要
在这一章中,你已经看到了如何使用对象和对象方法创建一个动态图库。您已经看到了一个原生 PHP 对象,并学会了使用它的一些方法,但是您还声明了一个带有属性和方法的自定义类定义。您甚至试图使用while
循环来自动重复代码。
您现在已经编写了两个类定义:Uploader
和Page_Data
。我怀疑你没有完全理解类、对象、属性和方法是什么。在本书中,你将接触到更多的类和对象。在接下来的几页中有大量的例子和解释等着你,所以坚持住。学习需要时间,我们才刚刚开始。。。
现在你已经看够了基本的 PHP。随着您对数据库驱动的动态网站越来越熟悉,我们将很快继续探索数据库,并了解您可以在项目中实现的新的可能性。但是首先,我们将进行一次简短而激烈的迂回,涉及一点 JavaScript 和客户端脚本。
五、使用 JavaScript 和 CSS 给你的图片库增添趣味
这一章完全是选读!你可以把它看作是来自 PHP 的一个小弯路的邀请。本章探索了一种将 JavaScript 集成到 PHP 项目中的方法。在这个过程中,你将不得不学习一些 JavaScript 来开发一个交互式图库。你可以探索这条弯路,或者干脆跳过它。
如果你关注任何关于网页设计和网页开发的博客,你一定会遇到一些涉及 JavaScript 的东西。也许你会遇到一些你想在你的 PHP 项目中实现的东西。这并不是一本真正关于 JavaScript 的书,但是我将向您展示一些使用 JavaScript 的例子。我的目标是向您展示一种在 PHP 项目中集成 JavaScript 的方法。如果你真的想学习 JavaScript,你必须参考其他资源。
Note
想了解更多关于浏览器中 JavaScript 的知识?考虑拿一本 Rex van der Spuy 的《HTML5 和 JavaScript 基础游戏设计》(Apress,2012)。您将使用 HTML5、CSS 和 JavaScript 为浏览器构建游戏。这是一种有趣的学习方式!
客户端与服务器端编程
PHP 是一种很好的 web 开发语言。它非常受欢迎有很多原因。但是 PHP 只是众多服务器端脚本语言中的一种。服务器端语言只在您的服务器上运行;没有办法在浏览器中执行 PHP 代码。到目前为止,您已经编写了输出 HTML 的 PHP 代码,HTML 被发送到浏览器。
为了更改生成的 HTML 中的任何内容,浏览器必须向服务器发送一个 HTTP 请求,这样 PHP 就可以运行并发回一个 HTTP 响应。但是发送一个 HTTP 请求,等待 PHP 运行,最后接收一个 HTTP 响应需要时间和带宽。
在某些情况下,当系统需要时,简单地运行一些代码会更好。幸运的是,有一种方法:可以使用 JavaScript 在浏览器中以编程方式操作 HTML,JavaScript 恰好是唯一一种在浏览器中本地运行的脚本语言。您可以在服务器端选择许多不同的语言。在客户端,也就是在浏览器中,只有一个:JavaScript。
JavaScript 是一种奇妙的语言,但是不同版本的不同浏览器以不同的方式实现了 JavaScript 的不同部分。因此,在一个浏览器中运行良好的 JavaScript 可能会在另一个浏览器中引发令人尴尬的错误。
处理这些差异的一种常见方法是使用渐进式增强,这基本上意味着您以这样一种方式编写代码,即您漂亮的 JavaScript 只能在完全理解它的浏览器中运行。
JavaScript 的渐进式增强为启用了 JavaScript 的现代浏览器提供了最佳的用户体验。旧浏览器或禁用了 JavaScript 的浏览器仍然可以提供所有内容。额外的 JavaScript 特性应该对不支持的浏览器保持隐藏,这样可以避免 JavaScript 错误。本章介绍的图像库使用渐进式增强。
编码灯箱画廊
让我们为图库编写一个所谓的 lightbox,以一种美观的方式呈现图像。灯箱是一种非常常见的显示图像的方式。当用户点击网页上的一个小图片时,JavaScript 会在所有页面内容上放置一个半透明的覆盖层。点击图像的大版本将显示在覆盖图的顶部。
从服务器端的 PHP,您将继续为每个访问浏览器提供所有 JPEG 图像的列表。但是如果用户有一个顶级的浏览器,你可以提供一个更好的解决方案:所有图片的小缩略图,这样用户可以快速浏览整个图库。如果用户点击一个缩略图,你真的可以把焦点放在那个特定的图片上。您可以将所有其他内容隐藏在半透明的覆盖图后面,真正突出显示所选图片。你甚至可以显示点击图像的更大版本:这是一个灯箱画廊。
这就是渐进式改进的意义所在:为所有浏览器提供所有内容,但为有能力的浏览器提供更好的用户体验。我们开始吧!
嵌入外部 JavaScript 文件
可以直接在 HTML 中编写 JavaScript 代码,但不推荐这样做。更好的方法是保持 HTML 和 JavaScript 的分离。可以在一个 HTML 文件中嵌入几个 JavaScript 文件,就像可以将多个样式表链接到一个 HTML 文件一样。要将 JavaScript 链接到 HTML 文件,可以使用一个<script>
元素:
<script src="path/to/Javascript-file.js"></script>
属性应该指向一个现有的 JavaScript 文件,所以路径正确是很重要的。您可能想知道为什么<script>
元素是一个容器标签?这是因为您可以决定在 HTML 文件中的<script>
元素中直接编写 JavaScript 代码。
Note
使用外部 JavaScript 文件并小心地避免 HTML 中的任何 JavaScript 代码也被称为不引人注目的 JavaScript。
从第四章的中,你已经有了一个 PHP 驱动的动态图库。向该项目添加一些 JavaScript 应该会让您对使用 JavaScript 可以做的一些事情有一个很好的了解。本章中的代码示例依赖于你拥有在第四章中开发的图库的 PHP 源代码。
为 JavaScript 文件准备 Page_Data 类
您可以更改 PHP 代码,为一个或多个 JavaScript 文件做准备。现有的Page_Data
类需要一个属性来保存一个或多个<script>
元素。您还可以在Page_Data
类中声明一个新方法来添加新的 JavaScript 文件。它将非常类似于保存样式表引用的<link>
元素的属性和添加新样式表的方法。我建议你继续做你在第四章开始的项目,所以要更新的文件是ch4/classes/Page_Data.class.php
。下面是它的完整代码:
<?php
//complete code listing for classes/Page_Data.class.php
class Page_Data {
public $title = "";
public $content = "";
public $css = "";
public $embeddedStyle = "";
//declare a new property for script elements
public $scriptElements = "";
//declare a new method for adding Javascript files
public function addScript( $src ){
$this->scriptElements .= "<script src='$src'></script>";
}
public function addCSS( $href ){
$this->css .= "<link href='$href' rel='stylesheet' />";
}
}
请注意名为$scriptElements
的新公共属性。它将容纳页面所需的任意数量的<script>
元素。还要注意公共函数addScript()
。看看它如何把一个$src
作为参数。$src
应该保存一个 JavaScript 文件的路径。接收到的路径将用于创建一个<script>
元素。创建的<script>
元素将通过增量连接的方式与任何先前添加的<script>
元素一起存储。
为 JavaScript 文件准备页面模板
您必须更新页面模板文件以接受 JavaScript 的<script>
元素,就像您更新页面模板以接受 CSS 的<link>
元素一样。编辑template/page.php
:
<?php
//complete code listing for templates/page.php
return "<!DOCTYPE html>
<html>
<head>
<title>$pageData->title</title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
$pageData->css
$pageData->embeddedStyle
</head>
<body>
$pageData->content
$pageData->scriptElements
</body>
</html>";
PHP 将通过$pageData->scriptElements
的方式嵌入脚本元素。请注意,任何<script>
元素都将放在页面上任何其他内容之后。这样做的时候,可以确保在 JavaScript 开始执行之前,所有的 HTML 元素都被加载到浏览器内存中。这正是我们想要的!
JavaScript 经常被用来操作 HTML。在我们操作它之前,有必要将 HTML 加载到浏览器内存中。
编写和运行外部 JavaScript 文件
我喜欢把我的 JavaScript 文件放在一个指定的文件夹中,以保持一个组织良好的文件结构。我建议你也习惯这样做。创建一个名为js
的新文件夹。使用您的编辑器创建一个名为lightbox.js
的新 JavaScript 文件。把它保存在你的js
文件夹里。
//complete code listing for js/lightbox.js
window.console.log("Hello from Javascript");
要运行 JavaScript 代码,必须告诉浏览器有一个 JavaScript 要运行。您可以从index.php
指向一个外部 JavaScript 文件。您将要编写的 JavaScript 将操纵您的 HTML,因此某些属性会动态地改变。
您还需要一个外部样式表。下面是来自index.php
的一小段代码,展示了如何指向外部样式表以及如何指向外部 JavaScript。这些代码行属于在新的$pageData
对象被创建之后和生成的$page
被回显之前的index.php
:
//partial code listing for index.php
//this line of code you already have. It creates a Page_Data object
$pageData = new Page_Data();
//new code below
//add this new line to embed an external Javascript file to your index.php
$pageData->addScript("js/lightbox.js");
//no other changes in index.php
您有一个外部 JavaScript,并且您已经从index.php
链接到它。您编写的任何 JavaScript 代码现在都应该可以完美运行了。如果你使用 Google Chrome 浏览器或者其他类似 JavaScript 控制台的浏览器,测试起来非常简单。我建议你使用谷歌浏览器,除非你已经习惯了另一个带有 JavaScript 控制台的浏览器。
首先,打开谷歌 Chrome。接下来,通过点击浏览器右上角的打开 Chrome 菜单。选择工具➤ JavaScript 控制台。当控制台打开时,只需在 Chrome 浏览器中加载 http://localhost/ch4/index.php。您应该会在控制台中看到一条消息,如图 5-1 所示。
图 5-1。
The JavaScript Console in Google Chrome
使用 window.console.log()
如您所见,控制台中的消息与您在 JavaScript 代码中编写的完全相同:
window.console.log("Hello from Javascript");
在 JavaScript 中,window
是一个表示打开的浏览器窗口的对象。在window
对象中,你可以找到console
对象。console
对象有一个在 JavaScript 控制台窗口中输出消息的方法log()
。记录到控制台通常用于检查一些 JavaScript 是否按预期工作。
在本例中,您使用它来检查 JavaScript 是否运行。如果您在控制台中没有看到消息,那么您知道您的 JavaScript 没有运行。在这种情况下,您可以检查index.php
的 HTML 源代码,看看是否找到链接到现有 JavaScript 文件的<script>
元素。也许没有<script>
元素,或者它的src
属性没有指向您的 JavaScript 文件。请确保您已经正确完成了前面的所有步骤。
从您编写的这一行 JavaScript 代码中,您可以推断出 JavaScript 类似于 PHP,因为它有对象和方法。您还可以看到 JavaScript 语法有一点不同。JavaScript 的对象操作符是一个.
,而 PHP 使用的是->
。如果 JavaScript 具有与 PHP 完全相同的语法,您应该编写以下代码:
//If Javascript had PHP’s object operator
window->console->log("hello");
我希望你能注意到功能上的相似之处和句法上的不同。JavaScript 在许多方面与 PHP 非常相似,但语法略有不同。在某些方面,JavaScript 和 PHP 确实不同,但那是另一回事了。
JavaScript 数组
您已经尝试在 PHP 中使用数组:$_GET
和$_POST
是超全局数组。让我们仔细看看数组,看看如何在 JavaScript 中使用它们。打开您的lightbox.js
文件并编写一些 JavaScript,如下所示:
var pets = new Array("cat", "dog", "canary");
var firstPet = pets[0];
window.console.log( "The first pet is at index 0\. It is a " + firstPet);
使用 var 声明变量
首先声明一个变量pets
并分配它来保存一个新的数组对象。您使用关键字var
来声明一个 JavaScript 变量。新的数组对象包含一个由三个字符串值组成的列表。这就是数组所能做的:它们可以保存一个项目列表。
为了对数组做任何有意义的事情,你必须能够在正确的时间获得正确的数组项。数组项是根据它们在列表中的位置来记忆的。这种职位的专业术语是index
。数组中的第一项的index
为 0,第二项的index
为 1,依此类推。从数组中获取项的一般语法如下:
arrayName[index];
如果您回头看看 pets 示例的代码,您可以看到变量firstPet
保存了在pets
数组中的index
0 处找到的项目。再说一次,JavaScript 非常类似于 PHP。要从 PHP 数组中获取一个项目,我们可以使用完全相同的语法。
遍历数组项
循环通常与数组一起使用。通过一个简单的while
循环,您可以遍历数组中的所有元素:
var pets = new Array("cat", "dog", "canary");
var index = 0;
while ( index < pets.length ) {
window.console.log( pets[index] );
index = index + 1;
}
再一次,您可以看到 JavaScript 和 PHP 是非常相似的语言:它们都可以使用while
循环。前面的循环将遍历pets
数组中的每一项,并将每一项输出到控制台。
在第一次迭代中,变量index
将保存值 0,因此,代码输出“cat”,这是在pets[0]
找到的项目。
while
循环的条件规定,只要index
小于pets
数组的长度,换句话说,只要index
小于 3,循环就会继续。
在第一次迭代结束时,index
的值被改变。它开始的值是 0,现在变成了 1,因为index = 0 + 1
。在下一次迭代中,代码将向控制台输出“dog ”,因为在pets[1]
找到了dog
。变量index
变成 2,循环继续,于是“金丝雀”出现在控制台上。
现在,index
变为 3,因此while
循环终止,因为 3 不小于 3。
简单渐进增强
在这个 lightbox 脚本中,您希望 JavaScript 为使用新浏览器的用户提供更好的体验。您可以通过响应只有相对较新的浏览器才能理解的事件来做到这一点。您可以在js/lightbox.js
中这样做,如下所示:
//complete code for js/lightbox.js
function init(){
window.console.log("Welcome, user with a new browser");
}
document.addEventListener("DOMContentLoaded", init, false);
注意上面代码中使用的document
对象。document
是一个本地 JavaScript 对象。浏览器中加载的每个 HTML 网页都有自己的document
对象。您可以使用document
对象来检索和操作加载页面中的 HTML 内容。
前面的代码指定在调度事件DOMContentLoaded
时自动调用函数init
。当浏览器完成加载 DOM(文档对象模型)时,将调度DOMContentLoaded
事件。DOM 是页面上 HTML 的表示。只有新浏览器会调度DOMContentLoaded
事件。因此,您从函数init()
内部编写或调用的任何 JavaScript 代码只有在用户拥有相对较新的浏览器时才会运行。
使用事件侦听器
事件监听器是 JavaScript 自带的。这是 JavaScript 和 PHP 真正不同的一点,因为 PHP 中没有事件监听器。事件侦听器用于将事件与函数相关联。这个想法是,每次某个事件发生时,应该运行一个特定的函数。添加事件侦听器的一般语法如下:
object.addEventListener(event, event handler, useCapture);
如您所见,addEventListener
有三个参数:一个事件、一个事件处理程序和 useCapture。
事件
第一个参数指定监听哪个事件。不同的对象可以响应不同的事件。在前面的例子中,您正在监听文档对象的DOMContentLoaded
事件。浏览器将调度该事件,document
对象可以响应该事件,但前提是您明确告诉它监听该事件。
事件处理程序
第二个参数指定在听到事件时运行哪个函数。addEventListener
为特定事件注册一个事件处理函数。在您的示例中,您已经注册了函数init
作为文档对象的DOMContentLoaded
事件的事件处理程序。
可选的使用捕获
addEventListener 的第三个参数表示一个称为 useCapture 的高级主题。对于大多数现代浏览器,这是一个可选参数,意味着您不必指定它。如果没有明确设置为true
,大多数浏览器会简单地假设它为false
。但是有些浏览器和浏览器版本需要设置这个参数,所以你不妨养成设置的习惯。
根据经验,您可以声明第三个参数并将其设置为false
。你可能会遇到一种特殊的情况,需要你把它设置为true
,但在本书的代码示例中不会出现。这是一个可以通过参考其他资源自己探索的 JavaScript 主题。
为覆盖图和大图像创建标记
您已经为渐进式增强建立了一个非常基本的框架:init
函数只有在浏览器支持的情况下才会运行。
是时候开始使用 lightbox 画廊了。首先添加一些 JavaScript 来动态创建一个小 HTML,为在透明覆盖层上显示大图像提供标记结构。您必须更新js/lightbox.js
中的init()
功能,如下所示:
//edit existing function
function init() {
var lightboxElements = "<div id='lightbox'>";
lightboxElements += "<div id='overlay' class='hidden'></div>";
lightboxElements += "<img class='hidden' id='big-image' />";
lightboxElements += "</div>";
document.querySelector("body").innerHTML += lightboxElements;
}
这段代码将创建一串 HTML 元素,并将它们添加到已经在<body>
中找到的 HTML 元素之后。请特别注意<img>
元素。它缺少一个src
属性,所以此时它不显示图片。如果保存您的工作并在浏览器中加载 http://localhost/ch4,您可能会惊讶地发现,尽管您尽了所有的 JavaScript 努力,似乎什么都没有改变。如果您的 JavaScript 工作正常,您应该有一个页面,并在末尾添加一些 HTML 元素。但是它们不包含任何内容,所以您什么也看不到——到目前为止!
在前面的代码示例中,您可以看到 JavaScript 可以像 PHP 一样连接字符串。请注意,JavaScript 的增量连接运算符不同于 PHP。这是功能相同,语法不同的另一种情况。
//Javascript's incremental concatenation operator
+=
//PHP's incremental concatenation operator
.=
document.querySelector()
函数中的最后一行看起来好像没有你在书中用过的东西。querySelector()
是document
对象的一个方法。如果你熟悉 CSS 选择器,这绝对是一个非常好的方法。通过querySelector()
,您可以使用 CSS 选择器语法从加载的页面中选择 HTML 元素。
document.querySelector("body").innerHTML += lightboxElements;
前面一行使用querySelector
来获取<body>
元素及其所有内容。JavaScript 将存储在变量lightboxElements
中的 HTML 字符串添加到<body>
中的现有内容之后。要访问<body>
中的 HTML 内容,可以使用innerHTML
属性。
显示覆盖图
我认为用 JavaScript 创建一个<div>
元素是非常值得的。但是如果你能看到它作为一个覆盖层工作,那就更有价值了。您可以通过在现有的样式表中添加一点 CSS 来实现这一点。我把我的放在css/layout.css
里。
/*declare a new style rule in css/layout.css */
div#overlay{
position: absolute;
width: 100%;
height:100%;
top:0px;
left:0px;
background:black;
opacity: 0.85;
}
如果保存该文件并在浏览器中重新加载 http://localhost/ch4,应该会看到一个半透明的覆盖图,覆盖了浏览器视窗中所有可见的内容。如果向下滚动,可以看到覆盖图只覆盖了视窗,而不是整个页面内容。这是应该的。看到这个覆盖图是 JavaScript 代码正在做一些事情的视觉确认。但是您只希望当用户单击特定图像时显示覆盖图。此外,您可能希望被单击的图像显示在覆盖图的顶部。你还有一些工作要做。
隐藏覆盖图并调整缩略图大小
默认情况下,您会希望图像显示为小缩略图。当一个缩略图被点击时,覆盖图应该出现以隐藏其他缩略图,并且被点击的图像应该几乎全屏显示。要实现这一点,你需要 CSS 和 JavaScript。您可以从准备一些 CSS 规则开始,稍后您可以通过 JavaScript 使用这些规则。在css/layout.css
中,你的样式表还有一些规则:
/*hide overlay and big-image*/
div#overlay.hidden, img#big-image.hidden{ opacity: 0; left:-200%; }
/*resize images and display them as a horisontal list*/
li.lightbox img{ height: 100px; }
li.lightbox{ display: inline-block; margin: 10px; }
如果刷新浏览器,可以看到覆盖图被隐藏了。您还可以看到,尽管使用了 CSS,缩略图仍未调整大小。不要太惊讶。原因很简单,因为<li>
元素还没有设置为lightbox
的class
属性。您将使用 JavaScript 动态设置class
。但是在编写更多的 JavaScript 之前,我想让你看看隐藏图像和覆盖的 CSS。您可以看到,这两个元素都被设计为完全透明,并且位于左侧很远的位置,即使它们不是完全透明的,它们也是不可见的。请记住,重要的是覆盖图和大图都有一个设置为hidden
的class
属性。如果类未设置为hidden
,两个元素都将显示。
默认情况下,没有属性设置为lightbox
的<li>
元素。所以,上面写的 CSS 规则目前不适用于任何东西。您可以通过编写一些 JavaScript 来声明一个class
属性,并在用于图库图像的所有<li>
元素上将该属性的值设置为lightbox
,但只针对有能力的浏览器。更新js/lightbox.js
如下:
//complete code listing for js/lightbox.js
//edit existing function
function init(){
var lightboxElements = "<div id='lightbox'>";
lightboxElements += "<div id='overlay' class='hidden'></div>";
lightboxElements += "<img class='hidden' id='big-image' />";
lightboxElements += "</div>";
document.querySelector("body").innerHTML += lightboxElements;
//add a new function call here
prepareThumbs();
}
//declare a new function
function toggle(){
window.console.log("show or hide a big image");
}
//declare new function
function prepareThumbs() {
var liElements = document.querySelectorAll("ul#images li");
var i = 0;
var image, li;
//loop through all <li> elements
while ( i < liElements.length ) {
li = liElements[i];
//set class='lightbox'
li.setAttribute("class", "lightbox");
image = li.querySelector("img");
//register a click event handler for the <img> elements
image.addEventListener("click", toggle, false);
i += 1;
}
}
document.addEventListener("DOMContentLoaded", init, false);
保存此代码并刷新浏览器。您应该会看到小缩略图的水平列表。当您单击一个图像时,您应该会在控制台窗口中看到一条消息,提示“显示或隐藏大图像”
这是一个步骤中的一大块代码。让我们调查一下,弄清楚到底发生了什么。
仅向支持的浏览器显示缩略图
JavaScript 使用渐进式增强,并有效地隐藏了浏览器可能无法理解的 JavaScript 特性。还记得函数init()
如何只在浏览器调度DOMContentLoaded
事件时运行吗?只有相对较新的浏览器才会调度该事件。如前所述,当给定页面的所有 HTML 元素都加载到浏览器的内存中时,它就会被调度。
函数prepareThumbs()
是从函数init()
内部调用的,所以prepareThumbs()
只能在新的浏览器中运行。您已经有效地隐藏了旧浏览器中的 JavaScript:您拥有渐进式改进!
用 querySelectorAll()获取 HTML 元素的数组
接下来,看看如何将class
属性添加到图库的所有<li>
元素上。第一个任务是选择所有的<li>
元素。您已经为该任务使用了方法querySelectorAll()
。querySelectorAll()
就像querySelector()
,除了它返回的不仅仅是一个 HTML 元素,而是匹配所用 CSS 选择器的所有 HTML 元素。
var liElements = document.querySelectorAll("ul#images li");
在前一行中,变量liElements
将保存在<ul>
中找到的所有<li>
元素的数组,并将id
属性设置为images
。
Note
你可以在 www.kirupa.com/html5/finding_elements_dom_using_querySelector.htm
了解更多关于使用querySelector()
和querySelectorAll()
的信息。
您已经看到了如何遍历一组宠物。遍历 HTML 元素的数组就是这样。在prepareThumbs()
内部,可以看到一个while
循环。只要变量i
保存的值小于找到的<li>
元素的数量,它就会一直循环下去。这实际上意味着您将使用图像的id
属性遍历<ul>
元素中的每个<li>
项目。
变量i
可以用作索引,从所有<li>
元素的数组中获取一个特定的<li>
元素。每一个<li>
都会被记忆在变量li
中,每一个<li>
都会得到一个lightbox
的class
属性。此类<li>
元素有一个 CSS 规则,这就是为什么缩略图在浏览器中显示为水平列表。还有另一个 CSS 规则在这样的<li>
元素中选择<img>
元素。该规则将缩略图的宽度调整为 100 像素。
仍然在while
循环中,使用querySelector()
选择<li>
元素中的<img>
元素。您为每个<img>
元素分配一个事件监听器。所以,每当用户点击一个<img>
,函数toggle()
就会被调用。换句话说,您为每个<img>
元素注册了一个名为toggle()
的事件处理函数。
显示大图像
每当用户点击一个<img>
元素,函数toggle()
就会运行。在这一点上,它没有多大作用。它只是在控制台窗口中输出一条消息。你在寻找一种不同的行为。如果单击了缩略图,您希望透明覆盖图隐藏所有缩略图,并且您希望显示所单击图像的大版本。如果单击一个大图像,您希望覆盖图和大图像消失,这样所有缩略图再次变得清晰可见。您将需要一个 CSS 规则来设计大图像的样式,还需要一些 JavaScript 来操作 HTML 类属性。你可以从 CSS 开始。给css/layout.css
再加一条规则:
/*partial code listing for css/layout.css*/
/*new CSS rule for showing the big-image*/
#big-image.showing{
max-width: 80%;
max-height:90%;
position:absolute;
background-color: white;
padding: 10px;
top:5%;
left: 10%;
}
要在浏览器中看到一些动作,您还必须向 js/lightbox.js 中声明的toggle()
函数添加一些代码,如下所示:
//edit existing function
function toggle( event ){
//which image was clicked
var clickedImage = event.target;
var bigImage = document.querySelector("#big-image");
var overlay = document.querySelector("#overlay");
bigImage.src = clickedImage.src;
//if overlay is hidden, we can assume the big image is hidden
if ( overlay.getAttribute("class") === "hidden" ) {
overlay.setAttribute("class", "showing");
bigImage.setAttribute("class", "showing");
} else {
overlay.setAttribute("class", "hidden");
bigImage.setAttribute("class", "hidden");
}
}
您可以看到 JavaScript 并没有真正显示或隐藏大图像或覆盖图。JavaScript 所做的只是操纵#overlay
和#big-image
的类属性。你可以在你的浏览器中检查覆盖和图像实际上是隐藏的。如果您单击缩略图,覆盖图将出现在缩略图的顶部,大图像将出现在覆盖图的顶部。
这种效果是通过联合 CSS 和 JavaScript 实现的。在 CSS 中,规则规定了如何呈现#overlay
和#big-image
。如果这些元素上的class
被设置为hidden
,它们将被隐藏。如果没有设置class hidden
,将显示元素。JavaScript 动态操纵class
属性。CSS 声明如何呈现#big-image
和#overlay
,这取决于class
属性的当前值。
JavaScript 有一个非常简单的工作:它只负责在覆盖图和大图像上设置class
属性。如果class
属性当前被设置为hidden
,它将被更改为showing
。否则,如果类别未设置为hidden
,则会设置为hidden
。
让我们更详细地检查一下toggle
中的代码,以便更好地理解它和 JavaScript。
使用 MouseEvent 对象
首先要注意的是添加到toggle()
函数中的event
参数。函数toggle()
被调用,因为它被注册为<img>
元素上点击事件的事件处理程序。它是从事件侦听器中调用的。当这种情况发生时,一个Event
对象在事件被触发时被传递。当用户点击鼠标按钮时,点击被触发,因此,被发送的Event
对象是一个MouseEvent
对象。
事件对象有很多非常有用的属性,可以在代码中使用。MouseEvent
对象有一个target
属性,它保存了对被点击的 HTML 元素的引用。
Note
如果在 toggle()函数中添加以下代码行,您可以在控制台窗口中看到其他可用的属性:window.console.log(event);
。
您使用MouseEvent.target
属性来获取被点击的<img>
元素。您可以用它来替换大图片的src
属性和被点击图片的src
属性。本质上,您使用它来显示单击缩略图的大版本,如以下代码所示:
bigImage.src = clickedImage.src;
棒形纽扣
toggle 的意思是在两种状态之间转换。当你开灯的时候,你拨动灯的开关,当你关掉灯的时候,你拨动同样的灯的开关。在这段 JavaScript 中,您想要切换覆盖图和大图像。
如果覆盖元素的class
属性被设置为hidden
,您想要隐藏覆盖和大图像。如果覆盖图的class
属性设置为showing
,您希望显示覆盖图和大图。如果你查看toggle
函数,你可以看到用代码表达的同样的想法。
if ( overlay.getAttribute("class") === "hidden" ) {
//code to show overlay and image
} else {
//code to hide overlay and image
}
操纵属性
要读取class
属性的值,可以使用getAttribute()
方法。这是所有 HTML 对象都有的标准 JavaScript 方法。getAttribute()
方法可以用来读取任何属性的值。一般语法如下:
element.getAttribute( whichAttribute );
getAttribute()
方法将返回在指定元素中找到的请求属性的值。有一个类似的方法,叫做setAttribute()
,用于改变属性值。一般语法如下:
element.setAttribute( whichAttribute, newValue );
方法可以为特定 HTML 元素的指定属性设置一个新值。在前面的toggle()
函数中,您使用它来更改覆盖图和大图像的属性值。
隐藏大图像
此时,您可以单击缩略图来显示覆盖图和大图。太棒了。但是你不能再隐藏覆盖图或大图了,这不是很好。要启用隐藏,只需将toggle()
函数注册为单击大图像时触发的事件处理程序。这可以通过init()
函数中的以下(粗体)两行额外代码来完成:
//edit existing function
function init(){
var lightboxElements = "<div id='lightbox'>";
lightboxElements += "<div id='overlay' class='hidden'></div>";
lightboxElements += "<img class='hidden' id='big-image' />";
lightboxElements += "</div>";
document.querySelector("body").innerHTML += lightboxElements;
//new code: register toggle as event handler
var bigImage = document.querySelector("#big-image")
bigImage.addEventListener("click",toggle, false);
//end of changes
prepareThumbs();
}
自己测试一下。在这一点上,你应该能够点击一个缩略图来显示覆盖图顶部的大图像。如果你点击大图,大图和覆盖图将再次隐藏,从而显示下面的缩略图。
使用 CSS 动画
如果半透明覆盖可以淡入隐藏缩略图,那不是很好吗?这可能是使灯箱画廊更加漂亮的最后一笔。您可以通过在css/layout.css
中添加以下(粗体)单行 CSS 来创建 CSS 动画:
#overlay{
position: absolute;
width: 100%;
height:100%;
top:0px;
background:black;
opacity: 0.85;
left:0px;
/*this is the animation to fade the overlay in gradually over 1 second*/
transition: opacity 1s ease-in;
}
编码挑战
灯箱画廊现在完成了。如果您浏览互联网,可以很容易地找到更多 JavaScript 驱动的图片库的例子。也许你会遇到一个你想在画廊里实施的行为。
一个非常常见的功能是在显示大图时单击覆盖图。大多数图库会在点击覆盖图时切换。这并不难实现,所以也许这是一个你可以自己解决的任务。您只需将toggle
注册为覆盖图中检测到的点击事件的事件处理程序。这种方法是可行的,但会触发一个 JavaScript 错误。您可以在控制台中看到错误消息。对您来说,一个额外的编码挑战可能是找出错误消息的含义,以及如何更改代码以避免错误。
你可以在 www.webmonkey.com/2010/02/make_a_javascript_slideshow/
找到另一个 JavaScript 库的教程,有下一个和上一个按钮。也许你可以弄清楚如何在你的 lightbox gallery 中实现这样的按钮。这可能是一个有趣的学习经历,也是你的灯箱画廊的一个很好的补充。
摘要
你已经在相对较少的几页中涵盖了很多内容。主要目标是为您提供一种在 PHP 项目中集成 JavaScript 解决方案的方法。在这个过程中,您使用了一个相对简单的 lightbox image gallery。
您已经看到 PHP 和 JavaScript 语言在许多方面都很相似。通常,只是语法不同,有时甚至不是这样。这对你来说是个好消息。一旦你学会了 PHP,你就可以相对容易地学习 JavaScript。
另一方面,您也看到了 JavaScript 和 PHP 之间的显著差异。要真正精通这两种语言,你最终会想要密切关注这些差异。
也许最重要的区别是 JavaScript 是一种客户端脚本语言,而 PHP 是一种服务器端脚本语言。您的 JavaScript 代码在用户的浏览器中运行。PHP 只在你的服务器上运行,所以浏览器永远看不到你的 PHP。浏览器只会得到 PHP 创建的结果。
Note
实际上,JavaScript 可以在服务器上运行。在互联网上搜索node.js
以了解更多信息。此外,PHP 可以在没有 web 服务器的情况下运行。但是 JavaScript 多用于客户端,PHP 多用于服务器端。
到目前为止,您已经看到当 PHP 完成它的任务时,它将创建一个输出。输出通常是发送到浏览器的 HTML 文件。JavaScript 可以在浏览器中操作 HTML,而不需要联系服务器。您最终可能会想要学习更多的 JavaScript,但是这超出了本书的范围。
六、使用数据库
现代网站非常强大,这种强大的力量很大程度上来源于它们存储信息的能力。存储信息允许开发者在他们的软件和用户之间创建高度可定制的交互,从基于条目的博客和评论系统到安全处理敏感交易的高性能银行应用。
本章涵盖了 MySQL 的基础知识,这是一个强大的开源数据库。我还演示了在 PHP 项目中使用 MySQL 的面向对象方法。涵盖的主题包括以下内容:
- MySQL 数据存储的基础
- 操作 MySQL 表中的数据
- 数据库表结构
- 使用 PHP 与 MySQL 数据库交互
- 用模型-视图-控制器方法组织 PHP 脚本
- 为什么编码就像演奏布鲁斯
这一章有很多东西要学,其中一些可能会让你头大。但是请放心,所有涉及的主题将在后续章节中重复和详细阐述。你将会有大量的学习机会。
MySQL 数据存储的基础
MySQL 是一个关系数据库管理系统,允许您在多个表中存储数据。每个表包含一组命名列,每一行由表中的一个数据条目组成。表格通常包含关于其他表格条目的信息。这样,一个事实可以存储在一个表中,但可以在其他表中使用。例如,看看如何存储音乐艺术家的信息(见表 6-1 和 6-2 )。
表 6-2。
The Album Table
相册 id | 艺术的 | 相册名称 |
---|---|---|
one | one | 对艾玛来说,永远以前 |
Two | one | EP3 血库 |
three | Two | 让它死去 |
four | Two | 提醒 |
表 6-1。
The Artist Table
艺术的 | 艺术家 _ 姓名 |
---|---|
one | 好的 Iver |
Two | Feist |
第一个表 artist 包含两列。第一列artist_id
,存储每个艺术家的唯一数字标识符。第二列artist_name
,存储艺术家的名字。
第二个表 album 在album_id
列中存储每个专辑的唯一标识符,在album_name
列中存储专辑名称。专辑表包括第三列artist_id
,它与艺术家和专辑表相关联。该列存储与录制专辑的艺术家相对应的唯一艺术家标识符。
乍一看,这似乎是一种愚蠢的数据存储方式。为什么要保留一个抽象的,无法理解的数字,而不是简单的写下每张专辑的艺人名字?表 6-3 想象你做了那件事。
表 6-3。
The Badly Designed Album Table
相册 id | 艺术家 | 相册名称 |
---|---|---|
one | 好的 Iver | 对艾玛来说,永远以前 |
Two | 好的 Iver | EP3 血库 |
three | Feist | 让它死去 |
four | 第一 | 提醒 |
请注意album_id 4
的拼写错误。因为每张专辑的艺术家姓名都是分开拼写的,所以可以为同一位艺术家存储不同的姓名。在一个只有四个条目的小表中,就像前面的表一样,很容易发现和纠正错误。但是在现实世界中,表很少这么小。假设您正在为一家音乐商店构建数据库。你必须记录数以千计的专辑。
如果你幸运的话,你会发现这个错误——然后你必须检查 Feist 的每张专辑,检查艺术家的名字是否拼写正确。这是可能的,但这将是对时间的疯狂浪费。
对另一个表结构(列于表 6-2 中)进行同样的思考实验。如果你把 Feist 拼错成 fiest,你就更有可能发现这个错误,因为 Feist 的每张专辑都会列在 fiest 下面。此外,纠正错误不会让你跋涉数以千计的条目。你只需简单地去一个声明艺术家名字的地方,写 Feist 而不是 fiest,每张专辑就会正确地列出来。
通过设计只存储一段数据一次的表,可以设计一个具有数据完整性的健壮数据库。SQL 社区的杰出人物 Joe Celko 恰当地创造了一个口号“一个简单的事实,在一个地方,一个时间。”记住这句口号,让你的数据库表遵循这条规则。
使用 SQL 操作数据
您可以通过结构化查询语言(SQL)操作 MySQL 表中的数据。SQL 是一种小型语言,大部分都非常容易阅读和理解。在本节中,您将学习执行以下操作的 SQL 语句:
- 创建数据库
- 在数据库中创建一个表
- 将数据插入表格
- 从表中检索数据
- 更新表中的数据
您将使用 XAMPP 提供的 phpMyAdmin 控制面板来测试这些命令。要使用 XAMPP,您必须先启动它。打开 XAMPP 控制面板(见图 6-1 )并启动 MySQL 数据库和 Apache Web 服务器。
图 6-1。
The XAMPP control panel
在 MySQL 和 Apache 运行的情况下,您可以打开浏览器并导航到 http://localhost/phpMyAdmin,以访问 phpMyAdmin 控制面板(图 6-2 )。
图 6-2。
The phpMyAdmin control panel
为民意调查开发数据库
感受数据库驱动的 web 页面的最好方法是创建一个用于测试的页面。在接下来的页面中,您将创建一个数据库驱动的网站投票。这是数据库驱动开发的一个简单例子,但它足以展示基本原理。最简单的站点投票将提出一个问题,站点访问者可以回答“是”或“否”。用户的所有答复都将显示出来,因此每个站点用户都可以看到其他站点访问者是如何回答的。
虽然这个例子很简单,但是它将总结到目前为止您在书中看到的所有内容,并且它将要求您学习如何在您的 PHP 项目中集成数据库驱动的数据。
这是一个完美的学习项目,因为它非常简单。它将需要相对较少的代码行,这意味着您可以专注于相关的原则,而不是淹没在冗长的语法中。这将是你在下一章开始的个人博客系统的完美准备。
网站投票依靠数据库表来存储投票问题和投票回复。PHP 将不得不连接到 MySQL 并检索相关数据,因此它可以在浏览器中显示为 HTML。从 PHP 中,您还将输出一个 HTML 表单,允许站点访问者与站点投票进行交互。每当访问者提交表单时,PHP 应该获得提交的答案,并相应地更新 MySQL 数据库表。首先创建一个包含一个表和一些投票数据的数据库。
使用 CREATE 创建数据库
SQL 使用 CREATE 一词来表示正在创建一个表或数据库。启动 CREATE 子句后,必须指明是创建数据库还是表。在您的例子中,您使用关键字 DATABASE 来表明您实际上正在创建一个数据库。最后,您必须指明新数据库的名称。
MySQL 最初是在瑞典开发的。所以,MySQL 中使用的默认字符集是瑞典语。也许你不想在你的解决方案中使用瑞典语。我喜欢在我的解决方案中使用 utf-8。创建使用 utf-8 的数据库很容易;您只需指定 utf-8 作为要使用的字符集。完整的命令应该如下所示:
CREATE DATABASE playground CHARSET utf8
要执行 SQL 语句,必须在 phpMyAdmin 控制面板中选择 SQL 选项卡(图 6-3 )。这将弹出一个文本字段,您可以使用它来输入 SQL 语句。要实际执行 SQL,您必须单击文本字段下面的 Go 按钮。
图 6-3。
The SQL tab in phpMyAdmin
CREATE TABLE 语句
MySQL 将数据存储在表中。自然,开始使用 MySQL 的第一件事就是创建第一个表。要做到这一点,您必须了解更多的 SQL。幸运的是,SQL 语法非常容易阅读和理解。创建表的一般语法如下:
CREATE TABLE table_name (
column_name datatype [any constraints or default values],
column_name datatype [any constraints or default values]
)
正如您所看到的,一个SQL CREATE
语句必须声明一个表名。它还应该声明每个表列或属性的名称和数据类型。SQL 语句可以为创建的属性声明约束或默认值。
通过在 phpMyAdmin 控制面板的左栏中单击数据库名称来访问数据库playground
。单击屏幕顶部的 SQL 选项卡,就可以创建第一个表了。下面是您需要的 SQL:
CREATE TABLE poll (
poll_id INT NOT NULL AUTO_INCREMENT,
poll_question TEXT,
yes INT DEFAULT 0,
no INT DEFAULT 0,
PRIMARY KEY (poll_id)
)
在 phpMyAdmin 的 SQL 选项卡中输入 SQL 后,可以单击 Go 来执行 SQL(图 6-4 )。这将创建新表。
图 6-4。
Create a new table in the playground database
您可以通过从 phpMyAdmin 左侧的面板中选择 poll 表来研究刚刚创建的新表。接下来,选择“结构”选项卡,该选项卡位于“SQL”选项卡的旁边(图 6-5 )。
图 6-5。
Poll table structure
这里有很多信息供你思考。您可以看到投票表有四个属性或列:poll_id
、poll_question
、yes
和no
。
您可以看到每个属性都有一个类型。表的字段只能保存正确类型的数据。比如,你只能在poll_id
、yes
和no
中存储整数。你可以在上图中看到,因为类型是int(11)
。你可以无视 11 这个数字。它在那里是因为 11 恰好是 MySQL 整数的默认显示宽度。
Note
整数是一个非十进制数,也就是你所说的整数。
你也可以看到你只能在poll_question
中存储文本。再往poll_question
里看,还可以看到归类是 utf8。
Note
归类是一组规则,用于指定字符集中哪些字符先出现。很明显 a 在 b 之前,但是字符 7 呢?应该放在字母字符之前还是之后?像#"#€%&这样的特殊字符呢?归类明确规定了字符应该如何排序。
最后,您可以看到yes
和no
属性是用默认值 0 创建的。其他属性都没有默认值。
了解主键
您可以看到,poll_id
属性带有下划线。这是一个直观的指示,表明poll_id
被设置为投票实体的主键。当属性被声明为主键时,它必须具有唯一的值。因此,无论投票表最终包含多少行数据,都不可能有两个相同的poll_id
值。
假设您有一行poll_id
为 1 的数据。如果您试图插入另一行数据,并且poll_id
也是 1,MySQL 将拒绝新行并给出一个错误消息。主键用于明确地标识一行数据。您实际上可以在 MySQL 中创建没有主键的表,但是这种表是特例。大多数情况下,您会希望创建带有主键的表,因为如果您不能唯一地标识条目,数据就没有什么用处。
您可以看到,poll 表是以这样一种方式创建的,即主键poll_id
必须有一个值。属性poll_id
被声明为 NOT NULL,这意味着空值对于poll_id
来说是不可接受的。poll_id
属性必须始终保持一个INTeger
值。poll_id
属性不能为空或未声明。
了解自动增量
轮询表有一个自动递增的主键。这是一个简单但强大的想法:投票表中的第一行数据将得到 1 的poll_id
。下一行将自动获得 2 的poll_id
。下一行将得到 3 的poll
_ id
,以此类推。poll_id
的值会自动增加。
MySQL 将跟踪已经被用作poll_id
的值。这样,poll
中的每一行新数据都将获得一个唯一的poll_id
。从某种意义上说,自动递增的主键非常类似于世界上许多国家用来唯一标识一个公民的社会安全号:它是一个用来唯一标识一个事物的任意数字。
INSERT 语句
创建好表后,就可以开始存储数据了。轮询表中的每个新条目都将存储为单独的一行。为了简单起见,您可以从插入单行数据开始。下面是一个 SQL 语句:
INSERT INTO poll (
poll_question
) VALUES (
"Is it hard fun to learn PHP?"
)
这个 SQL 语句将在表中插入一个新的数据行,称为poll
。它将为poll_question
列或属性声明一个值。更具体地说,poll_question
列将得到一个值 PHP 很有趣吗?还记得投票表总共有四个属性或列吗?其余的列poll_id
、yes
和no
将简单地用默认值创建。所以poll_id
会得到值 1,而yes
和no
都会得到值 0。
要让 MySQL 程序执行 SQL 语句,必须首先在 phpMyAdmin 控制面板中选择 playground 数据库。接下来,单击 SQL 选项卡并输入前面的 SQL 语句。最后,单击 Go,实际执行输入的 SQL 语句。
我假设您可以推导出一些INSERT
语句的通用语法。我希望您学习以下通用语法,这样您就可以很快开始制定自己的INSERT
语句:
INSERT INTO table_name (
column_name, other_column_name
) VALUES (
[data for column], [data for other column]
)
当您编写 I NSERT
语句时,您必须首先指出您想要将数据插入哪个表。然后,指定要将数据插入该表的哪些列。如果表中有更多的列,它们将获得默认值。
一旦指定了要添加插入内容的表和列,就必须列出要插入的实际数据。如果在INSERT
语句中指定一列,则必须列出一个值。如果指定两列,则必须列出两个值。换句话说,列的数量必须与 I NSERT
语句中指示的值的数量相匹配。
SELECT 语句
将一行数据插入投票表后,您可能希望看到新的一行。您可能需要一些视觉上的确认,以确认该行确实被插入了,所以您知道您有一个数据库表,其中存储了一些数据。要从数据库表中检索数据,必须使用 SQL SELECT
语句。SELECT
语句的一般语法非常简单。
SELECT column_name, column_name FROM table_name
需要注意的主要关键词是SELECT
。它用于检索数据库中指定表的数据指定属性FROM
。一个SELECT
语句总是返回一个填充了任何检索到的数据的临时表。临时表将具有紧接在关键字SELECT
之后的属性。您可以使用以下 SQL 语句从轮询表中检索数据:
SELECT poll_id, poll_question, yes, no FROM poll
请转到 phpMyAdmin 控制面板中的 SQL 选项卡,输入上面的SELECT
语句,并查看返回的表(参见图 6-6 )。
图 6-6。
Poll table with one row inserted
在图 6-6 中可以看到,SELECT
语句返回一个临时的、未命名的表,该表有四列,每一列对应于SELECT
语句中指示的一列。您可以看到表中有一行数据。它有一个为 1 的poll_id
和一个poll_question
。yes
和no
列分别为 1 和 0。
就其目前的状态来看,这并不算什么,但也许您会意识到,这就是在您的网站上显示站点投票所需的全部数据。您的网站将显示投票问题。网站访问者可以通过 HTML 表单发布他们的回答。可能的选项是“是”或“否”。来自站点访问者的所有响应都将存储在“是”或“否”字段中。所以,用一点数学知识,你可以计算出相对的反应,并显示如下信息:79%的网站访问者认为 PHP 学习起来很难也很有趣。
更新语句
您可能会发现,每次站点访问者提交响应时,您都必须更改投票表中的yes
或no
值。要做到这一点,您必须知道更多的 SQL 语句。你可以假装一个网站用户同意 PHP 很难学。您需要一条 SQL 语句将yes
属性的存储值增加 1,如下所示:
UPDATE poll SET yes = yes + 1
WHERE poll_id = 1
如果您愿意,您可以通过在 phpMyAdmin 的 SQL 选项卡中输入并单击 Go 来运行UPDATE
语句。如果这样做,可以看到poll
中第一行数据的 yes 属性的值为 1。如果再次运行相同的 SQL 语句,yes
将得到值 2。
请注意WHERE
子句如何限制哪些行将受到更新的影响。只有poll_id
为 1 的行将受到影响。由于WHERE
子句,表中的任何其他行都不会被更新。
没有WHERE
子句的UPDATE
语句将更新轮询表中所有行的yes
属性。在您的例子中,只有一行,所以WHERE
子句不是绝对必要的。但是您将使用的大多数表都不止一行,所以明确指出要更新哪一行是一个好习惯。
在前面的WHERE
子句中,您可以确定只有一行将被更新,因为WHERE
子句通过主键标识一行数据。您总是可以相信主键可以唯一地标识一行(除非您的表设计得非常糟糕)。
编写数据库驱动的站点投票
为了学习如何从 PHP 使用 MySQL 数据库,让我们编写一个数据库驱动的站点投票。让我们使用操场数据库和投票表来存储数据。您将学习使用所谓的 PDO 对象将您的 PHP 应用连接到 MySQL 数据库。从基本的动态 PHP 站点升级到数据库驱动的动态 PHP 站点会带来一些后果。
显然,您必须从 PHP 连接到数据库,并且您的 PHP 脚本必须与数据库表通信,以获取您的站点所需的内容。PHP 是一种非常宽容的语言,您可以用许多方式来完成这项任务。但是其中一些方法比其他方法更具可扩展性。一旦你开始处理更大的项目,比如一个博客系统,一些起初看起来很容易的方法可以把你的代码变成一个完全没有组织的,混乱的,意大利面条一样的混乱。让我们采用一种经过反复试验的方法来编写可以扩展以适应复杂项目的代码架构,即使这个站点投票是一个简单的项目。
用 MVC 分离关注点
模型-视图-控制器(MVC)设计模式是一致地组织脚本的常用方法。使用一致的方法来组织脚本可以帮助您更快、更有效地开发和调试。
学习理解 MVC 背后的基本原理也可以为学习 MVC 框架做好准备。最终,您可能会遇到 CodeIgnitor、cakePHP、yii 或其他 PHP MVC 框架。这样的框架将帮助您设计和开发更复杂的 web 应用。
最基本的,MVC 将编码问题分成三类:模型、视图和控制器。模型是代表数据的一段代码。您的模型还应该包含您正在构建的系统中涉及的大多数逻辑。视图是可视化显示信息的一段代码。视图要显示的信息是从模型接收的。控制器是一段代码,它从用户那里获取输入,并向相关模型发送命令。简而言之,MVC 将用户交互从系统逻辑和数据的可视化表示中分离出来。
Note
你可以在 http://en.wikipedia.org/wiki/Model-view-controller
阅读更多关于 MVC 的内容。
您已经看到了分离模型、视图和控制器的例子。还记得你是如何为 HTML 页面制作模板的吗?您已经使用了一个包含基本 HTML 页面框架的视图。你可以在第四章中开始建造的画廊中找到它。视图在ch4/templates/page.php
中。
在同一个项目中,您创建了一个与视图相关的模型:ch4/classes/Page_Data.class.php
,它声明了许多与 HTML 页面内容相关的方法和属性。
模型和视图通过控制器连接起来。在ch4/index.php
中,您为模型赋值,并使模型对视图可用,因此可以创建一个包含内容的格式良好的 HTML5 页面,并在浏览器中显示。所以,index.php
是你的控制器。
在本书中,我的目标是使用 MVC 的一个简单实现。您将遇到的大多数其他 MVC 实现可能要复杂得多。你可以很容易地找到许多不适合初学程序员的 MVC 例子。一旦您理解了基本的 MVC 原则,并且获得了一些在简单环境中使用这些原则的经验,您会发现理解更复杂的实现要容易得多。
规划 PHP 脚本
让我们保持简单的投票。创建一个index.php
来输出一个显示投票的有效 HTML5 页面。索引将是一个前端控制器。
前端控制器是 MVC web 应用中常见的一种设计模式。前端控制器是 web 应用的单一“入口”。到目前为止,您已经在项目中使用了前端控制器。还记得index.php
是唯一一个直接载入浏览器的脚本吗?这是给你的前端控制器的想法。
Note
前端控制器设计模式在网上有很好的文档。你可以在 http://en.wikipedia.org/wiki/Front_Controller_pattern
开始你自己的研究。
和前面的项目一样,index.php
将输出一个有效的 HTML5 页面,并加载投票控制器。投票控制器应该将poll
作为 HTML 返回,这样它就可以显示在index.php
上。注意每一个视图都有自己的模型和控制器(图 6-7 )。
图 6-7。
Distribution of responsibilities
查看如何有一个投票模型、投票控制器和投票视图。这三者应该协同工作来显示功能性投票。您还可以看到页面有自己的模型、视图和控制器。前端控制器是页面控制器。
创建投票项目
您可以创建一个站点结构来模拟代码职责。在XAMPP/htdocs
中新建一个文件夹。调用新文件夹poll
。在投票文件夹中,您可以创建另外三个文件夹:models
、views
和controllers
(图 6-8 )。
图 6-8。
Folder structure for the poll project
您可以从图库项目中复制ch4/templates/page.php
文件。将page.php
的副本保存为poll/views/page.php
。
同样,从图库项目中复制ch4/classes/Page_Data.class.php
,并在poll/models/Page_Data.class.php
中保存一份副本。
现在是时候创建poll/index.php
并编写一点代码来检查到目前为止一切都很好地一起工作了:
<?php
//complete code for htdocs/poll/index.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
//load model
include_once "models/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "PHP/MySQL site poll example";
$pageData->content .= "<h1>Everything works so far!</h1>";
//load view so model data will be merged with the page template
$page = include_once "views/page.php";
//output generated page
echo $page;
这段代码应该不会让你感到意外。它与您在本书的其他项目中使用的代码几乎相同。只有文件夹名称改变了!
我想你的观点也改变了。您现在正从模型-视图-控制器的角度查看这段代码。您可以通过将浏览器指向 http://localhost/poll/index . PHP 来测试代码。您可以看到前端控制器如何将模型和视图连接起来,并将一个格式良好的 HTML5 页面输出到浏览器供用户查看。
制作投票控制器
有了一个用自己的模型和视图创建的几乎空白的页面,并且设置了一个前端控制器,您就可以准备一个文件,用于在浏览器中显示您的投票示例。坚持 MVC 方法,您最终将需要一个投票模型、一个投票视图和一个投票控制器。最小的可能步骤是创建一个基本的轮询控制器,并从前端控制器index.php
加载它。在controllers
文件夹中创建一个新文件poll.php
:
<?php
//complete code listing for controllers/poll.php
return "Poll will show here soon";
接下来,您应该从index.php
加载轮询控制器。您应该在创建了$pageData
对象之后、包含页面模板之前加载控制器,如下所示:
//partial code listing for index.php
//comment out or delete this line
$pageData->content .= "<h1>Everything works so far!</h1>";
//new line of code to load poll controller
$pageData->content = include_once "controllers/poll.php";
//no changes below
$page = include_once "views/page.php";
echo $page;
如果您保存文件并在浏览器中加载 http://localhost/poll/index . PHP,您应该会看到 Poll 将显示在这里。如果你不这样做,当你输入代码的时候就会出错。
制作民意测验模型
有了初步的投票控制器,您就可以继续开发初步的投票模型了。仅用一种方法定义一个投票类。在models/Poll.class.php
中创建一个新文件,如下:
<?php
//complete code for models/Poll.class.php
//beginning of class definition
class Poll {
public function getPollData() {
$pollData = new stdClass();
$pollData->poll_question = "just testing...";
$pollData->yes = 0;
$pollData->no = 0;
return $pollData;
}
}
//end of class definition
注意关键字class
是如何用来声明一个类名的。这个类叫做Poll
,类定义中的代码定义了所有Poll
对象的蓝图。Poll
类只有一个方法。它将创建一个名为$pollData
的硬编码的StdClass
对象,并将其返回给调用者。
看看$pollData
对象如何拥有poll_question
、yes
和no
的属性。$pollData
对象表示显示投票所需的所有内容。换句话说,$pollData
模型会对数据进行投票。
制作投票视图
一个数据对象没什么好看的。您可以创建一个简单的投票视图,以便查看投票。在views/poll-html.php
中创建一个新文件,如下:
<?php
//complete code for views/poll-html.php
return "
<aside id='poll'>
<h1>Poll results</h1>
<ul>
<li>$pollData->yes said yes</li>
<li>$pollData->no said no</li>
</ul>
</aside>
";
使用投票模型查找投票视图
创建了初步的投票模型和投票视图后,您可以打开投票控制器来连接模型和视图,最后在浏览器中显示一些内容。在编辑器中打开controllers/poll.php
,进行以下必要的更改:
<?php
//complete code listing for controllers/poll.php
include_once "models/Poll.class.php";
$poll = new Poll();
$pollData = $poll->getPollData();
$pollView = include_once "views/poll-html.php";
return $pollView;
就这样!你有一个 MVC 投票。如果保存文件并在浏览器中加载 http://localhost/poll/index . PHP,您应该会看到一个格式良好的 HTML5 页面,其中有一个简单的<ul>
元素,显示一些初步的、硬编码的投票数据。你可以在图 6-9 中看到它应该是什么样子。
图 6-9。
The initial poll, seen in Chrome
也许你很想问一个问题,比如为什么我要创建三个不同的文件来显示一个简单的<ul>
元素?这是一个完全合理的问题。如果您想要的只是显示一个带有一些硬编码值的<ul>
元素,那么 MVC 方法完全是多余的。最好的方法可能是手工编写一个简短的 HTML 文件。
这里的重点是用一个非常简单的例子来介绍 MVC 设计模式,这样就不会有过于复杂的代码隐藏 MVC 的基本原理。使用 MVC 方法,您可以很好地创建数据库驱动的 web 应用。MVC 架构对于这么简单的东西来说几乎是不必要的,但是它确实可以解决你在更复杂的项目中遇到的一些挑战,比如你将在下一章开始制作的博客系统。
MVC 封装了来自控制器的模型视图。这意味着您可以在不更改任何其他内容的情况下更改视图。假设您不想在投票中加入<ul>
元素。您可以简单地更改在views/poll-html.php
中使用的 HTML 标签,并相信您的代码的其余部分能够正确运行。您可以轻松地更改视图,而无需更改任何其他内容。
类似地,您可以更改内容,并且仍然相信您的代码会按预期运行。将no
属性设置为 9 是一个简单的任务。你只需要修改models/Poll.class.php
中的一点点代码。您的投票应用是用独立的、松散耦合的元素构建的。
编码就像演奏布鲁斯
一些有创造性倾向的读者可能会反对严格的代码组织。你可能会觉得编码,尤其是当你试图实现一个标准化的方法,比如 MVC,是你的创造力的监狱。你可能会得出这样的结论:它没有给创造力留下空间。
我明白这种反对的理由,但我强烈反对。编码真的很像演奏蓝调音乐,它同样富有创造性,同样需要创造性的个人表达。
是的,实现 MVC 要求你把你的代码分成三类。是的,MVC 会强迫你在特定的、定义明确的地方写代码。当你在学习的时候,这样的限制就像监狱一样。但这就像学习演奏布鲁斯一样。
布鲁斯不是任何一种音乐:布鲁斯就是布鲁斯!要获得蓝调音乐,你不能只弹奏乐器上任何一个音符。在许多蓝调音乐中,你有三个和弦,所有的蓝调即兴创作都源于五声音阶——音乐家只有五个音符可供选择。也许严格的限制对创造性表达没有反作用。也许布鲁斯音乐家如此擅长创造性地表达自己,是因为布鲁斯仅限于三个和弦和五个音符。
要成为一名伟大的蓝调音乐家,你必须熟知蓝调音乐的规则。然后你开始弯曲它们。你开始添加过渡和弦来引导这三个基本蓝调和弦之间的音乐。你开始压五声音阶的五个音符。你开始在布鲁斯的限制下创造性地表达自己。
编码就是这样。这里有严格的、限制性的规则,个人创造性表达的空间很大。在学习的过程中,你会逐渐找到自己的路,但成为一个伟大的程序员和成为一个伟大的音乐家需要付出同样多的努力。所以开始练习吧!
从 PHP 连接到 MySQL
您的 MVC 架构将使建立数据库连接和使用数据库驱动的数据进行投票成为一项相当简单的任务。一旦建立了这样的连接,就可以从数据库中检索数据,并使用 PHP 将其发布为 HTML。这就是数据库驱动网站的本质。
有几种方法可以将 PHP 连接到 MySQL。如果您在互联网或其他书籍上寻找 PHP 代码示例,您可能会遇到一些不同的方法。你很有可能遇到过时的MYSQL()
和更新的MYSQLI()
。
PHP 数据对象(PDO)
在本书中,你将专门使用 PHP 数据对象(PDO)。这是一种从 PHP 连接到数据库的非常安全和有效的方式。PDO 支持多个数据库,并为处理大多数数据库交互提供了一套统一的方法。对于必须支持多种数据库类型的应用,如 PostgreSQL、Firebird 或 Oracle,这是一个很大的优势。
使用 PDO,从一种数据库类型转换到另一种数据库类型通常只需要重写非常少量的代码,然后继续照常工作。
使用 PDO 的一个潜在缺点是它依赖于 PHP5 的面向对象特性,这意味着运行 PHP4 的服务器不能运行使用 PDO 的脚本。这不再是一个大问题,因为很少有服务器不能访问 PHP5 然而,这仍然是你需要注意的事情。
打开连接
是时候连接到您的数据库了。为了简单起见,我建议您在index.php
中编写连接代码。当数据库连接在前端控制器中可用时,将它传递给任何其他需要它的代码将会非常容易。
默认 XAMPP 安装有默认用户名 root,没有密码。您为这个学习练习创建了一个名为 playground 的数据库。因此,您可以使用这些凭证连接到本地主机上运行的 MySQL 数据库。
您的 XAMPP 可能使用不同的凭据。您必须使用有效的凭证。您可以通过在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 site poll example";
//new code starts here
//database credentials
$dbInfo = "mysql:host=localhost;dbname=playground";
$dbUser = "root";
$dbPassword = "";
try {
//try to create a database connection with a PDO object
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$pageData->content = "<h1>We're connected</h1>";
}catch ( Exception $e ) {
$pageData->content = "<h1>Connection failed!</h1><p>$e</p>";
}
//comment out loading poll controller
//$pageData->content = include_once "controllers/poll.php";
//end of code changes
$page = include_once "views/page.php";
echo $page;
前面的代码创建了一个 PDO 对象,并将其存储在$db
变量中。默认情况下,PDO 将隐藏任何错误消息。您希望看到错误消息来学习。前面的代码会将任何与 PDO 相关的错误显示为所谓的异常。
Note
您还可以使用其他设置来创建到数据库的 PDO 连接。完整详细的报道可以咨询 www.php.net/manual/en/book.pdo.php
。
保存index.php
中的更改,并在浏览器中加载 http://localhost/poll/index . PHP。如果您完全正确地做了所有事情,您应该会在浏览器中看到一个输出,确认您连接成功。请注意,轮询控制器不再加载。我们很快会再次加载它。这段代码只测试数据库连接是否成功建立。
使用 try-catch 语句
当您尝试连接到数据库时,许多事情都可能出错。也许您的 XAMPP MySQL 服务器没有运行,或者您提供了无效的凭据,例如拼错的用户名或数据库名称。如果您的代码尝试连接到数据库并失败,则整个脚本都会失败。这是因为 PDO 会抛出一个所谓的异常。
异常很有趣,因为如果您处理异常,您的代码可以继续执行。这就是一条try-catch
语句所能做到的。它将尝试运行可能导致异常的代码块。如果抛出一个异常,它将被捕获并处理,因此剩下的脚本可以继续。一个优点是您可以为用户制定有意义的错误消息。try-catch
语句的一般语法如下:
try{
//try something that can fail
} catch ( Exception $e ) {
//whoops! It did fail
}
如果您更改了index.php
中的一行代码,您可以看到一条try-catch
语句正在运行。您可能会尝试使用错误的凭据连接到数据库,如下所示:
//partial code from index.php
//$dbUser = "root";
//use an invalid database user name for testing purposes
$dbUser = "bla bla";
保存并运行index.ph
p,您将看到 catch 块正在做它的事情。它将处理抛出的异常并输出一条错误消息。
请改回有效的数据库凭据。现在,您应该已经连接到数据库了。更改您的代码以再次加载轮询控制器:
//partial code listing for index.php
try {
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//delete or comment out the connection message
//$pageData->content = "<h1>We're connected</h1>";
}catch ( Exception $e ) {
$pageData->content = "<h1>Connection failed!</h1><p>$e</p>";
}
//loading poll controller again
$pageData->content = include_once "controllers/poll.php";
你的代码已经设置好了。您将很快使用数据库驱动的内容进行调查。但是首先,稍微绕一下面向对象编程中的一个重要话题:构造函数。
使用构造函数参数
当使用对象编程时,通常需要在新创建的对象中设置一些初始值。在 PHP 中,您可以通过使用构造函数方法来实现这一点。构造函数是一种方法,当对象第一次被创建时,它在对象的生存期内只运行一次。
这是一个需要了解的重要话题,也是面向对象工具箱的一个强大工具。您可以慢慢来,从一个简单的没有构造函数的类定义的例子开始。我在poll/test.php
中创建了一个 PHP 脚本。你不必这样做;您可以简单地阅读下面的代码示例:
<?php
class Greeter {
private $greeting = "Hello";
private $subject = "World";
public function greet(){
return "$this->greeting $this->subject";
}
}
$greeter = new Greeter();
echo $greeter->greet();
如果您要运行 http://localhost/poll/test.php,您将看到以下预期输出:
Hello World
您可以看到如何使用属性$greeting
和$subject
从方法greet()
中返回一个字符串。您还可以观察到如何使用$this
关键字从类定义内部访问对象属性。
想象一下,你想要一个除了“你好”之外还可以用来说话的Greeter
您必须能够在调用greet()
之前更改$greeting
属性。一种方法是在创建Greeter
对象时使用参数,如下所示:
<?php
class Greeter {
private $greeting = "Hello";
private $subject = "World";
//notice the two underscore characters in __construct()
//declare a constructor method with an argument
public function __construct( $g ) {
$this->greeting = $g;
}
public function greet(){
return "$this->greeting $this->subject";
}
}
//call constructor with an argument
$greeter = new Greeter( "Good Morning" );
echo $greeter->greet();
如果您要在浏览器中重新加载 http://localhost/poll/test.php,您会看到以下内容:
Good Morning World
构造函数方法是在创建新对象时运行的方法。在技术术语中,创建新对象的过程通常称为实例化。为了声明一个类的构造函数,你声明一个必须被调用的新方法__construct()
。注意,有两个下划线字符。
如果一个类有一个构造函数方法,这个构造函数方法将在一个新的对象被实例化时运行。所以,写new Greeter()
会调用Greeter
的构造函数。在前面的测试示例中,构造函数接受一个参数,因此在调用它时必须发送一个值。接收到的值存储在预定义的属性$greeting
中。这样,只要对象存在,接收到的值就会被记住。
Note
理解论点可能很棘手。你可以重读第三章中关于函数参数的解释,或者你可以在网上搜索“理解带参数的 PHP 函数”或者“学习 PHP 方法和参数”你一定会发现许多使用各种隐喻和代码示例的解释。我希望你能找到一个对你有用的解释。
与轮询模型共享数据库连接
您已经在index.php
中创建了一个数据库连接,一个 PDO 对象。您可以使用带参数的构造函数的思想来共享与轮询模型的数据库连接。在models/Poll.class.php
中更新您的代码,如下所示:
<?php
//complete code listing for models/Poll.class.php
class Poll {
//new code: declare a new property
private $db;
//new code: declare a constructor
//method requires a database connection as argument
public function __construct( $dbConnection ){
//store the received conection in the $this->db property
$this->db = $dbConnection;
}
//no code changes below – keep method getPollData() as it is
public function getPollData(){
$pollData = new stdClass();
$pollData->poll_question = "just testing...";
$pollData->yes = 0;
$pollData->no = 0;
return $pollData;
}
}
前面的变化已经准备好了Poll
类。它现在可以接收一个 PDO 对象。所以现在,你必须调用Poll
类的构造函数并传递一个 PDO 对象作为参数。停下来思考一下。您将在代码中的什么位置加载您的投票模型?
从您的投票控制器,从poll/controllers/poll.php
,如下所示:
<?php
//complete code listing for controllers/poll.php
include_once "models/Poll.class.php";
//Only change here: pass PDO object as argument
$poll = new Poll( $db );
$pollData = $poll->getPollData();
$pollView = include_once "views/poll-html.php";
return $pollView;
现在,您已经将数据库连接传递给了轮询模型,传递给了刚刚创建的新的Poll
对象。您现在可以保存并测试您的工作。您应该会看到和以前完全一样的投票。您的代码共享一个数据库连接,但是它还没有使用它做任何事情。
也许你想知道controllers/poll.php
中的 PHP 代码是如何知道变量$db
的。毕竟,$db
是在index.php
中声明的,那么怎么可能在另一个文件中使用呢?这是一个很好的问题,答案一点也不明显——除非你理解它。
变量$db
在index.php
中声明。在controllers/poll.php
有,因为controllers/poll.php
包含在index.php
里。包含一个文件很像从一个文件中复制所有代码并粘贴到另一个文件中。因此,包含文件中声明的所有变量都可以在包含文件中使用,反之亦然。
使用 PDOStatement 检索数据
您已经建立了从 PHP 到运行在本地 XAMPP 上的 MySQL 的连接。PDO 对象通过 phpMyAdmin 控制面板显式地连接到您已经用 SQL 创建的操场数据库。
在操场数据库中,有一个poll
表,其中有一个poll_question
以及它的yes
和no
属性值。您已经准备好从数据库中检索数据,因此可以在 PHP 中使用它。这需要 PHP 代码中的几个步骤。
处理投票数据的代码属于投票模型。所以,你要开models/Poll.class.php
。可以改变现有的方法getPollData
。此时,该方法返回硬编码的轮询数据。您希望它返回数据库驱动的轮询数据。
首先,您必须编写一条 SQL 语句,并使用 PDO 将它传递给 MySQL。PDO 可以告诉 MySQL 它应该执行 SQL 语句并返回一个结果:投票数据。下面是如何用 PHP 来表达:
//partial code listing for models/Poll.class.php
//udate existing method
public function getPollData () {
//the actual SQL statement
$sql = "SELECT poll_question, yes, no FROM poll WHERE poll_id = 1";
//Use the PDO connection to create a PDOStatement object
$statement = $this->db->prepare($sql);
//tell MySQL to execute the statement
$statement->execute();
//retrieve the first row of data from the table
$pollData = $statement->fetchObject();
//make poll data available to the caller
return $pollData;
}
前面的代码查询数据库并从 poll 表中检索第一行数据。您已经有了一个投票视图和一个将投票模型与投票视图联系起来的投票控制器。
保存您的工作,并在浏览器中加载 http://localhost/poll/index . PHP,查看它的运行情况。您应该看到投票表中的数据以 HTML 的形式表示和显示。只需花一分钟来惊叹您所看到的:一个数据库驱动的网页!
您可能还会欣赏 HTML 和数据之间的清晰分离。如果您不喜欢我在示例中使用的 HTML 元素,您可以很容易地将其更改为其他元素。改变 HTML 元素不会以任何方式影响 PHP 脚本。
同样,您很快就会看到,数据可以改变,而不必改变 HTML 元素。HTML 元素的内容将根据从数据库中检索到的值动态更新。
不过,有一个依赖,重要的是你要看到它。在views/poll-html.php
中,您需要一个$pollData
对象,并且它必须具有 yes 和 no 属性。因此,在views/poll.php
中,您必须创建这样一个对象,否则轮询将失败。该对象的名称和属性是至关重要的。
PDO 和 PDOStatement 对象
我想指出的是,在 PDO 将 SQL 传递给 MySQL 之前,应该将 SQL 字符串转换成一个PDOStatement
对象,然后在 MySQL 中执行 SQL。PDO 有一个叫做prepare()
的方法,它将一个简单的 SQL 字符串转换成一个PDOStatement
对象。
PDOStatement
对象有另一个方法,叫做execute()
,用于让 MySQL 执行 SQL。PDOStatement
s 还有一个方法,叫做fetchObject()
,从查询的数据库表中检索一行数据。
Note
可以在 www.php.net/manual/en/class.pdostatement.php
查阅PDOStatement
对象的官方文档。
PDOStatement 的 fetchObject()方法
fetchObject()
方法返回一个StdClass
对象,代表被查询表中的一行数据。在前面的代码中,您有一个 SQL 语句,它从投票表中选择了poll_question
、yes
和no
。
因此,返回的StdClass
对象将被自动创建,并带有poll_question
、yes
和no
的属性。您可以通过StdClass
对象属性访问投票表中的数据。在下面的内容中,您可以看到这些属性是如何在views/poll-html.php
中使用的,以在网页上显示从投票表中检索到的数据:
<?php
//complete code for views/poll-html.php
//$pollData->no holds the current value of the no attribute
//$pollData->yes holds the current value of the yes attribute
return "
<aside id='poll'>
<h1>Poll results</h1>
<ul>
<li>$pollData->yes said yes</li>
<li>$pollData->no said no</li>
</ul>
</aside>
";
您已经为$pollData
使用了一个StdClass
。最初,您对轮询的属性和值进行了硬编码。最大的不同是,fetchObject()
会自动创建一个新的StdClass
对象并返回它。创建的StdClass
对象的属性名与表格列名相同。
例如,$pollData
有poll_question
、yes
和no
属性,因为 SQL SELECT
语句创建了一个带有poll_question
、yes
和no
列的临时表。临时表从 MySQL 返回到 PHP。PDO 将接收到的数据转换成了一个StdClass
对象,因为您使用了fetchObject()
方法。
显示投票表格
你展示了数据库驱动的内容,这真是太棒了,但是你的例子还不是一个很好的网站投票。网站访问者应该被允许提交他们的意见,从而对投票结果做出贡献。不管怎样,你必须为站点访问者提供一个图形用户界面:HTML 表单是显而易见的选择。花几秒钟思考一下。这种表单的 HTML 属于哪里?在模型、视图或控制器中?在你继续阅读之前,想出你的最佳答案...
HTML 表单是用户看到的东西,所以它是一个视图。更新views/poll-html.php
中的投票视图代码,如下所示:
<?php
//complete code listing for views/poll-html.php
//new code below
$dataFound = isset( $pollData );
if( $dataFound === false ){
trigger_error( 'views/poll-html.php needs an $pollData object' );
}
return "
<aside id='poll'>
<form method='post' action='index.php'>
<p>$pollData->poll_question</p>
<select name='user-input'>
<option value='yes'>Yes, it is!</option>
<option value='no'>No, not really!</option>
</select>
<input type='submit' value='post' />
</form>
<h1>Poll results</h1>
<ul>
<li>$pollData->yes said yes</li>
<li>$pollData->no said no</li>
</ul>
</aside>
";
正如您可能已经想到的,这将显示一个 HTML 表单,非常类似于您在第三章中为动态测验创建的表单。你可以刷新你的浏览器看看它是什么样子。
触发自定义错误消息
你注意到那些触发错误信息的初始代码行了吗?好了,您已经确定了$pollData
对象对于脚本在输出投票时进行有意义的协作是至关重要的。
如果你——或者从事同一项目的开发人员——忘记创建一个$pollData
对象或者可能拼错了(例如,$pillData
),站点投票将不会正常进行。你可以测试一个$postData
对象是否可用。如果不是,您可以触发一个自定义的错误消息。这实际上是一种自助:如果您或其他开发人员偶然犯了这个错误,您可以相信系统会输出一个有意义的错误消息,这样就可以轻松快速地纠正错误。
当你独自开发新项目时,你可能会花大量的时间来修正错误。使用自定义错误信息,您可以通过测试您预测可能发生的错误来加快开发时间。
根据表单输入更新数据库表
完成站点投票示例还需要最后一步。您应该检索通过表单提交的任何用户输入,并用收到的输入更新投票表。如果一个站点访问者提交了一个 no,您应该增加轮询数据库表中的no
属性的值。
这与动态测验非常相似:您需要检测表单是否被提交。如果是,您可以检索提交的输入并相应地更新投票表。您可以看到这里有两个关注点:第一,检测输入;接下来,更新数据库。
更新数据库是模型的任务。处理用户交互是管制员的工作。您可以从用更新数据库表的方法更新轮询模型类开始。更新models/Poll.class.php
。
<?php
//declare a new method for the Poll class in models/Poll.class.php
//NB. Declare the method inside the Poll class code block
public function updatePoll ( $input ) {
if ( $input === "yes" ) {
$updateSQL = "UPDATE poll SET yes = yes+1 WHERE poll_id = 1";
} else if ( $input === "no" ) {
$updateSQL = "UPDATE poll SET no = no+1 WHERE poll_id = 1";
}
$updateStatement = $this->db->prepare($updateSQL);
$updateStatement->execute();
}
//no other code changes in Poll class
新方法updatePoll()
,应该相当容易理解。如果用户提交了 yes,那么您创建一个 SQL 字符串来更新 poll 表中的yes
属性。如果用户提交了一个 no,您创建一个不同的 SQL 字符串来更新no
属性。注意,无论提交的是 yes 还是 no,创建的 SQL 字符串都将存储在一个名为$updateSQL
的变量中。
一旦创建了$updateSQL
字符串,就使用 PDO 方法prepare()
将 SQL 字符串转换成一个PDOStatement
对象,该对象存储在变量$updateStatement
中。通过PDOStatement
,你可以execute()
查询到实际更新的民意测验表。
在被调用之前,updatePoll()
方法不会做任何事情。调用模型方法是控制器的工作...
响应用户输入
控制器负责处理用户交互。因此,poll controller
是从投票表单中截取用户输入的合适位置。脚本中还需要添加一些内容。
你必须检查表格是否已经提交。如果表单已提交,您应该获取通过表单接收的提交输入。当您调用方法到updatePoll()
时,接收到的输入应该被传递给模型。下面是如何在 controllers/poll.php 中用 PHP 来表达这一点:
<?php
//complete code listing for controllers/poll.php
include_once "models/Poll.class.php";
$poll = new Poll( $db );
//check if form was submitted
$isPollSubmitted = isset( $_POST['user-input'] );
//if it was just submitted...
if ( $isPollSubmitted ) {
//get input received from form
$input = $_POST['user-input'];
//...update model
$poll->updatePoll( $input );
}
//no changes here
$pollData = $poll->getPollData();
$pollAsHTML = include_once "views/poll-html.php";
return $pollAsHTML;
如果您输入此代码,您将拥有一个全功能的站点投票,允许站点访问者查看其他站点访问者对您的投票问题的看法。任何网站访问者都可以通过表单发表意见。输入将保存在数据库中,并显示在index.php
上。
您可以看到前面的脚本检查是否提交了调查表。它发生在剧本的顶部附近。代码寻找一个名为user-input
的 URL 变量。这样的 URL 变量将在提交投票表单时声明,因为投票表单有一个<select name='user-input'>
元素。投票表单使用了POST
方法,所以您的代码应该在$_POST
超全局中寻找用户输入,如下所示:
//check if form was submitted
$isPollSubmitted = isset( $_POST['user-input'] );
if ( $isPollSubmitted ) {
$input = $_POST['user-input'];
$poll->updatePoll( $input );
}
如果设置了 URL 变量,就知道已经提交了poll
表单。在这种情况下,您可以检索站点访问者的输入。接下来,代码调用Poll
对象中的updatePoll()
方法,并将用户输入作为参数传递。
摘要
现在您已经了解了全貌——您正在用面向对象的 PHP 开发数据库驱动的 web 应用!这幅图的某些部分可能还不太清楚。随着你阅读本书的其余部分,其中一些将会改变。但是真正的改变,真正有意义的学习,发生在你开始创建自己的项目的时候。
当你开始阅读这本书时,你可能对静态网站有所了解,也就是用手写 HTML 创建的网站。有了静态 HTML,你可以写一篇关于你认为 PHP 有多难/多容易的文章。用户将能够阅读你的观点——从你到你的用户的交流是单向的。
在前几章中,你学习了用 PHP 制作动态解决方案。您创建了动态测验,允许您的用户与您的内容进行交互。因为用户可能对测验问题有不同的回答,所以不同的用户会看到不同的内容。你的测验是一种交流你对学习 PHP 的看法的吸引人的方式。用户可以阅读他们的观点是否和你的一致,你可以写一个小测验,让它提供幽默的回答——交流仍然是单向的,从你到你的用户。
现在,您开始了解数据库驱动的网站。当您使用数据库存储内容时,新的可能性就出现了。用户可以向您创作的网站提供新内容。可以发布用户提交的新内容,以便所有站点访问者可以看到其他站点访问者所做的贡献。您制作了一个便于网站访问者之间交流的网页。注意现在的交流是多方向的,你不再是内容的唯一作者。随着您从静态解决方案发展到动态解决方案,再发展到数据库驱动的解决方案,这些都是向您展现的一些新的可能性。
在这一章中,你已经学习了 SQL 语句的基础知识,以及如何通过 PHP 脚本与数据库进行交互。在接下来的章节中,你将学习如何用一个基本的条目管理器来创建一个博客,它将允许你创建、修改和删除条目,以及在公共页面上显示它们。
七、构建条目管理器
至此,你已经足够了解如何开始构建你的个人博客系统了。这本书的其余部分涵盖了发展和改善个人博客。本章将带你了解如何构建你的博客应用的主干:博客条目管理器。您将构建的部分包括以下内容:
- 一个视图,它是一个接受条目输入的 HTML 表单
- 一个控制器,用于处理来自表单的输入
- 一个模型,用于保存和检索数据库中的条目
本章结束时,你将拥有一个基本的个人博客系统入口管理器。在构建条目管理器的过程中,您将会重温以前讨论过的主题,例如基本的数据库设计、SQL、用 MVC 组织您的 PHP,以及连接到数据库。
创建 blog_entry 数据库表
任何新应用最重要的步骤之一是规划保存数据的表。这对以后扩展应用的容易程度有很大的影响。扩展是应用处理更多信息和/或用户的扩展,如果在开始一个新项目时没有向前看,这可能是一个巨大的痛苦。首先,您的博客需要存储几种类型的条目信息才能运行,包括以下内容:
- 唯一标识
- 条目标题
- 条目文本
- 创建日期
为条目表中的每个条目使用一个唯一的 ID,使您能够访问只包含一个数字的信息。这对于数据访问非常有帮助,尤其是如果数据集在将来发生了变化(例如,如果您向表中添加了一个“imageURL”列)。
第一步是确定条目表需要的字段。您的表必须定义每列中存储什么类型的信息,所以让我们快速看一下每列必须存储的信息。
entry_id
:识别条目的唯一编号。这将是一个正整数,这个数字自动递增是有意义的,因为这样可以确保这个数字是唯一的。因为每个条目都有一个唯一的entry_id
,所以可以用这个数字来标识一个条目。entry_id
将是该表的主键。title
:应该相对较短的字母数字字符串。您将把字符串限制在 150 个字符以内。entry_text
:长度不定的字母数字字符串。你不会限制这个字段的长度(在合理的范围内)。date_created
:条目最初创建时自动生成的时间戳。您将使用它来按时间顺序对条目进行排序,并让您的用户知道条目最初是何时发布的。
现在是创建数据库的时候了。打开你的 XAMPP 控制面板,启动 MySQL 和 Apache。将浏览器指向 http://localhost/phpmyadmin/下面是执行此操作的 SQL:
CREATE DATABASE simple_blog CHARSET utf8
下一步是编写创建entries
表的代码。确保从 phpMyAdmin 控制面板左侧的菜单中选择simple_blog
数据库。接下来,打开 SQL 选项卡,输入以下 SQL 语句:
CREATE TABLE blog_entry (
entry_id INT NOT NULL AUTO_INCREMENT,
title VARCHAR( 150 ),
entry_text TEXT,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ( entry_id )
)
您可以看到创建blog_entry
表的语句类似于您用来创建 poll 表的CREATE
语句。你也可以看到一些不同之处。
title
属性有一个新的数据类型:VARCHAR(150)
。因此,任何标题都必须包含长度为VAR
的CHAR
角色。博客条目标题可以是 0 到 150 个字符长的字符串。如果您要插入 151 个字符长的标题,只有前 150 个字符会保存在blog_entry
表中。这就是VARCHAR(150)
的作用。
属性date_created
也用新的数据类型TIMESTAMP
声明。一个TIMESTAMP
保存着关于某一时刻的相当精确的信息。它将年、月、日、小时、分钟和秒存储为 YYYY-MM-DD HH:MM:SS
您已经看到了如何使用默认值创建 MySQL 表属性。这里再次针对date_created
属性。第一次插入新条目时,MySQL 会根据服务器的时钟自动存储当前的TIMESTAMP
。
规划 PHP 脚本
你已经为你的博客创建了一个数据库。合乎逻辑的下一步是用 PHP 创建一个博客条目编辑器,这样您就可以创建新的博客条目。博客条目编辑器只适用于博客作者。普通网站访问者应该不能创建新的条目。普通的站点访问者应该只是看到你的博客条目,而不能编辑现有的条目或创建新的条目。
完成这项任务的一个方法是创建两个主要的网站入口:index.php
供普通访问者使用,而admin.php
只供你观看。在 MVC 术语中,index.php
和admin.php
都是前端控制器。在本书的后面,我将向你展示如何通过登录来限制对admin.php
的访问。
管理页面应该能够列出所有的博客条目,它应该给你一个条目编辑器,这样你就可以创建新的条目,编辑或删除现有的条目。您将需要单独的视图:一个用于列出所有条目,另一个用于显示编辑器。
脚本admin.php
应该输出一个 HTML5 页面。它将是你的前端控制器,它将决定是显示编辑器还是列出所有条目。图 7-1 使用 MVC 思想来开发博客管理模块的示意图。
图 7-1。
Distribution of responsibilities
注意每个视图都有一个相应的控制器。还要注意,条目使用条目模型来显示所有条目,并由编辑器重用来保存新条目。让我们回顾一下 MVC 方法提出的关注点分离(见图 7-2 )。视图是用户可以看到的东西。模型包含内容。控制器负责将正确的视图与正确的模型连接起来,并将结果输出返回给用户。控制器还负责响应用户输入;通常这意味着更新模型。
图 7-2。
Model-view-controller
创建博客网站
在XAMPP/htdocs
中新建一个文件夹。调用新文件夹blog
。在blog
文件夹中,你可以创建另外四个文件夹:models
、views
、controllers
和css
。
您可以从图库项目中复制ch4/templates/page.php
文件。在blog/views/page.php
中保存一份page.php
的副本。同样,从图库项目中复制ch4/classes/Page_Data.class.php
,并在blog/models/Page_Data.class.php
中保存一个副本。在css/blog.css
中创建一个空白样式表。
现在是时候创建blog/admin.php
并编写一点代码来检查到目前为止一切都很好地一起工作了:
<?php
//complete code for blog/admin.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";
$pageData->addCSS("css/blog.css");
$pageData->content = "<h1>YES!</h1>";
$page = include_once "views/page.php";
echo $page;
保存文件并在浏览器中加载 http://localhost/blog/admin . PHP。如果一切按预期运行,您应该得到一个格式良好的 HTML5,带有一个 PHP/MySQL 博客演示的<title>
和一个高兴地欢呼是的<h1>
元素!在浏览器中看到“是”是对项目设置正确的直观确认。
创建条目管理器导航
您的条目管理器应该有两个基本视图:一个列出所有条目,另一个显示条目编辑器。让我们为条目管理器创建一个导航。在你完成之前,你可以期待这个项目包含许多 PHP 文件。我建议您创建一些文件夹,将与管理模块相关的脚本放在一起。在现有的views
文件夹中创建一个名为admin
的文件夹。在views/admin/admin-navigation.php
新建一个文件:
<?php
//complete code for views/admin/admin-navigation.php
return "
<nav id='admin-navigation'>
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
</nav>";
您可以看到,条目管理器导航与您在第四章和第五章中为动态图库创建的导航非常相似,或者与第二章中为动态作品集站点创建的导航非常相似。您应该记住 URL 变量page
,每当用户单击导航项目时,该变量就会被编码。
管理导航是一个静态视图,这意味着脚本中没有动态或数据库驱动的信息。导航不需要模型,因为所有内容都是硬编码到视图中的。您确实需要一个控制器来加载导航。这将非常简单,因为导航应该一直被加载和显示。你可以从admin.php
开始控制:
//partial code listing for admin.php
$pageData->addCSS("css/blog.css");
//code changes below here
//comment out or delete the YES
//$pageData->content = "<h1>YES!</h1>";
//load navigation
$pageData->content = include_once "views/admin/admin-navigation.php";
//no changes below
$page = include_once "views/page.php";
echo $page;
保存您的工作,并在浏览器中重新加载 http://localhost/blog/admin . PHP。您应该会看到浏览器窗口顶部显示的导航。单击导航项目不会有任何立即可见的效果;导航只是一个视图。单击任何导航项目都会对一个名为page
的 URL 变量进行编码。你可以在浏览器的地址栏里看到它。您需要创建控制器代码来响应用户交互,比如单击。
加载管理模块控制器
您可以使用admin.php
来控制当导航项目被点击时要做什么。这是前端控制器主要关心的问题。前端控制器应该加载与用户单击的导航项目相关联的任何控制器。在你的导航中有两个链接,所以你需要两个控制器。
要在单击导航项目时看到浏览器中的任何可见变化,您必须创建两个初级控制器。在controllers
文件夹中创建一个名为admin
的新文件夹,并创建一个新文件来控制条目编辑器视图,如下所示:
<?php
//complete source code for controllers/admin/editor.php
return "<h1>editor controller loaded!</h1>";
如您所见,编辑器控制器一开始不会做很多事情。最初,您只是想检查是否连接了正确的文件。有了这些,你就可以开发更复杂的代码了。创建另一个文件来控制最终列出所有条目的视图,如下所示:
<?php
//complete source code for controllers/admin/entries.php
return "<h1>entries controller loaded!</h1>";
您可以从admin.php
载入这些控制器。您必须检查导航项目是否被点击。如果是,您应该加载相应的控制器。在创建了$pageData
对象之后,在echo
之前插入这几行代码:
<?php
//complete code for blog/admin.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";
$pageData->addCSS("css/blog.css");
$pageData->content = include_once "views/admin/admin-navigation.php";
//new code begins here
$navigationIsClicked = isset( $_GET['page'] );
if ( $navigationIsClicked ) {
//prepare to load corresponding controller
$contrl = $_GET['page'];
} else {
//prepare to load default controller
$contrl = "entries";
}
//load the controller
$pageData->content .=include_once "controllers/admin/$contrl.php";
//end of new code
$page = include_once "views/page.php";
echo $page;
记住文件名entries.php
和editor.php
。这些名字是必不可少的。它们必须与用户单击导航项时声明的 URL 变量page
的相应值相匹配。让我们仔细看看导航中使用的href
值:
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
当用户点击All entries
项时,URL 变量page
得到一个值entries
。在admin.php
中,使用$_GET
获取 URL 变量page
。字符串值entries
存储在一个名为$contrl
的变量中,随后用于包含controllers/admin/$contrl.php
,这将真正转化为包含controllers/admin/entries.php
,因为变量$contrl
保存值entries
。
如果点击Editor
项,将包含controllers/admin/editor.php
。
在前面的代码中,您应该不会感到惊讶。尽管如此,明智的做法是进行理智检查。保存文件并在浏览器中重新加载 http://localhost/blog/admin . PHP。默认情况下,您应该会看到从条目控制器返回的消息。如果您单击编辑器的导航项,您应该会看到从编辑器控制器返回的消息。
创建条目输入表单
现在你有了一个动态导航,你不妨把它用在一些有意义的事情上。您可以为编辑器显示一个 HTML 表单。最终,应该可以使用编辑器创建新的博客条目。因此,编辑器表单应该有用于创建条目标题和条目文章的字段。还应该有一个保存新条目的按钮。同时,您也可以创建一个删除条目的按钮。创建一个新文件views/admin/editor-html.php
,如下所示:
<?php
//complete source code for views/admin/editor-html.php
return "
<form method='post' action='admin.php?page=editor' id='editor'>
<fieldset>
<legend>New Entry Submission</legend>
<label>Title</label>
<input type='text' name='title' maxlength='150' />
<label>Entry</label>
<textarea name='entry'></textarea>
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
</fieldset>
</fieldset>
</form>
";
大多数前面的代码应该是熟悉的,即使您看到了一些您以前可能没有遇到过的元素。你可以看到两个<fieldset>
元素。它们用于将相关的表单字段分组在一起。主<fieldset>
有一个<legend>
元素。一个<legend>
就像一个<fieldset>
元素的标题。
条目标题的<input>
元素的maxlength
属性设置为 150。您可能会猜到显示的文本字段只接受 150 个字符。这很好,因为数据库中的条目表接受最多 150 个字符的新title
属性。
属性增强了表单的可用性,因为用户很难通过表单创建无效的标题。属性执行客户端验证,只允许提交有效的标题。需要记住的一点是,客户端验证对于增强可用性非常重要。它不会提高安全性,因为您应该预料到恶意用户能够覆盖客户端验证。
创建了新的编辑器视图后,您必须更新控制器,以便它显示视图。编辑器的控制器可以在controller/admin/editor.php
中找到。更改代码,如下所示:
<?php
//complete source code for controllers/admin/editor.php
$editorOutput = include_once "views/admin/editor-html.php";
return $editorOutput;
当从admin.php
加载控制器时,这几行代码应该连接视图并显示它。重装 http://localhost/blog/admin . PHP 就可以自己看了?page =浏览器中的编辑器。您应该会看到表单。
样式编辑器
您可能会同意,无样式的条目编辑器表单看起来很难看。一点点 CSS 可以带你走很长一段路,提高审美。您很可能希望在编辑器的视觉设计上做更多的工作。这里有一个小小的 CSS 让你开始:
/* code listing for blog/css/blog.css */
form#editor{
width: 300px;
margin:0px;
padding:0px;
}
form#editor label, form#editor input[type='text']{
display:block;
}
form#editor #editor-buttons{
border:none;
text-align:right;
}
form#editor textarea, form#editor input[type='text']{
width:90%;
margin-bottom:2em;
}
form#editor textarea{
height:10em;
}
您可能还记得,也可能不记得,我在admin.php
中的代码期望在css/blog.css
中找到一个外部样式表。如果你的admin.php
和我的一样,你会希望你的样式表在那个位置。显然,您可以在不同的文件夹中用不同的名称创建样式表。记得更新admin.php
,让它指向你的样式表。你可以在图 7-3 中看到最终的编辑器设计。
我喜欢保持我的 HTML 整洁,没有id
和class
属性,我喜欢手工编写我的 CSS。我需要能够将样式规则与正确的 HTML 元素挂钩。我发现,通过使用上下文选择器和属性选择器,我经常可以应付过去。
如果你更喜欢使用 CSS 框架,你可能会采取完全相反的方法。您将手动编写很少的 CSS,并大量使用您最喜欢的 CSS 框架使用的class
和id
属性。你看着办吧!
图 7-3。
The editor displayed in Google Chrome
连接到数据库
您已经完成了基本的编辑器视图。很快,您应该能够通过编辑器表单将新的博客条目插入到您的blog_entry
数据库表中。为此,您需要一个从 PHP 应用到 MySQL 数据库的数据库连接。
您可以采用与轮询相同的方法:使用 PDO 建立连接,并在前端控制器中建立连接,以便与随后加载的控制器共享。
您可以在admin.php
中创建一个 PDO 对象,并与您将要加载的所有控制器共享它。在创建了$pageData
之后,在echo
之前,您应该在admin.php
中编写以下代码:
//partial code listing for admin.php
//new code starts here
$dbInfo = "mysql:host=localhost;dbname=simple_blog";
$dbUser = "root";
$dbPassword = "";
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//end of new code – no changes below
$navigationIsClicked = isset( $_GET['page'] );
if ($navigationIsClicked ) {
$contrl = $_GET['page'];
} else {
$contrl = "entries";
}
$pageData->content .=include_once "controllers/admin/$contrl.php";
$page = include_once "views/page.php";
echo $page;
请注意输入正确的数据库凭证,并指明您想要连接到simple_blog
数据库。您可以通过重新加载浏览器来测试连接是否正常。如果连接失败,您应该会看到一条错误消息。因此,如果您没有看到错误消息,这意味着您已经成功连接到数据库。
使用设计模式
设计模式是对一项常见任务的通用最佳实践解决方案。有些设计模式定义得相当全面。随着经验的增长,你会遇到更多的设计模式。作为一个绝对的初学者,你不需要对设计模式进行全面的处理——那可能会更令人困惑而不是更有帮助。
这本书使用了一些设计模式的简单实现。您已经看到了前端控制器设计模式的简单实现和同样简单的 MVC 实现。您可以看到在同一个项目中组合几种设计模式是可能的。
Note
你可以在 http://en.wikipedia.org/wiki/Software_design_pattern
了解设计模式。
许多设计模式对于初学者来说很难理解。但是许多常见的编码问题都有设计模式。这本书在简单的实现中使用了一些设计模式。您应该知道您在代码中使用的是哪种设计模式,以及还有其他的可能性。你可以把这样的知识当做未来发展的路线图。
Note
我可以强烈推荐 Matt Zandstra 的书《PHP 对象、模式和实践,第四版》(Apress,2013)。在 www.apress.com/9781430260318
找到它。这不是一本真正适合初学者的书,但你可以把它记在心里以备将来参考。
表格数据网关设计模式
您的代码必须与数据库表进行广泛的通信。有许多方法可以持续地管理这种沟通。一种方法是实现表数据网关设计模式。
表数据网关设计模式并不是大型 MVC 框架中处理数据库访问最常见的方法。许多流行的 PHP MVC 框架使用活动记录模式。我建议您使用表数据网关,因为这是一种相对容易理解的设计模式。表数据网关模式指定为数据库中的每一个表创建一个(PHP)类。这个想法是你的系统和那个表之间的所有通信都通过一个这样的对象发生。表数据网关封装了系统和特定数据库表之间的数据通信。
这意味着该通信所需的所有 SQL 语句都将在表数据网关类定义中声明。这有几个优点:一是您知道在哪里编写 SQL 语句。因此,您也知道在哪里可以找到与特定数据库表相关的 SQL 语句。
数据库设计和开发本身就是一种职业。如果作为一名 PHP 开发人员,您将所有的 SQL 封装在相对较少的类定义中,那么您团队中的任何数据库专家都只需要处理这几个类。与将 SQL 语句分散在代码库中相比,这是一个巨大的优势。
编写 Entry_Table 类
最初,您需要能够插入从编辑器表单接收的新博客条目。您可以为blog_entry
表的表数据网关创建一个新的类定义。它将用于与您的数据库通信。
新类将需要一个 PDO 对象来与数据库通信。你可以使用你在前一章已经看到的想法。使用构造函数方法创建一个类,并将 PDO 对象作为参数传递。停下来思考一下你将要写的类。应该是模型、视图,还是控制器?答案应该是显而易见的:负责与您的数据库通信的脚本应该是一个模型!
在名为Blog_Entry_Table.class.php
的新文件中的models
文件夹中创建一个新的类定义。下面是它的代码:
<?php
//complete code listing for models/Blog_Entry_Table.class.php
class Blog_Entry_Table {
private $db;
//notice there are two underscore characters in __construct
public function __construct ( $db ) {
$this->db = $db;
}
public function saveEntry ( $title, $entry ) {
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( '$title', '$entry' )";
$entryStatement = $this->db->prepare( $entrySQL );
try{
$entryStatement->execute();
} catch (Exception $e){
$msg = "<p>You tried to run this sql: $entrySQL<p>
<p>Exception: $e</p>";
trigger_error($msg);
}
}
}
在被另一个脚本使用之前,Blog_Entry_Table
不会做任何事情。在使用之前,我想看看这段代码。如您所见,Blog_Entry_Table
有一个属性db
和两个方法:constructor
和saveEntry()
。这个构造函数接受一个 PDO 对象作为参数。接收到的 PDO 对象将被存储在db
属性中。这样,Blog_Entry_Table
的所有方法都可以访问 PDO 对象,并通过它访问simple_blog
数据库。
在面向对象的术语中,Blog_Entry_Table
和 PDO 现在通过has-a
关系联系在一起。Blog_Entry_Table has-a
PDO 的对象。
在开发的早期阶段,您只能保存新条目。因此,Blog_Entry_Table
类除了构造函数之外只有一个方法。saveEntry()
方法有两个参数:title
和blog_entry
保存在数据库中。
变量$entrySQL
保存一个 SQL 字符串,以使用接收到的title
和blog_entry
插入一个新的blog_entry
。$entryStatement
是一个PDOStatement
对象,然后你可以try()
到execute()
来实际插入一个新的blog_entry
。
如果该操作失败,它将抛出一个Exception
对象,您的代码将捕获该对象。如果发生异常,您的代码将触发一个错误,显示导致异常的 SQL 字符串以及对异常的更详细的查看。
处理表单输入并保存条目
创建了Blog_Entry_Table
类后,您可以继续开发。合乎逻辑的下一步可能是处理从编辑器表单接收的输入,并使用一个Blog_Entry_Table
对象在数据库中保存一个新的博客条目。哪个负责处理用户交互?模型、视图还是控制器?MVC 设计模式规定用户交互应该在相关的控制器中处理。在这种情况下,相关的控制人是controllers/admin/editor.php
:
<?php
//complete code for controllers/admin/editor.php
//include class definition and create an object
include_once "models/Blog_Entry_Table.class.php";
$entryTable = new Blog_Entry_Table( $db );
//was editor form submitted?
$editorSubmitted = isset( $_POST['action'] );
if ( $editorSubmitted ) {
$buttonClicked = $_POST['action'];
//was "save" button clicked
$insertNewEntry = ( $buttonClicked === 'save' );
if ( $insertNewEntry ) {
//get title and entry data from editor form
$title = $_POST['title'];
$entry = $_POST['entry'];
//save the new entry
$entryTable->saveEntry( $title, $entry );
}
}
//load relevant view
$editorOutput = include_once "views/admin/editor-html.php";
return $editorOutput;
保存并测试您的编辑器。它应该能够插入新的博客条目。尝试通过编辑器表单创建一些新的博客条目。一旦您完成了这些,您就可以使用 phpMyAdmin 控制面板浏览您的blog_entry
表的内容。您可以在您的blog_entry
表中找到创建的博客条目。你的条目编辑器工作了!
哪个按钮被点击了?
条目编辑器表单中有两个按钮。最终,您会希望脚本根据单击的按钮做出不同的响应。所以,你的代码必须知道哪个按钮被点击了。让我们来看看产生按钮的 HTML:
//partial source code for views/admin/editor-html.php
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
</fieldset>
看看这两个不同的按钮如何具有相同的name
属性。按钮之间唯一的区别是它们的value
属性。您可以使用该知识来识别哪个按钮被单击了。实际上,你已经在controllers/admin/editor.php
做到了:
//one line of code from controllers/admin/editor.php
//don't make any changes
$buttonClicked = $_POST['action'];
变量$buttonClicked
将保存被点击按钮的值。所以,如果用户点击了保存按钮,$buttonClicked
将保存相应的值'save'
。如果$buttonClicked
的值为save
,那么您知道用户正在尝试插入一个新条目。看看下面一行。变量$insertNewEntry
会持有什么?
//one line of code from controllers/admin/editor.php
//don't make any changes
$insertNewEntry = ( $buttonClicked === 'save' );
在括号内,变量$buttonClicked
与字符串'save'
进行比较。如果$buttonClicked
与'save'
相同,$insertNewEntry
的值将为true
。如果不是,它将有一个值false
。在随后的代码中,您可以看到,如果$insertNewEntry
为真,则会插入一个新的blog_entry
。所以你的大部分代码只有在用户点击保存按钮时才会运行。
安全警报:SQL 注入攻击
saveEntry()
方法展示了一个漂亮的编码实践。它使用表单输入数据动态生成 SQL 字符串。这是一个很好的技巧,因为它是将用户输入插入数据库表的关键。但这也是一个巨大的安全漏洞。SQL 注入攻击可能是对数据库驱动的 web 应用最常见的攻击。您的条目编辑器现在很容易受到这种攻击。
Note
在 http://en.wikipedia.org/wiki/SQL_injection
了解更多关于 SQL 注入的信息。
基本上,SQL 注入攻击利用了通过表单输入的文本被用来动态生成 SQL 语句这一事实。用户通过表单输入的任何内容都将用于 SQL:
//the code that makes you vulnerable
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( '$title', '$entry' )";
看看前面的代码。变量$title
和$entry
是从表单接收的文本的简单占位符。通过表单输入的任何内容都将成为 SQL 字符串的一部分。因此,对 SQL 有深入了解的恶意用户可以通过表单输入恶意 SQL,从而直接访问您的数据库表。您保存在数据库中的任何数据都可能会暴露。攻击者可能会在表单的输入字段中输入类似如下的字符串:
attack'); DROP TABLE blog_entry;--
有了这个输入,$entrySQL
就会变成
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( '$title', ' attack'); DROP TABLE blog_entry;--' )";
您可以看到,$entrySQL
现在保存了两条 SQL 语句:一条用于插入一个blog_entry
,另一条用于删除整个blog_entry
表。很明显,您不希望用户能够在您的系统中删除表!您应该知道前面描述的特定攻击实际上不会起作用。我不想教你如何执行 SQL 注入攻击,但我想让你知道这个漏洞。
可用性警告:带有引号字符的博客条目
条目编辑器也有一个主要的可用性缺陷。(如果您的编辑器容易受到最常见类型的黑客攻击还不够的话!)如果您通过编辑器的输入字段输入以下文本,您可以自己看到问题:
Brennan's child said: "I want some breakfast!"
PDO 抛出了一个例外。罪魁祸首是单引号字符:
//the code that makes your form submission break
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( '$title', '$entry' )";
因为变量$title
和$entry
包含在单引号字符中,所以您的条目编辑器表单不能处理任何包含单引号字符的内容。因此,您键入的句子会导致异常。
只需对代码做一点小小的改动,您就可以用双引号将$title
和$entry
括起来,但是这样一来,您的条目编辑器将无法处理任何带有双引号字符的条目。所以,这个句子仍然会导致一个异常。
解决方案:准备好的报表
很明显,动态生成 SQL 字符串的代码给你带来了很多痛苦。这使你容易受到 SQL 注入的攻击,正因为如此,你的博客文章必须没有单引号或双引号字符。PDO 优雅地支持一个可以同时解决这两个问题的功能。PDO 支持事先准备好的声明。
Note
这里有一个很好的教程,可以进一步了解 PDO 和准备好的语句: http://net.tutsplus.com/tutorials/php/why-you-should-be-using-phps-pdo-for-database-access
。
预准备语句是为动态内容准备的 SQL 语句。您可以在 SQL 语句中声明占位符,然后用实际值替换这些占位符,而不是直接在 SQL 语句中删除 PHP 变量。如果您使用 PDO 预准备语句,您将不会受到 SQL 注入攻击,也不会因为单引号和双引号等特殊字符而遇到任何麻烦。下面是如何在models/Blog_Entry_Table.class.php
中做到的:
//partial code for models/Blog_Entry_Table.class.php
//edit existing method
public function saveEntry ( $title, $entry ) {
//notice placeholders in SQL string. ? is a placeholder
//notice the order of attributes: first title, next entry_text
$entrySQL = "INSERT INTO blog_entry ( title, entry_text )
VALUES ( ?, ?)";
$entryStatement = $this->db->prepare( $entrySQL );
//create an array with dynamic data
//Order is important: $title must come first, $entry second
$formData = array( $title, $entry );
try{
//pass $formData as argument to execute
$entryStatement->execute( $formData );
} catch (Exception $e){
$msg = "<p>You tried to run this sql: $entrySQL<p>
<p>Exception: $e</p>";
trigger_error($msg);
}
}
有三个同样重要的变化,如下所示:
characters are used as placeholders in the SQL string. An array is created with the dynamic data. The order of items must match the order used in the SQL string. Pass the array with dynamic data as an argument to the execute()
method.
你可以自己测试。用单引号和双引号插入条目。你将很难证实你的编辑器不再容易受到 SQL 注入的攻击。您必须能够执行 SQL 攻击,才能验证编辑器现在是安全的。我想你必须相信我的话。
摘要
这是一个相对较短的章节,只演示了几个新的编码原则。但是你已经向个人博客系统迈出了第一步,并且开发了一个超酷的条目编辑器。
在这个过程中,你有机会更加熟悉你在前面章节中学到的几乎所有东西。你有
- 创建了一个带有表的 MySQL 数据库
- 通过 web 表单将数据插入到表格中
- 创建了 MVC web 应用结构
- 使用前端控制器设计模式
- 使用了表格数据网关设计模式
除此之外,您还了解了 SQL 注入攻击,并了解了如何使用 PDO 准备好的语句来保护您的 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 开发人员,你在未来一定会再次遇到。
十一、向博客条目添加图像
你的博客已经完成了!当然还有改进的空间,但它功能齐全。管理模块可以用来创建博客条目,但是有一些严重的缺陷。
- 如果数据库中有与博客条目相关的评论,它不能删除该条目。
- 它不能给博客文章添加图片,我相信你会想用图片给你的博客文章增添情趣。
- 为了写博客,管理员必须了解 HTML,并不是所有伟大的作家都同样擅长 HTML。
- 管理员可以创建一个没有标题的条目,有效地使用户无法访问博客条目。
我相信你可以想出其他你想添加到管理模块的功能。本章将实现刚刚列出的特性。在此过程中,您将了解以下内容:
- 删除条目和相关注释
- 为您的条目编辑器使用 WYSIWYG 编辑器
- 将图像上传到服务器
- 从服务器中删除图像文件
问题:无法删除带有注释的条目
我相信你会同意评论系统是一个很大的改进。不幸的是,注释也在管理模块中引入了不必要的系统行为。无法删除带有注释的条目。
我想让你在解决问题之前看到它。转到http://localhost/blog/admin.php?page=editor
并创建新的博客条目。现在将你的浏览器指向http://localhost/blog/index.php
,点击阅读你刚刚创建的条目。通过意见表为新条目添加一两条意见。现在你有了一个带有评论的博客条目。
在你的浏览器中加载http://localhost/blog/admin.php?page=entries
并点击你刚刚写的博客条目的标题。博客条目将被加载到您的博客条目编辑器中。点击删除,尝试删除帖子。单击 Delete 应该会给出一条类似于下面所示的错误消息。
Exception: exception 'PDOException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row:
a foreign key constraint fails
了解外键约束
遇到错误总是有点烦人,但这是一个友好的错误。这确实可以防止您破坏数据库的完整性。Table 11-1 查看我的表,以查看我通过编辑器表单创建的博客条目。
表 11-1。
One Row from the blog_entry
Table
条目 id | 标题 | 条目 _ 文本 | 创造 |
---|---|---|---|
Seventeen | 删除我 | 测试测试 | 2014-03-23 10:26:18 |
我还创建了一个与该特定博客条目相关的评论。表 11-2 显示了注释表中的相应行。
表 11-2。
One Row from the Comment Table
comment_id | 条目 id | 作者 | 文本文件(textfile) | 日期 |
---|---|---|---|---|
four | Seventeen | 托马斯 | 测试注释 | 2014-03-23 10:26:40 |
假设你用entry_id = 17
删除了blog_entry
。你可能会有一个与不再存在的博客条目相关的评论。注释只有在正确的上下文中才有意义。与comment_id = 4
的评论会失去它的上下文;它会失去它的完整性。你可以想象如果用entry_id = 17
插入一个新的博客条目会发生什么。该博客条目将带有完全不相关的评论。
外键约束的目的是维护数据完整性。所以,当你试图删除一个有评论的blog_entry
时,MySQL 会阻止你,因为删除操作会在你的系统中留下一个叛逆的评论,没有有意义的上下文。只有没有评论的博客条目可以被删除,而不会丢失数据完整性。
删除博客条目前的评论
一旦你看到只有没有评论的博客条目才能被删除而不丧失数据完整性,那么解决方案就很容易看到了:删除博客条目时,你应该先删除任何与该条目相关的评论。您已经有了一个类,它提供了对注释表的单点访问。您可以添加一个新方法来删除与特定entry_id
相关的所有注释,如下所示:
//partial code for models/Comment_Table.class.php
//declare new method
public function deleteByEntryId( $id ) {
$sql = "DELETE FROM comment WHERE entry_id = ?";
$data = array( $id );
$statement = $this->makeStatement( $sql, $data );
}
应该在删除博客条目之前调用此方法。博客条目从Blog_Entry_Table
类中删除。一个解决方案是让Blog_Entry_Table
类使用Comment_Table
类,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//edit existing method
public function deleteEntry ( $id ) {
//new code: delete any comments before deleting entry
$this->deleteCommentsByID( $id );
$sql = "DELETE FROM blog_entry WHERE entry_id = ?";
$data = array( $id );
$statement = $this->makeStatement( $sql, $data );
}
//new code: declare a new private method inside Blog_Entry_Table.class.php
private function deleteCommentsByID( $id ) {
include_once "models/Comment_Table.class.php";
//create a Comment_Table object
$comments = new Comment_Table( $this->db );
//delete any comments before deleting entry
$comments->deleteByEntryId( $id );
}
前面的代码完成后,您就可以开始测试了。您应该能够通过编辑器删除任何博客条目。如果有任何与blog_entry
相关的注释,这些注释将首先被删除,以避免违反外键约束。
利用所见即所得提高可用性
所见即所得的缩写。有了 WYSIWYG HTML 编辑器,对 HTML 知之甚少或一无所知的管理员可以编写以编程方式标记为 HTML 的博客条目。所见即所得编辑器看起来很像任何其他文字处理软件,但它可以将文本保存为 HTML,而不是保存为.doc
或.odf
文件。
集成 TinyMCE
TinyMCE 是一个流行的开源所见即所得编辑器。它集成在许多流行的 CMS 系统中,如 Drupal、Joomla 和 WordPress。很快,它将集成到您的条目编辑器中,这将是一个非常棒的改进。
集成 TinyMCE 相当容易。第一步,从 www.tinymce.com/download/download.php
下载。解压缩下载的文件,并在现有的js
文件夹中保存一份tinymce
文件夹的副本。查看一下js/tinymce
内部,确认您可以找到主 JavaScript 文件tinymce.min.js
。这就是你的admin.php
应该使用的 JavaScript 文件。
TinyMCE JavaScript 和 CSS 文件可以将普通的<textarea>
元素变成所见即所得编辑器。你所要做的就是初始化 TinyMCE 并把它指向你的文本区域。您可以从views/admin/editor-html.php
开始,如下所示:
//partial code for views/admin/editor-html.php
//notice the new <script> elements added at the end
return "
<form method='post' action='admin.php?page=editor' id='editor'>
<input type='hidden' name='entry_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>
<script type='text/javascript' src='js/tinymce/tinymce.min.js'> </script>
<script type='text/javascript'>
tinymce.init({
selector: 'textarea',
plugins: 'image'
});
</script>";
看看这两个<script>
元素。第一个嵌入了主 TinyMCE JavaScript 文件。第二个函数初始化 TinyMCE 并用几个参数对它进行配置。
selector
参数表示 TinyMCE 应该将页面上的每个<textarea>
元素转换成花哨的 WYSIWYG 编辑器。在这个特殊的页面上,只有一个<textarea>
元素。
plugins
参数指示应该为编辑器激活哪些 TinyMCE 插件。图像插件将允许用户在博客条目中插入图像。
大多数时候,你不应该在你生成的 HTML 中散布小的<script>
元素。您在代码中保留的<script>
元素越多,您就越难追踪 JavaScript 错误。但是在这种情况下,我会接受这种不适,因为我只希望 TinyMCE 编辑器出现在这个特定的页面上。我特别不希望 TinyMCE 控制我博客系统中的任何其他<textarea>
元素。要看 TinyMCE 长什么样,见图 11-1 。您也可以将浏览器导航至http://localhost/blog/admin.php?page=editor
。
图 11-1。
The TinyMCE WYSIWYG editor
在图 11-1 中,我稍微修改了一下我的 CSS,使编辑器<form>
的宽度为 625 像素。我在css/blog.css
做了改动。
创建图像管理器
此时,admin
模块有两个不同的页面视图:条目列表和条目编辑器。您可以创建第三个页面来上传和删除您将在博客条目中使用的图像。您可以从创建图像管理器的菜单项开始。更新views/admin/admin-navigation.php
中的代码,如下所示:
<?php
//complete code for views/admin/admin-navigation.php
//notice item added for image manager
return "
<nav id='admin-navigation'>
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
<a href='admin.php?page=images'>Image manager</a>
</nav>";
参见href
中的图像管理器菜单项。单击该项将对一个名为page
的 URL 变量进行编码,并将其值设置为images
。由于你在admin.php
中编写前端控制器的方式,你可能会猜到下一步:你需要一个新的控制器脚本,名为images.php
。另外,在controllers/admin
中保存也很重要。这样,每当点击菜单项图像管理器时,控制器将从admin.php
自动加载。像往常一样,从一小步开始,在错误仍然容易纠正的时候捕捉它们。在controllers/admin/images.php
中创建一个新文件。
<?php
//complete code for controllers/admin/images.php
$imageManagerHTML = "Image manager coming soon!";
return $imageManagerHTML;
在您的浏览器中加载http://localhost/blog/admin.php?page=images
,您应该会看到确认您的脚本按预期运行的输出。
Image manager coming soon!
显示用于上传图像的表单
既然已经设置了 image manager 控制器脚本,就可以继续创建和输出 image manager 视图了。让我们从一个基本的 HTML 表单开始,您最终可以用它来上传图像。创建一个新的 php 文件,并将其另存为views/admin/images-html.php
:
<?php
//complete code for views/admin/images-html.php
if ( isset( $uploadMessage ) === false ){
$uploadMessage = "Upload a new image";
}
return "
<form method='post' action='admin.php?page=images'
enctype='multipart/form-data'>
<p>$uploadMessage</p>
<input type='file' name='image-data' accept='image/jpeg' />
<input type='submit' name='new-image' value='upload' />
</form>
";
您可以看到,views
代码准备了一个占位符,用于向用户显示上传消息。默认上传消息是上传新图像。很快,你的系统就会让用户知道上传是否成功。但是在你开始之前,我想重复一下上传的基本知识。
要允许用户上传图像等文件,您需要一个 HTML 表单。您必须使用 HTTP 方法POST
,并且必须显式声明表单的编码类型,以允许文件上传。默认情况下,HTML 表单被设置为application/x-www-form-urlencoded
。然而,这在你上传文件的时候是行不通的。相反,您必须将表单的 enctype 设置为multipart/form-data
,它将接受文件和普通 URL 编码的表单数据。
带有type=file
的<input>
元素将创建一个文件选择器,允许用户浏览他们的本地计算机来上传文件。这个特殊的文件选择器有一个accept
属性,限制可以选择的文件类型。这个文件选择器将只接受 JPEG 图像。请记住,客户端验证可以提高可用性,但不能提高安全性。可以相信恶意用户能够绕过任何类型的客户端验证。为了保护您的系统免受攻击,您必须实现服务器端验证。回头给你点提示。首先,更新 image manager 控制器,以便显示上传表单。更改controllers/admin/images.php
中的代码:
<?php
//complete code for controllers/admin/images.php
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
保存并将浏览器指向http://localhost/blog/admin.php?page=images
,以确认表单确实显示在您的浏览器中。
$_FILES 超全局阵列的快速复习
你在第四章中了解到了$_FILES
超级地球,但是在继续之前回顾一下它的作用可能会有所帮助。每当一个文件通过 HTML 表单上传时,该文件被存储在临时内存中,并且关于该文件的信息在$_FILES
超级全局变量中传递。你可以通过查看传递给你的images
控制器的内容来了解这一点。将此代码添加到controllers/admin/images.php
的顶部,如下所示:
<?php
//complete code for controllers/admin/images.php
$imageSubmitted = isset( $_POST['new-image'] );
if ( $imageSubmitted ) {
$testOutput = "<pre>";
$testOutput .=print_r($_FILES, true);
$testOutput .= "</pre>";
return $testOutput;
}
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
您可以通过在浏览器中加载http://localhost/blog/admin.php?page=images
来测试您的代码。使用该表单选择要上传的图像。您的代码不会将上传的文件保存在您的服务器上,但是您可以从下面的输出中看到 PHP 可以访问该文件:
Array (
[image-data] => Array (
[name] => alberte-lea.jpg
[type] => image/jpeg
[tmp_name] => /Applications/XAMPP/xamppfiles/temp/phprDui5l
[error] => 0
[size] => 119090
)
)
你可以看到$_FILES
是一个Array
。第一个Array
在命名索引image-data
下持有另一个Array
。默认情况下,$_FILES
将提供关于文件名、类型、tmp_name
、error
和size
的信息。但是你必须问自己一个重要的问题是,image-data
这个名字是从哪里来的?
答案是你提供的!这里的image-data
是因为您为文件选择器元素编写的 name 属性。如果您在views/admin/images-html.php
中查看您的代码,您可以找到您设置文件选择器的 name 属性的地方:
//one line of code from views/admin/images-html.php
<input type=``'file' name='image-data'
因为您将image-data
设置为文件选择器的 name 属性,PHP 可以在嵌套在$_FILES
中的数组中找到相关的文件数据,该数组位于命名索引image-data
下。
嵌套数组保存了您试图用这个特定的文件选择器上传的图像的信息。您可以看到原始图像名称及其 mimetype。您可以看到图像数据被临时保存在您的服务器上,临时名称为tmp_name
。可以看到错误代码0
,表示上传过程中没有出现错误。您还可以看到图像文件的size
,以字节为单位。
如果您在浏览器中看到类似的内容,您只需一两行代码就可以上传图片了。你所要做的就是把文件数据保存在你的服务器上。文件数据已经上传并临时保存在tmp_name
下image-data
数组内$_FILES
内。要获取文件数据,您只需按照下面的方式编写一些内容:
//don't write this anywhere...yet
$fileData = $_FILES['image-data']['tmp_name'];
注意你的代码是如何访问$_FILES
来找到数组image-data
的。在image-data
内部,PHP 找到了tmp_name
。要保存文件数据,只需将其从临时位置移走,并以一个名称保存在目标文件夹中。
但是我建议你不要在controllers/admin/images.php
中这样做,主要是因为你想写代码来处理上传过程中可能出现的一些错误。在未来的某一天,您将需要再次通过表单上传文件。因此,您可以编写一个可重用的类来上传,而不是每次需要时都重新发明一个解决方案。这样,您可以在许多项目中重用您的上传代码,而无需更改它。
想起来了,你已经在第四章的中写了一个Uploader
类。你可以照原样使用它。从简单地重用Uploader
开始。在这个过程中,我会指出进一步改进Uploader
的方法。在XAMPP/htdocs/ch4/classes/Uploader.class.php
中复制一个Uploader
类,并将副本保存在XAMPP/htdocs/blog/models/Uploader.class.php
中。或者,如果你从本书的配套网站 www.apress.com
下载第四章的源代码,你也可以获得Uploader
类。
上传图像
您可以尝试使用Uploader
类上传图像。首先为图像创建一个新文件夹。我在项目的根目录下创建了一个名为img
的文件夹。我会上传图片到img
文件夹。
提交图像管理器上传表单时,您的代码应该尝试上传指定的文件。提交表单是一个用户交互,所以代码属于控制器,在controllers/admin/images.php
中:
<?php
//complete code for controllers/admin/images.php
//new code: include Uploader class definition
include_once "models/Uploader.class.php";
$imageSubmitted = isset( $_POST['new-image'] );
//if the upload form was submitted
if ( $imageSubmitted ) {
//new code below
//create an Uploader object
$uploader = new Uploader( 'image-data' );
//indicate destination folder on server
//please check that you have an img folder in your project folder
$uploader->saveIn( "img" );
$uploader->save();
$uploadMessage = "file probably uploaded!";
//end of new code
}
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
一切都准备好上传了。您可以通过将浏览器指向http://localhost/blog/admin.php?page=images
来测试您的代码。现在,尝试通过表单上传一张图片。您的代码现在应该能够上传文件了,所以您应该能够在img
文件夹中找到您上传的图像。
此时遇到的最常见的错误与文件夹权限有关。当您在像 XAMPP 这样的本地 web 服务器上开发时,这种问题尤其常见。如果你的目标文件夹是写保护的,PHP 不能保存上传文件。因此,如果您无法上传文件,请尝试更改img
文件夹的文件夹权限。将权限设置更改为read & write
。
会出什么问题呢?
当您尝试通过表单上传文件时,可能会出现一些问题。有八个可能的错误代码与$_FILES
相关,其中一个错误代码表示没有错误。实际上,在上传过程中潜伏着七个以上的潜在问题。如果当你遇到这些错误时,你的整个博客系统暂时崩溃,那将是一个遗憾。系统可以在不崩溃的情况下处理错误。是时候学习如何编写优雅地失败的代码了。
更新上传程序类
您可以更改Uploader
类,这样它会正常地失败,并在失败时提供有意义的错误消息。为此,您需要一个属性来存储错误消息,另一个属性来存储遇到的任何标准 PHP 错误代码。在models/Uploader.class.php
中添加几个属性。
//partial code for models/Uploader.class.php
//edit existing Uploader class
class Uploader {
private $filename;
private $fileData;
private $destination;
//new code: add a property for an error message
private $errorMessage;
//new code: add a property for standard PHP error codes
private $errorCode;
查看代码,注意新属性没有赋值。每当您的代码准备尝试上载时,您都必须为这些属性分配实际值。$_FILES
数组将立即提供一个错误代码。在Uploader
的构造函数方法中获取当前的错误代码是一个显而易见的选择,因为您知道构造函数将只运行一次,就像一个新的Uploader
对象被创建一样:
//partial code for models/Uploader.class.php
//edit existing constructor method
public function __construct( $key ) {
$this->filename = $_FILES[$key]['name'];
$this->fileData = $_FILES[$key]['tmp_name'];
//new code: remember the current upload error code
$this->errorCode = ( $_FILES[$key]['error'] );
}
我怀疑你已经在第四章中读到了大部分代码,就像你输入的一样。我也强烈怀疑你没有完全理解每一个小细节。如果你像大多数学习者一样,你将不得不允许你对代码的理解在一小步一小步中增长。每一小步,你都会学到东西,总会有你没学到的东西。通过重复已经学过的课程,你可以逐渐形成更全面的理解。您可以仔细看看在Uploader
类中使用的$key
参数。请记住,要获取文件数据,您可以编写如下内容:
$fileData = $_FILES['image-data']['tmp_name'];
tmp_name
是由$_FILES
提供的默认名称,image-data
是在设置 HTML 文件选择器输入元素的name
属性时提供的名称。您希望您的Uploader
类能够上传文件,而不管文件选择器的名称属性。总是依赖image-data
将是一个糟糕的设计决策。所以,我为你做的设计决定是,Uploader
类应该被赋予 used name
属性作为构造函数方法的参数。就是$key
的说法。这里有一个例子来告诉你这个想法:
//Example: don't write any of this code anywhere
//this would upload from <input type='file' name='image-data' />
$imgUploader = new Uploader("image-data");
//this would upload from <input type='file' name='video-file' />
$vidUploader = new Uploader("video-file");
您可能会看到,因为相关的 name 属性是作为参数提供的,Uploader
类更容易在不同的情况下重用。
错误:限制性文件夹权限
当您在像 XAMPP 这样的本地 web 服务器上进行开发时,经常会遇到文件夹权限过于严格的情况。如果一个文件夹是只读的,PHP 不能将上传图像的文件数据写入该文件夹。它不属于通过$_FILES
错误代码报告的标准错误。但是测试一个目标文件夹是否可写是非常简单的。你已经在第四章中做过了,但是我想稍微修改一下代码。您可以通过在Uploader
中声明一个新方法来检查问题,如下所示:
//partial code for models/Uploader.class.php
//declare a new private method in the Uploader class
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
//provide a meaningful error message
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
//indicate that code is NOT ready to upload file
$canUpload = false;
} else {
//assume no other errors - indicate we're ready to upload
$canUpload = true;
}
return $canUpload ;
}
和往常一样,方法在被显式调用之前不会运行。因此,您应该调用这个新方法,就像代码试图保存上传文件一样。您应该完全重写models/Uploader.class.php
中的现有方法save()
,如下所示:
//partial code for models/Uploader.class.php
// rewrite existing method save() completely
public function save () {
//call the new method to look for upload errors
//if it returns TRUE, save the uploaded file
if ( $this->readyToUpload() ) {
move_uploaded_file(
$this->fileData,
"$this->destination/$this->filename" );
} else {
//if not create an exception - pass error message as argument
$exc = new Exception( $this->errorMessage );
//throw the exception
throw $exc;
}
}
本地 PHP 函数move_uploaded_file()
将上传的文件数据保存在服务器上的一个新的目的地。这是一个敏感的过程,只有在没有潜在错误等待发生的情况下,您的代码才应该尝试这样做。
还记得在用 PDO 建立到数据库的连接时,您是如何使用try-catch
语句的吗?我怀疑你发现尝试一些可能出错的东西,然后捕捉任何异常有点抽象。什么是例外呢?一个例外是由 PHP 的原生Exception
类生成的 PHP 对象。当可能出错的事情确实出错时,PHP 会抛出一个异常。
打个比喻,你可以把一个异常想象成一封有坏消息的信。抛出异常就像是在途中发送信件一样。要阅读坏消息,你必须收到这封信。异常也是这样,但是它们是被捕获的,而不是被发送到邮箱。
抛出异常与触发错误非常相似,但有一个显著的区别:异常很容易被捕获,因此您的代码可以继续运行,并且您的代码可以优雅地解决问题。当您调用一个可能抛出异常的方法时,您可以将该方法调用包装在一个try-catch
语句中,从而编写一个优雅地失败的系统。
Note
也可以拦截普通的 PHP 错误。捕捉异常是面向对象的方法。
你就快到了。您的Uploader
类检查上传文件是否太大,如果太大,就会抛出一个异常。要创建一个正常失败的图像管理器,您必须编写代码来捕捉任何可能抛出的异常。您可以从controllers/admin/images.php
开始,如下所示:
//partial code for controllers/admin/images.php
//edit existing if-statement
if ( $imageSubmitted ) {
$uploader = new Uploader( 'image-data' );
$uploader->saveIn( "img" );
//try to save the upload file
try {
$uploader->save();
//create an upload message that confirms succesful upload
$uploadMessage = "file uploaded!";
//catch any exception thrown
} catch ( Exception $exception ) {
//use the exception to create an upload message
$uploadMessage = $exception->getMessage();
}
}
该测试了,但首先,你要确定有问题。您可以更改img
文件夹的文件夹权限。制作文件夹Read-only
。现在,在你的浏览器中加载http://localhost/blog/admin.php?page=images
,并尝试通过你的表单上传一个图像文件。
图像不应该被上传,您也不应该看到任何标准的 PHP 错误消息。您应该会看到一条从 PHP 脚本发出的消息。
Error: destination folder is not writable, change permissions
一旦您确认您的代码发现了文件夹权限问题并提供了一条错误消息,您可以将文件夹权限改回Read & Write
,这样您就可以再次上传图像。
错误:上传文件太大
您已经成功构建了一个图像管理器,它可以正常失败,但前提是目标文件夹不可写。文件上传过程中还潜伏着许多其他潜在的问题。一个是 PHP 配置了最大文件上传大小。因为上传有最大文件大小限制,一些用户可能会尝试上传太大的文件。凡是能破的,用户一定会破。您的系统应该为此类用户提供有意义的错误消息。
对一个上传错误的测试涉及到几个不同文件中相当多的代码更改。测试多一个上传错误是非常容易的。只需在私有的readyToUpload
方法中添加一个额外的else-if
块:
//partial code for models/Uploader.class.php
//edit existing method in the Uploader class
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
$canUpload = false;
//new code: add an else-if code block to test for error code 1
} else if ( $this->errorCode === 1 ) {
$maxSize = ini_get( 'upload_max_filesize' );
$this->errorMessage = "Error: File is too big. ";
$this->errorMessage .= "Max file size is $maxSize";
$canUpload = false;
//end of new code
} else {
//assume there are no other errors
//indicate that we're ready to upload
$canUpload = true;
}
return $canUpload ;
}
现在,您的代码可以进行测试了。你应该有一个系统,如果上传一个太大的文件有问题,它会优雅地失败。为了测试您的工作,您应该尝试上传一个大于最大上传文件大小的文件。嗯,我的最大上传文件大小是多少?我几乎能听到你的问题。我很高兴你问了这个问题,因为这给了我一个写更多关于 PHP 配置的借口。
要查看您的 PHP 是如何配置的,您可以创建一个新的 PHP 文件,例如,在您的 blog 文件夹中。称之为test.php
:
<?
//complete source code for test.php
phpinfo();
保存文件并在浏览器中加载http://localhost/blog/test.php
。您可以看到很多关于您的 PHP 安装的信息——我的意思是,很多!
我发现我的安装有一个 128MB 的upload_max_filesize
。此设置声明了上传文件的上限。
为了测试当用户试图上传太大的文件时,我的Uploader
类是否会提供错误消息,我可以尝试上传大于 128MB 的 JPEG 图像!我没有那么大的 JPEG 图片。我改改upload_max_filesize
吧。
通过 ini.php 或配置。文件
当试图配置 PHP 时,您有几个选择。如果您拥有自己的服务器,您可以直接在 PHP 配置文件中更改upload_max_filesize
。它叫做ini.php
,phpinfo()
的输出可以告诉你ini.php
保存在你电脑的什么地方,如果你在Loaded Configuration File
下找的话。您可以使用 Komodo Edit 或任何其他文本编辑器打开ini.php
,并手动更改upload_max_filesize
。一旦保存了更改,就应该重启 Apache 服务器。
当你在互联网上发布你的 PHP 项目时,你通常会使用一个共享的托管解决方案。你将通过一些虚拟主机服务提供商购买域名。它既便宜又方便,但是您可能无法访问ini.php
文件。大多数网络主机都允许你通过一个所谓的.htaccess
文件进行一些配置,你不妨马上熟悉一下.htaccess
。幸运的是,更换upload_max_filesize
是一项非常简单的任务。
用 Komodo Edit 或你使用的任何编辑器创建一个新文件。将新文件另存为.htaccess
并保存在blog
文件夹中。注意,完整的文件名不应该是.htaccess.txt
或.htaccess.php
。文件名只能是.htaccess
。下面是.htaccess
文件的完整内容:
php_value upload_max_filesize 1M
保存后,您可以在浏览器中重新加载http://localhost/blog/test.php
。你应该看到local values for upload_max_filesize
现在是 1M(意思是 1MB)。Master Values
仍然是 128 米,但是对于你的博客文件夹和子文件夹,它现在改为 1 米。
Note
你可以在 http://htaccess-guide.com
了解更多关于.htaccess
的事情。使用.htaccess
来限制对文件夹的访问是很常见的。另一个常见的任务是动态重写 URL,以提供搜索引擎友好的 URL。
现在,测试您的图像管理器的上传表单将变得更加容易。找一个大于 1MB 的 JPEG 应该不难。试着通过你的表单上传这样一个文件,你会发现文件不会上传。您应该会看到一条错误消息,告诉您上传文件太大。现在你已经测试了你的系统,并确认它正常地失败了,你可以把upload_max_filesize
改回你认为适合你的博客的值。
检测其他错误
你知道$_FILES
超级全局天生支持七种不同的错误。您只是在测试其中一个问题:Upload
文件太大。
Note
您可以在 www.php.net/manual/en/features.file-upload.errors.php
了解更多错误代码及其含义。
我不打算详细讲述你在上传文件时可能遇到的其他类型的错误。但是我将向您展示如何提供一个默认的错误消息,以防其余六个错误中的任何一个发生。这和你想象的一样简单。您只需在您的readyToUpload()
方法中添加一个额外的else-if
代码块,如下所示:
//partial code for models/Uploader.class.php
//edit existing function
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
$canUpload = false;
} else if ( $this->errorCode === 1 ) {
$maxSize = ini_get( 'upload_max_filesize' );
$this->errorMessage = "Error: File is too big. ";
$this->errorMessage .= "Max file size is $maxSize";
$canUpload = false;
//new code: add code block for other errors
//the error codes have values 1 to 8
//we already check for code 1
//if error code is greater than one, some other error occurred
} else if ( $this->errorCode > 1 ) {
$this->errorMessage = "Something went wrong! ";
$this->errorMessage .= "Error code: $this->errorCode";
$canUpload = false;
//end of new code
//if error code is none of the above
//we can safely assume no error occurred
} else {
//indicate that we're ready to upload
$canUpload = true;
}
return $canUpload;
}
太棒了!您的代码现在检查两个常见问题,并为这些问题提供自定义错误信息。此时,您的系统甚至应该提供一个默认的错误消息,以防发生任何其他错误。您可以通过强制一个错误来测试是否捕捉到这样的错误。浏览浏览器至http://localhost/blog/admin.php?page=images
,点击上传按钮,无需选择要上传的文件。您的代码将尝试不上传任何内容,这将导致错误 4:没有文件上传。
如果您看到该错误消息,则表明您已经确认,当发生一个典型的文件上传错误时,您的系统会提供默认的错误消息。
进一步的改进
您应该能够进一步改进错误消息。您可以简单地检查剩余的错误代码,并为它们提供定制的错误消息。从几个方面来说,这对你来说都是一次极好的学习锻炼。
- 您必须了解其余错误代码的含义。
- 你必须学会如何触发这些错误。
- 您应该考虑如何向用户传达有意义的错误消息。
我希望你能花时间做这件事。您还可以考虑自己实现其他一些改进。你可以看看图片上传的一个不幸的特征。假设你上传了一张图片test.jpg
。现在试着上传一个不同的图片,名字也是test.jpg
。结果将是第一幅图像被下一幅图像覆盖。
如果您能在上传前检查名称冲突,这将是一个显著的改进。也许你的系统应该抛出一个异常,提示用户在上传之前重命名图像?或者您甚至可以更改上传表单,允许用户通过表单重命名图像?
另一个显著的改进是,如果用户试图上传一个不属于类型image/jpeg
的文件,就会抛出一个异常。您已经有了客户端验证,但是恶意用户可以很容易地绕过它。实现文件类型的服务器端验证怎么样?您需要您的代码将上传文件的 mimetype 与一个或多个可接受的 mime type 进行比较。PHP 可以在$_FILES
超全局中找到上传文件的 mimetype。
显示图像
您有一个允许上传图像的图像管理器,并且在条目编辑器中集成了 TinyMCE 富文本编辑器。博客管理员应该可以看到所有可用的图像,并在博客条目中使用其中的任何一个。
要使用 TinyMCE 在博客条目中嵌入图像,您必须知道图像文件的路径。是时候更新图像管理器了,所以它显示了所有上传图像的缩略图以及每个图像的路径。这样,博客管理员可以相对容易地将路径复制到给定的图像,并将其粘贴到 TinyMCE 中,以便在博客条目中使用图像。列出所有的图片将会非常非常类似于你在第四章中为图片库编写的代码。
您可以创建一个<dl>
元素来列出所有图像。你可以使用一个DirectoryIterator
对象来遍历img
文件夹并找到所有的 JPEG 图片。每个图像应该显示为一个<img>
元素。在图片的正下方,你应该显示图片的路径。如果图片能再删除就好了。所以,你最好提供一个删除每张图片的链接。更新views/admin/images-html.php
中的代码,如下所示:
<?php
//complete source code for views/admin/images-html.php
if ( isset( $uploadMessage ) === false ){
$uploadMessage = "Upload a new image";
}
//new code starts here
//declare a variable to hold HTML for all your images
$imagesAsHTML = "<h1>Images</h1>";
$imagesAsHTML .= "<dl id='images'>";
$folder = "img";
$filesInFolder = new DirectoryIterator( $folder);
//loop through all files in img folder
while ( $filesInFolder->valid() ) {
$file = $filesInFolder->current();
$filename = $file->getFilename();
$src = "$folder/$filename";
$fileInfo = new Finfo( FILEINFO_MIME_TYPE );
$mimeType = $fileInfo->file( $src );
//if file is a jpg...
if ( $mimeType === 'image/jpeg' ) {
//display image and its src
$href = "admin.php?page=images&delete-image=$src";
$imagesAsHTML .= "<dt><img src='$src' /></dt>
</dd>Source: $src <a href='$href'>delete</a></dd>";
}
$filesInFolder->next();
}
$imagesAsHTML .= "</dl>";
//notice that $imagesAsHTML is added at the end of the returned HTML
return "
<form method='post' action='admin.php?page=images' enctype='multipart/form-data'>
<p>$uploadMessage</p>
<input type='file' name='image-data' accept='image/jpeg' />
<input type='submit' name='new-image' value='upload' />
</form>
<div>
$imagesAsHTML
</div>";
//end of changes
查看一下http://localhost/blog/admin.php?page=images
,看看img
文件夹中的所有图像是如何显示的,每个图像都有一个路径和一个删除链接。
删除图像
此时,图像管理器会以原始大小列出所有图像。它不是很漂亮,但它足以让你使用图像管理器浏览图像。你可以很容易地写一些 CSS 来提高美感。你甚至可以在第五章中重用图片库中的一些 JavaScript 想法。当你读完这一章时,请考虑这样做。在你开始研究美学之前,我还想让你做一些功能上的增强。首先:删除图像。
单击链接删除文件是一种用户交互。因此,响应点击的代码属于控制器。有人可能会说,实际删除文件的代码属于模型脚本。但这只是一行代码,所以我将全部保存在控制器中,在controllers/admin/images.php
中:
<?php
//complete code for controllers/admin/images.php
include_once "models/Uploader.class.php";
$imageSubmitted = isset( $_POST['new-image'] );
if ( $imageSubmitted ) {
$uploader = new Uploader( 'image-data' );
$uploader->saveIn( "img" );
try{
$uploader->save();
$uploadMessage = "file uploaded!";
} catch ( Exception $exception ) {
$uploadMessage = $exception->getMessage();
}
}
//new code starts here: if a delete link was clicked...
$deleteImage = isset( $_GET['delete-image'] );
if ( $deleteImage ) {
//grab the src of the image to delete
$whichImage = $_GET['delete-image'];
unlink($whichImage);
}
//end of new code
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
现在,您应该能够使用图像管理器删除图像了。先尝试一下,然后你可以继续读下去,了解它是如何发生的。
删除文件的原生 PHP 函数是unlink
。要删除一个文件,需要将文件的路径和名称作为参数传递给unlink()
。理解 PHP 如何找到图像的路径和名称是很重要的。如果查看代码,可以看到 PHP 从一个 URL 变量delete-image
中获取了要删除的图像源,这个变量是用$_GET
方法检索的。下一个要思考的问题是,图像的来源是如何被编码成 URL 变量的?
您可以在浏览器中查看图像管理器的 HTML 源代码。您会看到每个删除链接都编码了一个保存图像源的 URL 变量。如果您仔细观察一下href
属性,就可以看到它。它们大概如下所示:
<a href='``admin.php?page=images&delete-image=img/coffee.jpg
delete
</a>
看看一个 URL 变量page
如何被设置为images
,另一个 URL 变量delete-image
如何被设置为img/coffee.jpg
。因此,点击这个删除链接将加载images
控制器并删除在img/coffee.jpg
中找到的图像。
要考虑的最后一个问题是,在 PHP 代码的什么地方创建了删除链接?你有一个选择:要么你思考这个问题并浏览代码来找到你自己的答案,要么你继续阅读来看我的答案。
删除链接在views/admin/images-html.php
中创建。在while
循环中,您可以找到创建 HTML 的 PHP,用于显示一个图像、它的源和一个删除链接:
//partial code for views/admin/images-html.php
//make no changes to any code
$file = $filesInFolder->current();
$filename = $file->getFilename();
//$src holds the relative path to the file
$src = "$folder/$filename";
$fileInfo = new Finfo( FILEINFO_MIME_TYPE );
$mimeType = $fileInfo->file( $src );
if ( $mimeType === 'image/jpeg' ) {
//href for delete link created below
$href = "admin.php?page=images``&
$imagesAsHTML .= "<dt><img src='$src' /></dt>
</dd>Source: $src
<a href='``$href
</dd>";
给你。变量$src
保存了一个文件 PHP 的相对路径,该路径是通过用一个DirectoryIterator
对象遍历一个文件夹找到的。如果找到的文件是 JPEG 图像,它将显示为 HTML。注意如何使用$src
为删除链接创建一个href
属性。
在博客条目中使用图像
列出了所有的图片后,在你的博客条目中嵌入图片应该是一件简单的事情。在http://localhost/blog/admin.php?page=images
查看您的所有图像,找到您想要使用的图像。请注意每个图像的源是如何在浏览器中显示的。选择并复制一些你想在博客文章中使用的图片的来源。现在,将现有的博客条目加载到您的博客编辑器中。请注意 TinyMCE 是如何提供插入图像的按钮的。点击图像按钮,弹出 TinyMCE 的图像对话框,如图 11-2 所示。通过将图像src
粘贴到源字段来嵌入图像。
图 11-2。
The image dialog pop-up in TinyMCE
一旦你在博客条目中嵌入了图像,你应该保存它,然后在你的浏览器中加载http://localhost/blog/index.php
,以检查图像是否确实显示在博客条目中。
提高编辑器的可用性
现在你的博客有了一个了不起的管理模块。通过管理模块,您可以创建、更新和删除博客条目。在我的指导下不会添加更多的功能。
条目编辑器有一个可用性缺陷,我想指出并改进它。将一个现有的条目加载到条目编辑器中,稍微更改条目,然后保存更改。这将在您的条目编辑器中重新加载已保存的更改,您应该会看到一条提示条目已保存的消息,清楚地告诉您在编辑器中看到的内容已保存在数据库中。
但是,如果您将条目更改得更多一点,消息仍然会坚持说条目已保存,即使您我都知道这是误导。在您单击“保存”按钮之前,不会保存更改。这是糟糕的可用性!来自系统的误导性反馈可能比没有反馈更糟糕。你如何补救这个问题?这是一个调用 PHP 或 JavaScript 的任务吗?在你继续读下去之前,花一分钟思考一下。
这是一个绝对需要 JavaScript 的任务!PHP 是一种服务器端脚本语言。每次联系服务器上的 PHP 脚本时,它都会运行一次。如果请求寻找 PHP 资源,PHP 将为每个 HTTP 请求运行一次。您的条目编辑器只是一个 HTML 表单,带有一个用 JavaScript 和 CSS 增强的<textarea>
元素。PHP 只会在你提交表单时运行。在<textarea>
中输入几个字符不会提交表单。
JavaScript 是一种客户端脚本语言。它在浏览器中运行得非常好。您已经看到了当某些浏览器事件发生时如何调用 JavaScript 函数。到目前为止,您已经了解了当 HTML DOM 内容被加载到浏览器中、当用户单击以及当表单被提交时,如何调用 JavaScript 函数。每当用户按下键盘上的某个键时,JavaScript 也会做出响应。您可以在这里使用该事件。如果用户在<input name='title'>
处于焦点时按下一个键,你就知道标题被更改了。如果标题已更改,但表单尚未提交,您知道更改尚未保存在数据库中。打开您的editor.js
JavaScript 文件,并声明一个用于更新更新消息的新函数。在这个过程中,添加一个事件侦听器,以便在用户每次更改标题时调用这个新函数,如下所示:
//partial code for js/editor.js
//new code: declare a new function
function updateEditorMessage () {
console.log( "editor changes not saved yet!" );
}
//edit existing function
function init(){
var editorForm = document.querySelector("form#editor");
var title = document.querySelector("input[name='title']");
title.required = false;
//code changes start here
title.addEventListener("keyup", updateEditorMessage, false);
//end of changes
editorForm.addEventListener("submit", checkTitle, false);
}
updateEditorMessage()
函数还没有做任何有意义的事情。它只是向控制台输出一条消息。但是你可以用它来测试你的进步。打开您的浏览器及其 JavaScript 控制台。如果你用的是 Chrome,你可以用 Cmd+Alt+J 打开控制台,如果你用的是 Mac,或者 Ctrl+Alt+J,如果你用的是 Windows。
一旦浏览器的 JavaScript 控制台打开,您就可以将浏览器导航到http://localhost/blog/admin.php?page=entries
。单击一个条目,将其加载到您的博客条目编辑器中。编辑条目并保存更改。注意,通常的条目是由 PHP 创建的保存消息。现在,对条目标题做一点修改,注意控制台中的输出:JavaScript 注意到发生了一些事情。还要注意,条目已保存的消息现在变得具有误导性(见图 11-3 ):条目中最近的更改没有保存!
Note
如果你在控制台中看不到预期的输出,可能是因为 Google Chrome 保存了你之前 JavaScript 文件的缓存版本。换句话说,谷歌 Chrome 还没有注意到你已经更新了你的 JavaScript。你只需打开“清除浏览数据”对话框(Mac: Cmd+Shift+Delete,Windows: Ctrl+Shift+Delete),清除“缓存的图像和文件”
图 11-3。
A message in Chrome’s console, and a misleading editor message
很高兴看到 JavaScript 注意到编辑器中发生了一些事情。JavaScript 知道更改还没有保存在数据库中。JavaScript 知道编辑器消息条目已保存不再有效,但实际上是误导。现在 JavaScript 已经检测到条目编辑器的标题字段发生了变化,是时候为用户改变反馈了。在js/editor.js
中又用了几行 JavaScript 代码:
//partial code for js/editor.js
//edit existing function
function updateEditorMessage() {
var p = document.querySelector("#editor-message");
p.innerHTML = "Changes not saved!";
}
就是这样!每当<title>
中有未保存的更改时,用户都会收到通知。每当<textarea>
发生变化时,添加一个事件监听器来调用updateEditorMessage()
是非常简单的。您可能认为可以在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']");
title.required = false;
//changes start here
//sadly, this code will not solve the problem...
var textarea = document.querySelector("form textarea");
textarea.addEventListener("keyup", updateEditorMessage, false);
//end of changes
title.addEventListener("keyup", updateEditorMessage, false);
editorForm.addEventListener("submit", checkTitle, false);
}
您可能会惊讶地发现,每当<textarea>
发生变化时,前面的代码并不更新编辑器消息。问题是您的 JavaScript 代码与 TinyMCE JavaScript 冲突。看起来你运气不好。幸运的是,TinyMCE 是可以配置的。TinyMCE 开发人员甚至努力记录配置选项。我在 TinyMCE 文档中搜索了一下,发现只要 TinyMCE 编辑器发生变化,TinyMCE JavaScript 就可以调用您的updateEditorMessage()
。在 Komodo Edit 中打开views/admin/editor-html.php
,稍微修改一下代码,如下:
//partial code for views/admin/editor-html.php
<script type='text/javascript'>
//change the existing call to tinymce.init
//add the code to call your updateEditorMessage function...
//...whenever the tinymce editor changes
tinymce.init ({
selector: 'textarea',
plugins: 'image',
setup: function (editor) {
editor.on ('change', function (e) {
updateEditorMessage();
});
}
});
</script>
每当 TinyMCE 编辑器以任何方式改变时,该代码将调用函数updateEditorMessage()
。那正是你想要的。
Note
了解如何在 www.tinymce.com/wiki.php/Configuration
配置 TinyMCE。
期末考试的时间到了。将一个现有条目加载到条目编辑器中,稍加修改,然后保存。现在,您应该看到一条编辑器消息,说明条目已保存。对条目再做一点修改,看看编辑器消息如何立即变成 Changes not saved!这是您的 JavaScript 函数updateEditorMessage()
的工作,该函数通过 TinyMCE change
事件调用,由 TinyMCE 编辑器中的任何更改触发。
需要记住的一点是,TinyMCE 依赖于 JavaScript。如果不启用 JavaScript,条目编辑器不会给人留下深刻印象。任何博客管理员都必须承认,他们需要一个支持 JavaScript 的现代浏览器。
下一章将向您展示如何限制对管理模块的访问。您将创建一个登录表单,以便只允许拥有正确用户名和密码的用户使用管理模块。
摘要
本章对管理模块进行了一些重大改进。在这个过程中,您还有机会了解 PHP 文件上传以及如何用 PHP 删除文件。
您已经仔细研究了与定制 PHP 异常一起使用的try-catch
语句。您已经看到了如何使用try-catch
,以便您的 PHP 代码可以优雅地失败。您已经使用异常向用户提供了有意义的错误消息。您应该知道一些开发人员会反对这种异常的使用。一些开发人员会说,你能预测的任何错误都不是例外。这些情况不应使用例外。最终决定权在你:至少现在,你有选择权。
我希望你和 TinyMCE 的合作给你带来了开胃菜。在网上可以找到许多可靠的、记录良好的 JavaScript,将这样的 JavaScript 集成到您的项目中可以极大地提高您的工作质量。另一方面,你应该小心处理你不完全理解的库和框架。如果你向客户交付一个项目,然后发现系统中有一个 bug,如果你不理解使用的代码,你可能很难修复这个 bug。和例外的情况一样,最终都要做出明智的决定。
十二、密码保护
在你可以称你的博客为“网络就绪”之前,你需要做的最后一件事是对没有被授权的用户隐藏管理模块。在本章中,您将学习如何构建一个系统,让您创建新的管理员,并要求管理员在创建、编辑和删除博客条目之前使用密码登录。创建此系统需要您执行以下任务:
- 在
simple_blog
数据库中创建一个管理表 - 使用单向密码加密
- 创建用于创建新管理员的 HTML 表单
- 在管理表中插入一个或多个管理员
- 为管理员创建登录表单
- 对未授权用户隐藏管理模块
- 使用会话跨多个 HTTP 请求保持登录状态
在数据库中创建 admin_table
为您的站点启用授权管理员要求您创建一个表来存储他们的数据。我创建了一个管理表来存储关于管理员的以下信息:
admin_id
:识别一个管理员的唯一编号email
:管理员的电子邮件地址password
:管理员的密码
您将使用所谓的 MD5 算法将密码加密成 32 个字符的字符串。因此,密码列可以使用VARCHAR
数据类型,并将输入限制为 32 个字符。
要创建管理表,在浏览器中导航到http://localhost/phpmyadmin
,选择您的simple_blog
数据库并打开 SQL 选项卡。输入以下命令创建您的表:
create table admin(
admin_id INT NOT NULL AUTO_INCREMENT,
email TEXT,
password VARCHAR(32),
primary key (admin_id)
)
用 MD5 加密密码
一旦您开始向 admin 表中插入新用户,您将看到如何用 MD5 算法加密字符串。MD5 算法提供所谓的单向加密。
加密密码是一种很好的做法,因为加密的密码更难被窃取。让我们假设你的密码是 test。通过 MD5 运行,test 变成 098 f 6 BCD 4621d 373 cade 4 e 832627 B4 f 6。无论你的密码有多长或多短,一旦通过 MD5,它就会变成 32 个字符的字符串。
即使您的数据库遭受严重攻击,并且所有用户名和密码都暴露了,攻击者仍然无法滥用用户名和密码。
单向加密
密码通常通过所谓的单向加密来保护。如果用单向加密算法加密密码,实际上应该是无法解密的。因此,如果您有一个加密的密码,如 098 F6 BCD 4621d 373 cade 4 e 832627 B4 f 6,那么实际上不可能发现未加密的密码是 test。这意味着即使您的数据库遭到破坏,密码仍然受到保护。
任何可以恢复丢失密码的系统本质上都是不安全的。如果您的原始密码可以恢复,系统必须以某种方式记住您的密码。如果您的密码被系统记住,系统管理员很可能会看到您的密码。如果你的密码可以被看到,那它就是易受攻击的。一个安全的系统只会存储加密的密码。如果您忘记了密码,安全系统会为您提供重新设置密码的机会。使用单向加密,可以对系统管理员隐藏密码等敏感数据。
在您的博客系统中,这意味着管理员密码受到保护,不会被您或其他博客管理员知道。如果用户丢失了密码,您就不能再将密码发送给她。您可以给她一个重置密码的机会,但密码是完全安全的——就像它应该的那样。
足够的安全性
假设即使是最安全的系统也可能被黑客攻击。问题是攻击者需要花费多少时间和精力。您的任务是提供足够的安全性来阻止攻击者访问您的内容。如果你的内容非常有价值,你需要非常严格的安全措施。大多数个人博客对 IT 罪犯来说可能不是特别有吸引力的目标。
MD5 是一种单向加密算法。它经常在 PHP 中使用,因为它在 PHP 应用中非常容易使用。MD5 本身不足以创建最佳的密码安全性。尽管 MD5 有很多优点,但它已经被攻破了。但对我们来说,这就足够了。你们已经对 SQL 注入的袭击采取了措施。很快,你也将拥有加密的密码,这样你就有可能阻止任何来自你博客系统的攻击者。
Note
还有另一种常见的攻击类型,称为跨站脚本(XSS)攻击。你可以考虑做一些互联网研究,防止 XSS 对 PHP 站点的攻击。
您应该知道,关于构建安全的 PHP 应用,还有很多东西需要学习。如果您开发一个保存有价值数据的系统,您就不能指望 MD5 能阻止攻击者。您可以考虑参考 Chris Snyder 的 Pro PHP Security (Apress,2010)来了解更多关于这个主题的信息。
在数据库中添加管理员
您有一个数据库表来保存管理员凭据;您已经准备好开始创建博客管理员了。第一步是创建一个允许您输入电子邮件地址和相应密码的表单。完成后,您必须将信息存储在数据库中以备后用。
构建 HTML 表单
正如您已经了解到的,最好创建向用户提供反馈的表单。您也可以马上为用户反馈准备好表单。在views/admin/new-admin-form-html.php
中创建新文件。我的表单看起来是这样的:
<?php
//complete code for views/admin/new-admin-form-html.php
if( isset($adminFormMessage) === false ) {
$adminFormMessage = "";
}
return "<form method='post' action='admin.php?page=users'>
<fieldset>
<legend>Create new admin user</legend>
<label>e-mail</label>
<input type='text' name='email' required/>
<label>password</label>
<input type='password' name='password' required/>
<input type='submit' value='create user' name='new-admin'/>
</fieldset>
<p id='admin-form-message'>$adminFormMessage</p>
</form>
";
要显示表单,您需要一个控制器来加载它并将其返回给admin.php
。您可以创建一个新的导航项目和一个相应的控制器。首先使用附加链接更新您的导航,如下所示:
<?php
//complete code for views/admin/admin-navigation.php
//new code: notice item added for a 'Create admin user' page
return "
<nav id='admin-navigation'>
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
<a href='admin.php?page=images'>Image manager</a>
<a href='admin.php?page=users'>Create admin user</a>
</nav>
";
创建了表单的视图和新的导航项后,下一步可以创建一个控制器来加载视图并显示它。在前面的例子中,我为请求名为 users 的新页面的新链接创建了一个href
。所以,我的系统期望一个名为controllers/admin/users.php
的控制器。创建这样一个文件,并编写以下代码:
<?php
//complete code for controllers/admin/users.php
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;
保存您的工作并在浏览器中加载http://localhost/blog/admin.php?page=users
。您应该会看到一个无样式但有效的 HTML 表单。您还不能真正使用它:它不能向数据库表中插入新管理员。
在数据库中保存新管理员
向数据库表中插入新行是模型脚本的工作。您已经多次很好地利用了表数据网关设计模式。您甚至准备了一个基本的Table
类,您可以扩展它来为 admin 表创建一个表数据网关。
此时,您不需要做太多事情:您希望避免名称冲突,因此在创建新的 admin 用户之前,代码应该检查该电子邮件是否已经在表中使用。如果电子邮件没有被使用,模型脚本应该在 admin 表中插入一个新条目。创建新文件models/Admin_Table.class.php
,并编写新的类定义,如下所示:
<?php
//complete code for models/Admin_Table.class.php
//include parent class' definition
include_once "models/Table.class.php";
class Admin_Table extends Table {
public function create ( $email, $password ) {
//check if e-mail is available
$this->checkEmail( $email );
//encrypt password with MD5
$sql = "INSERT INTO admin ( email, password )
VALUES( ?, MD5(?) )";
$data= array( $email, $password );
$this->makeStatement( $sql, $data );
}
private function checkEmail ($email) {
$sql = "SELECT email FROM admin WHERE email = ?";
$data = array( $email );
$this->makeStatement( $sql, $data );
$statement = $this->makeStatement( $sql, $data );
//if a user with that e-mail is found in database
if ( $statement->rowCount() === 1 ) {
//throw an exception > do NOT create new admin user
$e = new Exception("Error: '$email' already used!");
throw $e;
}
}
}
前面的代码正在等待控制器调用。create()
方法为新电子邮件和相关密码提供两个参数。私有方法checkEmail()
用于检查所提供的电子邮件是否已经存在于数据库中。如果是的话,一个Exception
对象被创建,并带有一条相关的消息,创建的异常被抛出。这意味着您可以在控制器中使用try-catch
语句。您将很快编写代码,尝试创建一个新的管理员用户。如果操作失败,您的代码将通过捕捉异常并向用户提供相关反馈来正常失败。
看看create()
方法中的$sql
变量。看看接收到的密码在插入数据库时是如何用 MD5 加密的?这是可能的,因为 SQL 语言有一个内置的 MD5 函数。让我们回到controllers/admin/users.php
中的控制器脚本,写一些代码,这样来自表单的输入将用于在数据库中插入新的管理员用户:
<?php
//complete code for controllers/admin/users.php
//new code starts here
include_once "models/Admin_Table.class.php";
//is form submitted?
$createNewAdmin = isset( $_POST['new-admin'] );
//if it is...
if( $createNewAdmin ) {
//grab form input
$newEmail = $_POST['email'];
$newPassword = $_POST['password'];
$adminTable = new Admin_Table($db);
try {
//try to create a new admin user
$adminTable->create( $newEmail, $newPassword );
//tell user how it went
$adminFormMessage = "New user created for $newEmail!";
} catch ( Exception $e ) {
//if operation failed, tell user what went wrong
$adminFormMessage = $e->getMessage();
}
}
//end of new code
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;
保存您的代码,并在浏览器中运行http://localhost/blog/admin.php?page=users
进行测试。尝试创建一个管理员用户。您应该会在表单中看到一条确认消息。检查http://localhost/phpmyadmin
以查看 admin 用户是否确实被插入到 admin 数据库表中。
创建新的管理员用户后,您可以尝试使用相同的电子邮件地址再创建一个管理员用户。这是不允许的,您应该会在表单中收到一条错误消息。更具体地说,您应该看到从Admin_Table
类的checkEmail()
方法抛出的Exception
的消息。
规划登录
本章的其余部分集中在限制对管理模块的访问。应该只允许经过身份验证的用户创建、编辑和删除博客条目。
您可以继续使用 MVC 思想来组织代码。您需要两个视图:一个登录表单和一个注销表单。您需要一个控制器来处理从这两个视图接收到的用户交互,还需要一个模型来实际执行登录和注销。该模型还应该记住一个状态:它应该记住用户是否登录。
创建登录表单
从登录表单开始。您最终应该得到一个系统,在这个系统中,用户必须提供有效的电子邮件和匹配的密码,才能被允许访问博客管理模块。因此,您需要一个包含电子邮件和密码字段的表单。在views/admin/login-form-html.php
中创建一个新文件:
<?php
//complete code for views/admin/login-form-html.php
return " <form method='post' action='admin.php'>
<p>Login to access restricted area</p>
<label>e-mail</label><input type='email' name='email' required />
<label>password</label>
<input type='password' name='password' required />
<input type='submit' value='login' name='log-in' />
</form>";
您还应该创建一个简单的控制器。此时,它应该只加载视图并输出它。在controllers/admin/login.php
中创建一个新文件:
<?php
//complete code for controllers/admin/login.php
$view = include_once "views/admin/login-form-html.php";
return $view;
管理导航没有为登录提供菜单项,也不应该提供。您希望登录表单显示为admin.php
的默认视图。只有当用户通过身份验证并登录后,才允许用户查看博客管理模块。
您可以创建一个类来表示管理员用户。这个类应该记住当前的访问者是否已经登录。最初,您可以安全地假设用户没有登录。用户应该可以登录和注销。您可以用属性表示状态(即用户是否登录)。您将需要登录和注销的方法。每个方法都应该操作由属性表示的登录状态。在models/Admin_User.class.php
中创建一个新文件,如下:
<?php
//complete code for models/Admin_User.class.php
class Admin_User {
private $loggedIn = false;
public function isLoggedIn(){
return $this->loggedIn;
}
public function login () {
$this->loggedIn = true;
}
public function logout () {
$this->loggedIn = false;
}
}
对未授权用户隐藏控件
非常重要的是,登录实际上可以对未授权用户隐藏系统的一部分。你可能会说这是登录的全部意义。到目前为止,访问admin.php
的任何人都可以很容易地获得管理模块。你需要在这里做一些改变。
Create an Admin_User
object to remember login state. If a visitor is not logged in, show only the login form. If a user is logged in, show the admin navigation and the administration module.
为此,您必须对admin.php
进行一些修改。一些变化将涉及注释掉或删除现有代码。但是您还必须添加一些新的代码行,如下所示:
<?php
//complete code for blog/admin.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";
$pageData->addCSS("css/blog.css");
$pageData->addScript("js/editor.js");
//code changes start here: comment out navigation
//$pageData->content = include_once "views/admin/admin-navigation.php";
$dbInfo = "mysql:host=localhost;dbname=techreview_blog";
$dbUser = "root";
$dbPassword = "";
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//code changes here: comment out some of the existing code in admin.php
//$navigationIsClicked = isset( $_GET['page'] );
//if ($navigationIsClicked ) {
// $contrl = $_GET['page'];
//} else {
// $contrl = "entries";
//}
//new code added below
include_once "models/Admin_User.class.php";
$admin = new Admin_User();
//load the login controller, which will show the login form
$pageData->content = include_once "controllers/admin/login.php";
//add a new if statement
//show admin module only if user is logged in
if( $admin->isLoggedIn() ) {
$pageData->content .=include "views/admin/admin-navigation.php";
$navigationIsClicked = isset( $_GET['page'] );
if ($navigationIsClicked ) {
$controller = $_GET['page'];
} else {
$controller = "entries";
}
$pathToController = "controllers/admin/$controller.php";
$pageData->content .=include_once $pathToController;
} //end if-statement
//end of new code
$page = include_once "views/page.php";
echo $page;
有了这段代码,您就可以测试您的登录了。如果您将浏览器导航到http://localhost/blog/admin.php
,除了登录表单,您应该什么也看不到。这是理所应当的,因为$admin
对象的loggedIn
属性默认为false
,当用户未登录时,不允许用户进入管理区域。
登录用户
是时候允许用户登录了。此时,我不会真正对用户进行身份验证。我不会编写代码来检查所提供的电子邮件和密码是否有效。在本章的后面,我将向你展示一种方法。现在,您只需登录任何提交登录表单的人。处理这样的用户交互是登录控制器的工作。在controllers/admin/login.php
中更新您的代码:
<?php
//complete code for controllers/admin/login.php
//new code here
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
$admin->login();
}
//end of new code
$view = include_once "views/admin/login-form-html.php";
return $view;
变化很小,现在对你来说应该很容易理解。尝试将浏览器指向http://localhost/blog/admin.php
并登录。你可以使用任何凭证!
它应该像魔咒一样工作,你应该看到管理导航和所有博客条目的可点击列表。但是等等!单击其中一个条目,将其加载到条目编辑器中。会发生什么?哦不!您看不到条目编辑器。相反,您发现自己再次查看登录表单。很明显,你立刻被注销了?为什么呢?!?
HTTP 是无状态的
超文本传输协议是互联网上许多数据通信的基础。它是无状态的,这意味着它将每个新请求视为一个单独的事务。实际上,这意味着所有 PHP 变量和对象都是随着每个新请求从头开始创建的。
这对你有一定的影响。当您提交登录表单时,您实际上发出了一个 HTTP 请求。PHP 将会运行,$admin
对象将会记住您已经登录。接下来,单击一个条目。这将产生一个新的 HTTP 请求,从而创建一个新的$admin
对象。PHP 不会记得您刚刚登录,因为新的 HTTP 请求被视为一个独立的事务。之前的 HTTP 请求发生了什么完全被遗忘了。
重新访问超级全局变量:$_SESSION
这种无状态 HTTP 对您的登录来说是个坏消息。无论你登录多少次,PHP 都会随着每个新的请求而忘记它。这是非常不切实际的,当然,有一个解决方案:跨请求保持状态。您需要一种方法来强制 PHP 记住一个给定的用户已经登录。你需要一个 PHP 会话。
当 PHP 会话启动时,访问用户的浏览器将被分配一个唯一的标识号:会话 id。服务器将在服务器端创建一个小的临时文件。您需要应用跨请求记住的任何信息都应该存储在这个文件中。PHP 将为您处理这个临时文件。PHP 会话的默认生存期为 20 分钟,持续时间可以在php.ini
文件中配置。
Note
www.php.net/manual/en/intro.session.php
阅读更多。
PHP 提供了一个超级全局变量来简化会话处理。你已经见过一些超级大人物:$_GET
、$_POST
和$_FILES
。现在,你该见见$_SESSION
了。
通过会话保持状态
若要使用会话,您的代码必须启动一个会话。这样,您就可以创建一个会话变量,它的值是有状态的,这意味着它的值可以跨 HTTP 请求保持不变。
您已经有了一个Admin_User
类,它使用一个普通属性来记住登录状态。您可以使用会话变量来代替。这是如何做到的:
<?php
//complete code for models/Admin_User.class.php
class Admin_User {
//declare a new method, a constructor
public function __construct(){
//start a session
session_start();
}
//edit existing method
public function isLoggedIn(){
$sessionIsSet = isset( $_SESSION['logged_in'] );
if ( $sessionIsSet ) {
$out = $_SESSION['logged_in'];
} else {
$out = false;
}
return $out;
}
//edit existing method
public function login () {
//set session variable ['logged_in'] to true
$_SESSION['logged_in'] = true;
}
//edit existing method
public function logout () {
//set session variable ['logged_in'] to false
$_SESSION['logged_in'] = false;
}
}
保存代码更改并在浏览器中加载http://localhost/blog/admin.php
。您仍然可以使用任何凭证登录,但是这一次,PHP 将记住您已经登录。因此,点击一个博客条目实际上会将点击的条目加载到条目编辑器中。这是一个好消息:您可以作为管理员登录,一旦登录,您就可以使用管理模块。但是一切都还不完美。任何人都可以登录,因为电子邮件和密码不会真正与合法管理员的数据库记录进行比较。此外,已登录的用户无法注销。让我们先处理最简单的任务。
注销用户
通常为登录用户提供注销选项。这也是一个好主意:您不希望管理员在没有注销选项的情况下保持登录状态。如果管理员不得不离开电脑去冲一杯新咖啡,该怎么办?您不会想让管理模块暴露在外。让我们为注销创建一个新视图。在views/admin/logout-form-html.php
中创建一个新文件,如下:
<?php
//complete code for views/admin/logout-form-html.php
return "
<form method='post' action='admin.php'>
<label>logged in as administrator</label>
<input type='submit' value='log out' name='logout' />
</form>";
每当用户登录时,都应该显示该视图。但是简单地显示一个注销表单实际上不会注销用户。如果用户单击 Logout 按钮,应该会运行一个脚本来注销用户。这些是管制员的任务。您必须为controllers/admin/login.php
中的登录控制器编写一些代码:
<?php
//complete code for controllers/admin/login.php
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
$admin->login();
}
//new code below
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
$admin->logout();
}
if ( $admin->isLoggedIn() ) {
$view = include_once "views/admin/logout-form-html.php";
} else {
$view = include_once "views/admin/login-form-html.php";
}
//comment out the former include statement
//$view = include_once "views/admin/login-form-html.php";
//end of code changes
return $view;
你进步很大!你现在应该可以登录和注销了,PHP 会通过一个会话变量记住你的当前状态。任何人都可以使用任何凭证登录,这有点问题。该代码不检查提供的用户名和密码是否正确。
仅允许授权用户
登录即将完成。您只需检查提供的电子邮件和密码是否与数据库中的一条记录完全匹配。这些信息可以在 admin 表中找到,并且您已经有了一个名为Admin_Table
的表数据网关。您可以创建一个新方法来检查提交的凭据是否有效。
//partial code for models/admin/Admin_Table.class.php
//declare new method in Admin_Table class
public function checkCredentials ( $email, $password ){
$sql = "SELECT email FROM admin
WHERE email = ? AND password = MD5(?)";
$data = array($email, $password);
$statement = $this->makeStatement( $sql, $data );
if ( $statement->rowCount() === 1 ) {
$out = true;
} else {
$loginProblem = new Exception( "login failed!" );
throw $loginProblem;
}
return $out;
}
看看收到的密码是怎么用 MD5 加密的?在数据库中,您有 MD5 加密的密码。要将数据库中的密码与从登录表单中收到的密码进行比较,您的代码必须对收到的密码进行加密。如果将加密的密码与未加密的密码进行比较,用户将无法登录,因为 test 与 098 F6 BCD 4621d 373 cade 4 e 832627 B4 f 6 不同。
请注意,如果提交的电子邮件和密码与 admin 表中的一行数据不完全匹配,该方法将如何创建一个新的Exception
对象并抛出它。声明了这个方法后,您就可以在用户试图登录时从登录控制器调用它了。
<?php
//complete code for controllers/admin/login.php
//new code: include the new class definition
include_once "models/Admin_Table.class.php";
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
//code changes start here: comment out the existing login call
//$admin->login();
//grab submitted credentials
$email = $_POST['email'];
$password = $_POST['password'];
//create an object for communicating with the database table
$adminTable = new Admin_Table( $db );
try {
//try to login user
$adminTable->checkCredentials( $email, $password );
$admin->login();
} catch ( Exception $e ) {
//login failed
}
//end of code changes
}
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
$admin->logout();
}
if ( $admin->isLoggedIn() ) {
$view = include_once "views/admin/logout-form-html.php";
} else {
$view = include_once "views/admin/login-form-html.php";
}
return $view;
这在安全性方面是一个巨大的进步。保存前面的代码后,您应该只能使用有效的用户凭据登录。但是不要相信我的话。尝试使用错误的凭据登录;应该是不可能的。
练习
您应该考虑一些与登录相关的附加内容。你已经在前面的章节中尝试过了,所以这些补充对你来说应该是很好的学习练习。
您的登录表单使用了必需的属性。正如你在第十章中了解到的,不同的浏览器对所需属性的处理是不同的。您可以使用 JavaScript 来提供跨现代浏览器的更一致的行为。这将提高可用性,也给你一个机会练习一点 JavaScript。
登录表单此时会自动失败。当用户登录失败时,抛出一个Exception
对象。异常甚至会被捕获,但是异常消息不会作为反馈显示给用户。也许您可以获取异常消息并将其显示在登录表单中。这很像在本章开始时创建一个$adminFormMessage
。
一旦实现了基本的用户反馈,就可以为登录表单尝试一些更高级的东西。当用户登录失败时,检查数据库中是否存在该电子邮件。如果有,让用户知道。密码错误,请重试。如果电子邮件不存在,告诉用户提供的电子邮件与系统中的任何记录都不匹配,请重试。
摘要
这是一个简短的章节。有了用于隐私的单向加密和用于有状态内存的会话,您已经设法有效地限制了对您的博客的管理模块的访问。我希望你会同意实现一个登录系统是非常有益的。我特别喜欢为您提供额外的机会来创建与用户交流的表单。
你的博客已经可以上网了。下一章将带你完成将你的项目上传到在线主机的过程,这样你的博客就可以在互联网上发布了。
十三、公开你的博客
你已经开发了一个完整的、数据库驱动的博客,甚至还有一个管理模块。但是它只能在你的本地计算机上运行。博客实际上是为读者写作。如果你的博客只在你的本地电脑上运行,那么它的受众肯定是有限的。本章将向您展示将您的博客从本地开发环境迁移到在线 web 主机所需的所有知识。
将你的博客放在互联网上的过程包括几个步骤。
You’ll need access to a web hosting service. It should offer an A-pache web server, MySQL, and PHP. You’ll need an FTP program, so that you can upload your files from your local computer to your web host. You’ll need to export your local database, so that you can import it into your web host’s MySQL database.
Web 主机要求
你需要一个虚拟主机来存放你的在线博客。它应该支持你用过的技术:PHP 5.4 和 MySQL 5.x .你的 web 主机也应该支持 FTP,这将允许你把你的 PHP 文件上传到 web 主机。
您已经使用 phpMyAdmin 访问了 MySQL 数据库。还有其他程序可以提供对 MySQL 的访问。如果这是您的第一个 PHP/MySQL 项目,您可能应该寻找一个提供 phpMyAdmin 的 web 主机。但是要明白 phpMyAdmin 并不是一个固定的需求。我推荐它,因为它是你已经用过的程序。
搜索“虚拟主机”,寻找适合你的解决方案。有免费的解决方案、便宜的解决方案和昂贵的解决方案。我假设这是你的第一个 PHP/MySQL 项目,所以我建议你寻找一个提供实时聊天支持的网络主机。一路上你可能会遇到问题,和一个有能力的支持人员实时聊天可以解决很多小问题。
导出和导入数据库
一旦确定了托管解决方案,就可以开始将数据库从本地开发环境迁移到新的 web 主机上。移动数据库包括创建一个 SQL 文件,该文件包含所有用于创建和填充表的 SQL 语句。幸运的是,这很容易做到。
首先打开 XAMPP 控制面板,确保 Apache 和 MySQL 都在运行。将浏览器指向http://localhost/phpmyadmin
并选择您的simple_blog
数据库。现在,点击顶部导航中的导出选项卡(参见图 13-1 )。
图 13-1。
Exporting the simple_blog
database with phpMyAdmin
要导出您的simple_blog
数据库及其所有的表和内容,您只需接受 SQL 作为导出格式,然后单击 Go。phpMyAdmin 将生成一个名为simple_blog.sql
的 SQL 文件。你可以在你的downloads
文件夹中找到生成的文件。这个simple_blog.sql
文件保存了你的simple_blog
数据库中所有东西的备份。
下一步是访问 web 主机的 phpMyAdmin。您需要查阅来自您的 web 主机的信息,以找到在您的 web 主机上访问 phpMyAdmin 的正确 URL。您需要一个有效的数据库用户名和密码来登录 phpMyAdmin。
一旦您在 web 主机上登录到 phpMyAdmin,您就可以准备导入您的simple_blog
数据库了。在某些 web 主机上,允许用户创建新的数据库。如果您可以创建一个新的数据库,我建议您为您的项目这样做。您可以将新数据库称为simple_blog
。
如果您的 web 主机不允许您创建新的数据库,您可以简单地为您的项目使用任何预制的数据库。这对你来说应该不成问题。
选择将用于项目的数据库,并单击 phpMyAdmin 导航栏中的 Import 选项。要导入您的simple_blog
,您需要做的就是将simple_blog.sql
文件从您的本地计算机上传到您的 web 主机。一旦你点击 Go,你的simple_blog
数据库中的所有表格和数据都应该被导入到你的新数据库中,运行在你的 web 主机上。通过使用 phpMyAdmin 浏览数据库和表,您可以自己看到。
您的数据库已经准备好了,您可以安全地进入下一步了。
为上传准备 PHP 文件
一旦您的数据库被导入到您的 web 主机,您就可以准备您的 PHP 文件进行上传。不需要太多努力。您只需更改在index.php
中使用的数据库凭证,如下所示:
//Partial code for index.php
//comment out or delete localhost credentials
//$dbInfo = "mysql:host=localhost;dbname=simple_blog";
//$dbUser = "root";
//$dbPassword = "";
//declare valid web host credentials
$dbInfo = "mysql:host=[????];dbname= [????]";
$dbUser = "[????]";
$dbPassword = "[????]";
如果您使用前面的代码示例,您必须用 web 主机所需的有效值替换所有的[????]
实例。您可能需要查阅您的 web 主机文档,看看使用哪个mysql:host
。dbname
应该是您在上一步导入simple_blog
数据库时使用的数据库。Username
和password
应该是您在 web 主机上登录 phpMyAdmin 时使用的用户名和密码。
一旦用有效凭证更新了index.php
中的这三行,就可以将 PHP 文件上传到 web 主机了。
用 FileZilla FTP 上传文件
FTP(文件传输协议)是用于将文件从本地计算机上传到 web 主机的标准协议。许多不同的程序可以用来处理 FTP。你可以使用浏览器,甚至是 Komodo Edit。
也许你已经使用 FTP 将一个 HTML 项目上传到一个网站上。如果你已经熟悉了 FTP 程序,我建议你继续使用它。
如果这是你第一次使用 FTP,建议你从 https://filezilla-project.org/download.php?type=client
下载安装 FileZilla FTP 客户端。我喜欢 FileZilla FTP,因为它简单、功能多样、易于使用。
要将您的博客文件上传到您的 web 主机,您必须建立 FTP 连接。您必须查阅您的 web 主机文档,以了解要连接到哪个 URL 或主机。在 FileZilla FTP 中,你可以在 host 字段中输入你的 FTP 主机的 URL,如图 13-2 所示。您还必须输入用户名和密码。再一次,你将不得不查阅你的 web 主机文档来找到你的用户名和密码。输入凭据后,您可以点按“快速连接”来建立 FTP 连接。
图 13-2。
FileZilla FTP connected to a web host
图 13-2 显示 FileZilla 连接到 web 主机。右侧的窗口显示 web 主机上的文件和文件夹。左边的窗口显示我的本地计算机上的文件和文件夹。
还记得你不得不把你的 PHP 文件放在 XAMPP 的htdocs
文件夹里吗?一些网络主机也有这样的文件夹。互联网上的任何人都可以看到你上传到那个文件夹的任何东西。在你的网络主机上,这个文件夹可能被叫做htdocs
、public_html
或www
。或者你的网络主机会让你用 FTP 上传的任何东西都可以在网上看到。再次,你将不得不查阅你的网站主机文档,看看你的网站主机是如何设置的。
你可以简单地拖放文件和文件夹来上传你的博客。您可能希望将所有来自XAMPP/htdocs/blog
的文件上传到您的本地计算机上,并将这些文件和文件夹传输到您的 web 主机上。
一旦你的 PHP 文件被上传,你的博客就在互联网上直播了!要查看您的作品,您应该将浏览器导航到您的域名的 URL。您应该会看到您的博客,如果您导航到admin.php
,您还应该能够使用您的博客管理员凭据登录。你的博客是实时的,你可以开始为你的读者写吸引人的博文:互联网上的每个人。
摘要
这是一个简短但重要的章节。只需几个步骤,你就完成了博客项目,并在互联网上发布给全世界。
你也到了这本书的结尾,但这并不意味着没有更多的东西要学。实际上,在你从本书中学到的所有知识完全整合之前,你可能还需要创建一些 PHP/MySQL 驱动的网站。进一步发展你的能力的过程可能会很有趣、迷人和令人沮丧。你可以考虑开始学习 PHP 和 MySQL,从新手到专家,作者是 W. Jason Gilmore (Apress,2010)。它更详细地介绍了 PHP 和 MySQL 的概念,并且将带您超越本书所涉及的基础知识。