PHP、MySQL 和 JavaScript 学习指南第六版(四)
原文:
zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f译者:飞龙
第十章: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,-INF或NAN,具体取决于情况。
例如,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 使用的核心部分,并且应被视为高级扩展,所以本书不涵盖它们。
新函数包括RANK、DENSE_RANK、PERCENT_RANK、CUME_DIST、NTILE、ROW_NUMBER、FIRST_VALUE、LAST_VALUE、NTH_VALUE,如果需要了解更多信息,可以在 MySQL 的官方网站上找到完整文档。
MySQL 8 还引入了递归通用表达式(CTE)、增强的 SQL 锁定子句中的NOWAIT和SKIP 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 和安全性的更多信息,请访问官方网站。
问题
-
当声明类属性时,PHP 8 现在允许您做什么?
-
什么是空安全操作符,它有什么作用?
-
如何在 PHP 8 中使用
match表达式,并且它为什么比替代方法更好? -
PHP 8 中现在可以使用什么简单易用的新函数来确定一个字符串是否存在于另一个字符串中?
-
在 PHP 8 中,如何进行浮点数除法计算,而不会导致除以零错误?
-
什么是 polyfill?
-
PHP 8 中,查看由
preg_函数调用生成的最新错误的简单新方法是什么? -
MySQL 8 默认使用什么作为其事务性存储引擎?
-
在 MySQL 8 中,你可以使用什么命令来代替
ALTER TABLE...CHANGE TABLE来修改列名? -
MySQL 8 中默认的认证插件是什么?
参见 “第十章答案”,在 附录 A 中找到这些问题的答案。
第十一章:使用 PHP 访问 MySQL
如果你已经完成了前面的章节,那么你应该已经熟练掌握了使用 MySQL 和 PHP。在本章中,你将学习如何通过使用 PHP 的内置函数来集成这两者,以访问 MySQL。
使用 PHP 查询 MySQL 数据库。
使用 PHP 作为 MySQL 接口的原因是将 SQL 查询的结果格式化为在网页上可见的形式。只要你能使用用户名和密码登录到你的 MySQL 安装中,你也可以从 PHP 中进行这样的操作。
然而,与其使用 MySQL 的命令行来输入指令和查看输出,你可以创建传递给 MySQL 的查询字符串。当 MySQL 返回其响应时,它将以 PHP 可以识别的数据结构形式返回,而不是你在命令行工作时看到的格式化输出。进一步的 PHP 命令可以检索数据并为网页格式化它。
过程
使用 PHP 与 MySQL 的过程如下:
-
连接到 MySQL 并选择要使用的数据库。
-
准备一个查询字符串。
-
执行查询。
-
检索结果并将其输出到网页。
-
重复步骤 2 至 4,直到检索到所有所需数据。
-
与 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>";
}
?>
图 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]);
}
?>
图 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循环以显示每行的内容。然后,程序通过调用$result的fetch方法,将结果行填充到数组$row中。
有了$row中的数据,现在很容易在随后的 heredoc echo语句中显示它,我选择使用<pre>标签来使每个记录的显示对齐。
在每个记录显示后,还有一个第二个表单,也会提交到sqltest.php(程序本身),但这次包含两个隐藏字段:delete和isbn。delete字段设置为yes,isbn设置为$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,然后输出一个带有四个标题——Column、Type、Null 和 Key——以及表格中所有列的 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 作为键的理论代码。
执行附加查询
好了,关于猫的趣味就到此为止。要探索一些稍微复杂的查询,我们需要恢复使用您在第八章中创建的customers和classics表。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 " '$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分别输入fredsmith和mypass的值,则作为传递给 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]);
在继续之前,验证命令是否成功执行是有意义的。以下是您如何通过调用$stmt的rowCount方法来执行验证:
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 处理,它将变成以下完全无害的字符串:
<script src='http://x.com/hack.js'> </script>
<script>hack();</script>
因此,如果您要显示用户输入的任何内容,无论是立即显示还是在将其存储到数据库后显示,都需要首先使用 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);
}
?>
问题
-
如何使用 PDO 连接到 MySQL 数据库?
-
如何使用 PDO 向 MySQL 提交查询?
-
什么样的
fetch方法可以用来将行作为按列编号的数组返回? -
如何手动关闭 PDO 连接?
-
在向具有
AUTO_INCREMENT列的表添加行时,该列应传递什么值? -
哪个 PDO 方法可以用来正确转义用户输入以防止代码注入?
-
在访问数据库时确保数据库安全的最佳方法是什么?
请查看“第十一章答案”在附录 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 所示。
图 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;
?>
图 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`*">
我们已经讨论了name和value属性,但在这里引入了两个新属性:size和maxlength。size属性指定框的宽度(以当前字体的字符数表示),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>
要控制宽度和高度,请使用cols和rows属性。两者均使用当前字体的字符间距来确定区域的大小。如果省略这些值,将创建一个默认输入框,其尺寸将根据使用的浏览器而异,因此应始终定义它们,以确保表单的外观。
最后,您可以使用wrap属性控制输入框中输入的文本如何换行(以及如何将此类换行发送到服务器)。表 12-1 显示了可用的换行类型。如果您省略了wrap属性,则使用软换行。
表 12-1. 输入中可用的换行类型
| 类型 | 动作 |
|---|---|
off | 文本不换行,行与用户输入的完全一样显示。 |
soft | 文本自动换行,但作为一个长字符串发送到服务器,不包含换行和换行符。 |
hard | 文本自动换行,并以软换行或硬换行及换行符的形式发送到服务器。 |
复选框
当您希望向用户提供多个不同选项以供选择时,复选框是一个不错的选择。以下是使用的格式:
<input type="checkbox" name="*`name`*" value="*`value`*" checked="checked">
默认情况下,复选框是方形的。如果包含checked属性,当浏览器显示时该框将已被选中。您为属性分配的字符串应该用双引号或单引号括起来,或者使用值"checked",或者不赋值(只写checked)。如果不包括该属性,则该框显示为未选中。这里是一个创建未选中框的示例:
I Agree <input type="checkbox" name="agree">
如果用户不选中复选框,则不会提交任何值。但如果他们这样做,名为agree的字段将提交值"on"。如果您希望自己的值提交而不是单词on(比如数字 1),您可以使用以下语法:
I Agree <input type="checkbox" name="agree" value="1">
另一方面,如果您希望在提交表单时为读者提供一个通讯快讯,您可能希望复选框已默认为选中状态:
Subscribe? <input type="checkbox" name="news" checked="checked">
如果您希望允许一次选择多个项目组,请为它们分配相同的名称。然而,只有最后一个选中的项目将被提交,除非您将数组作为名称传递。例如,示例 12-4 允许用户选择他们喜欢的冰淇淋(请参见图 12-4 了解其在浏览器中的显示方式)。
示例 12-4. 提供多个复选框选项
Vanilla <input type="checkbox" name="ice" value="Vanilla">
Chocolate <input type="checkbox" name="ice" value="Chocolate">
Strawberry <input type="checkbox" name="ice" value="Strawberry">
图 12-4. 使用复选框进行快速选择
如果只有一个复选框被选中,例如第二个,只有该项将被提交(名为ice的字段将被赋值为"巧克力")。但是如果选择了两个或更多个,只有最后一个值将被提交,之前的值将被忽略。
如果您希望实现独占行为——即只能提交一个项目——那么您应该使用单选按钮而不是复选框(请参见下一节)。否则,要允许多次提交,您必须稍微更改 HTML,如示例 12-5 所示(请注意在ice值后添加方括号[])。
示例 12-5. 使用数组提交多个值
Vanilla <input type="checkbox" name="ice[]" value="Vanilla">
Chocolate <input type="checkbox" name="ice[]" value="Chocolate">
Strawberry <input type="checkbox" name="ice[]" value="Strawberry">
现在,当表单被提交时,如果这些项目中有任何项目被选中,将提交一个名为ice的数组,其中包含所有选定的值。您可以像这样将单个提交的值或值数组提取到一个变量中:
$ice = $_POST['ice'];
如果字段ice已经作为单个值发布,$ice将是一个单一的字符串,比如"草莓"。但是,如果ice在表单中被定义为数组(如在示例 12-5 中),$ice将是一个数组,并且其元素数量将是提交的值的数量。表 12-2 展示了此 HTML 提交的一个、两个或三个选择的七个可能的值集合。在每种情况下,将创建一个包含一个、两个或三个项目的数组。
表 12-2. 数组$ice的七种可能值集合
| 提交一个值 | 提交两个值 | 提交三个值 |
|---|
|
$ice[0] => Vanilla
$ice[0] => Chocolate
$ice[0] => Strawberry
|
$ice[0] => Vanilla
$ice[1] => Chocolate
$ice[0] => Vanilla
$ice[1] => Strawberry
$ice[0] => Chocolate
$ice[1] => Strawberry
|
$ice[0] => Vanilla
$ice[1] => Chocolate
$ice[2] => Strawberry
|
如果 $ice 是一个数组,用于显示其内容的 PHP 代码非常简单,可能看起来像这样:
foreach($ice as $item) echo "$item<br>";
这里使用标准的 PHP foreach 结构来迭代数组 $ice 并将每个元素的值传递到变量 $item,然后通过 echo 命令显示。<br> 只是 HTML 格式设备,用于在显示中每种口味之后强制换行。
单选按钮
单选按钮得名于许多旧收音机上找到的推入预置按钮,其中任何先前按下的按钮在按下另一个按钮时会弹起。当您只想要从两个或多个选项的选择中返回单个值时使用。组中的所有按钮必须使用相同的名称,并且由于只返回一个值,您不必传递数组。
例如,如果您的网站为从您的商店购买的物品提供交货时间选择,您可以使用类似于 示例 12-6 中的 HTML(参见 图 12-5 了解其显示方式)。默认情况下,单选按钮是圆形的。
示例 12-6. 使用单选按钮
8am-Noon<input type="radio" name="time" value="1">
Noon-4pm<input type="radio" name="time" value="2" checked="checked">
4pm-8pm<input type="radio" name="time" value="3">
图 12-5. 使用单选按钮选择单个值
这里,默认选择了第二个选项 Noon–4pm。此默认选择确保用户至少选择一个交货时间,如果他们喜欢,可以将其更改为其他两个选项之一。如果没有一个项目已经被选中,用户可能会忘记选择选项,而对于交货时间则根本不提交任何值。
隐藏字段
有时候,拥有隐藏的表单字段会很方便,这样您可以跟踪表单输入的状态。例如,您可能希望知道表单是否已经提交过。您可以通过在 PHP 代码中添加一些 HTML 来实现,例如以下内容:
echo '<input type="hidden" name="submitted" value="yes">'
这是一个简单的 PHP echo 语句,将一个 input 字段添加到 HTML 表单中。假设表单是在程序外创建并显示给用户的。第一次 PHP 程序接收到输入时,这行代码还没有运行,因此不会有名为 submitted 的字段。PHP 程序重新创建表单,添加 input 字段。因此,当访问者重新提交表单时,PHP 程序将收到带有 submitted 字段设置为 "yes" 的输入。代码可以简单地检查该字段是否存在:
if (isset($_POST['submitted']))
{...
隐藏字段还可以用于存储其他详细信息,例如您可能创建用于识别用户的会话 ID 字符串等。
警告
永远不要将隐藏字段视为安全的,因为它们并非如此。某人可以通过使用浏览器的“查看源代码”功能轻松查看包含它们的 HTML。恶意攻击者也可以制作一个帖子,以删除、添加或更改隐藏字段。