PHP 编程指南第四版(四)
原文:
zh.annas-archive.org/md5/516bbc09499c161bb049b4edb114d468译者:飞龙
第七章:日期和时间
典型的 PHP 开发人员可能需要了解可用的日期和时间函数,比如向数据库记录条目添加日期戳或计算两个日期之间的差异。PHP 提供了 DateTime 类,可以同时处理日期和时间信息,以及与之配合使用的 DateTimeZone 类。
最近几年来,随着网页门户和社交网络社区(如 Facebook 和 Twitter)的出现,时区管理变得更加突出。能够将信息发布到网站,并让其根据用户所在的世界位置识别你在同一网站上的位置,确实是当今的一个要求。但请记住,像 date() 这样的函数会从运行脚本的服务器获取默认信息,因此,除非人类客户告诉你他们在世界上的位置,否则自动确定时区位置可能会非常困难。不过,一旦获取了信息,操作数据就变得很容易(本章稍后将详细讨论时区)。
注意
原始的日期(及相关)函数在 Windows 和某些 Unix 安装中存在时间缺陷。由于底层使用的 32 位有符号整数的特性来管理日期和时间数据,它们无法处理 1901 年 12 月 13 日之前或 2038 年 1 月 19 日之后的日期。因此,建议使用更新的 DateTime 类系列以提高准确性。
处理日期和时间的有四个相互关联的类。DateTime 类处理日期本身;DateTimeZone 类处理时区;DateInterval 类处理两个 DateTime 实例之间的时间跨度;最后,DatePeriod 类处理日期和时间的常规间隔遍历。还有两个不常用的支持类 DateTimeImmutable 和 DateTimeInterface 属于整个 DateTime “家族”,但本章不涵盖这些。
DateTime 类的构造函数自然是一切的起点。此方法接受两个参数,时间戳和时区。例如:
$dt = new DateTime("2019-06-27 16:42:33", new DateTimeZone("America/Halifax"));
我们创建 $dt 对象,为其分配日期和时间字符串作为第一个参数,并使用第二个参数设置时区。在此示例中,我们内联实例化了 DateTimeZone 实例,但您也可以将 DateTimeZone 对象实例化为其自己的变量,然后在构造函数中使用,如下所示:
$dtz = new DateTimeZone("America/Halifax");
$dt = new DateTime("2019-06-27 16:42:33", $dtz);
显然,我们正在为这些类分配硬编码的值,而这种信息类型可能并非始终可供您的代码使用,或者可能不是您想要的。或者,我们可以从服务器获取时区值,并在 DateTimeZone 类中使用。要获取当前服务器值,请使用类似以下代码的代码:
$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
$dt = new DateTime("2019-06-27 16:42:33", $dtz);
这些代码示例为两个类 DateTime 和 DateTimeZone 设定了一组值。最终,你将在脚本的其他地方以某种方式使用这些信息。DateTime 类的一个方法是 format(),它使用与 date_format() 函数相同的格式输出代码。这些日期格式代码都列在附录中,位于 date_format() 函数的部分。这里是 format() 方法作为输出发送到浏览器的示例:
echo "date: " . $dt->format("Y-m-d h:i:s");
`date``:` `2019``-``06``-``27` `04``:``42``:``33`
到目前为止,我们已经提供了日期和时间给构造函数,但有时您还希望从服务器获取日期和时间值。要做到这一点,只需将字符串 "now" 作为第一个参数提供。
下面的代码与其他示例相同,除了这里我们从服务器获取日期和时间类的值。实际上,由于我们从服务器获取信息,类的属性更加充分(请注意,某些 PHP 实例可能未设置此参数,因此将返回错误,且服务器的时区可能与您的时区不匹配):
$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
$dt = new DateTime("now", $dtz);
echo "date: " . $dt->format("Y-m-d h:i:s");
`date``:` `2019``-``06``-``27` `04``:``02``:``54`
DateTime 的 diff() 方法做你可能期望的事情 —— 它返回两个日期之间的差异。方法的返回值是 DateInterval 类的一个实例。
要获取两个 DateTime 实例之间的差异,请使用:
$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
$past = new DateTime("2019-02-12 16:42:33", $dtz);
$current = new DateTime("now", $dtz);
// creates a new instance of DateInterval $diff = $past->diff($current);
$pastString = $past->format("Y-m-d");
$currentString = $current->format("Y-m-d");
$diffString = $diff->format("%yy %mm, %dd");
echo "Difference between {$pastString} and {$currentString} is {$diffString}";
`Difference` `between` `2019``-``02``-``12` `and` `2019``-``06``-``27` `is` `0``y` `4``m``,` `14``d`
diff() 方法是在一个 DateTime 对象上调用的,参数是另一个 DateTime 对象。然后我们使用 format() 方法准备浏览器输出。
注意,DateInterval 类也有一个 format() 方法。由于它处理两个日期之间的差异,格式字符代码与 DateTime 类略有不同。每个字符代码之前都要加上百分号 %。可用的字符代码在表 7-1 中提供。
表 7-1. DateInterval 格式控制字符
| 字符 | 格式化效果 |
|---|---|
a | 天数(例如,23) |
d | 天数(不包括已包含在月数中的天数) |
D | 天数,如果小于 10 天则包括前导零(例如,02 和 125) |
f | 数字微秒(例如,6602 或 41569) |
F | 数字微秒,前导零,至少六位数长度(例如,006602 或 041569) |
h | 小时数 |
H | 小时数,如果小于 10 小时则包括前导零(例如,12 和 04) |
i | 分钟数 |
I | 分钟数,如果小于 10 分钟则包括前导零(例如,05 和 33) |
m | 月数 |
M | 月数,如果小于 10 个月则包括前导零(例如,05 和 1533) |
r | 如果差异为负数,则为 –;如果差异为正数,则为空 |
R | 如果差异为负数,则为 –;如果差异为正数,则为 + |
s | 秒数 |
S | 秒数的数量,如果少于 10 秒,则包括前导零(例如,05 和 15) |
y | 年份的数量 |
Y | 年份的数量,如果少于 10 年,则包括前导零(例如,00 和 12) |
% | 百分号 % |
现在让我们更仔细地看看DateTimeZone类。可以使用get_ini()方法从php.ini文件中获取时区设置。使用getLocation()方法可以从时区对象获取更多信息。它提供时区的原始国家、经度和纬度,以及一些注释。通过这几行代码,您可以开始开发基于网络的 GPS 系统:
$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
echo "Server's Time Zone: {$tz}<br/>";
foreach ($dtz->getLocation() as $key => $value) {
echo "{$key} {$value}<br/>";
}
`Server``'``s` `Time` `Zone``:` `America``/``Halifax`
`country_code` `CA`
`latitude` `44.65`
`longitude` `-``63.6`
`comments` `Atlantic` `-` `NS` `(``most` `areas``);` `PE`
如果您想设置除服务器时区外的时区,必须将该值传递给DateTimeZone对象的构造函数。以下是一个示例,将时区设置为意大利罗马,并使用getLocation()方法显示信息:
$dtz = new DateTimeZone("Europe/Rome");
echo "Time Zone: " . $dtz->getName() . "<br/>";
foreach ($dtz->getLocation() as $key => $value) {
echo "{$key} {$value}<br/>";
}
`Time` `Zone``:` `Europe``/``Rome`
`country_code` `IT`
`latitude` `41.9`
`longitude` `12.48333`
`comments`
可以在PHP 在线手册中找到按全球地区分类的有效时区名称列表。
使用相同的技术,您可以通过为访问者提供一个支持的时区列表,并通过ini_set()函数临时调整您的php.ini设置,使网站对访问者“本地化”。
尽管我们在本章讨论的类提供了相当多的日期和时间处理能力,但这只是冰山一角。请务必在 PHP 官网上进一步了解这些类及其功能。
接下来是什么
在设计 PHP 网站时,除了日期管理之外,还有很多需要理解的内容,因此可能会遇到许多问题,增加烦恼和 PITA(让人头疼的事)因素。下一章提供了多种技巧、窍门以及一些需要注意的问题,帮助减少这些痛点。涵盖的主题包括处理变量、管理表单数据以及使用 SSL(安全套接层)保护网络数据。做好准备吧!
第八章:Web 技术
PHP 设计为一种 Web 脚本语言,尽管可以在纯命令行和 GUI 脚本中使用它,但 Web 占 PHP 使用的绝大多数。动态网站可能具有表单、会话,有时还会进行重定向,本章解释了如何在 PHP 中实现这些元素。您将学习 PHP 如何提供对表单参数和上传文件的访问,如何发送 Cookie 并重定向浏览器,如何使用 PHP 会话等等。
HTTP 基础知识
Web 运行在 HTTP 或超文本传输协议上。该协议规定了 Web 浏览器如何从 Web 服务器请求文件以及服务器如何发送文件。为了理解本章中将向您展示的各种技术,您需要对 HTTP 有基本的了解。有关 HTTP 的更详细讨论,请参阅 HTTP Pocket Reference(O'Reilly)由 Clinton Wong 编写。
当 Web 浏览器请求 Web 页面时,它向 Web 服务器发送 HTTP 请求消息。请求消息始终包含一些头部信息,有时还包含正文。Web 服务器以回复消息响应,该消息始终包含头部信息,并且通常包含正文。HTTP 请求的第一行如下所示:
GET /index.html HTTP/1.1
此行指定了一个称为 方法 的 HTTP 命令,后跟文档的地址和正在使用的 HTTP 协议的版本。在这种情况下,请求正在使用 GET 方法使用 HTTP 1.1 请求 index.html 文档。在此初始行之后,请求可以包含可选的头部信息,为服务器提供有关请求的其他数据。
例如:
User-Agent: Mozilla/5.0 (Windows 2000; U) Opera 6.0 [en]
Accept: image/gif, image/jpeg, text/*, */*
User-Agent 头部提供有关 Web 浏览器的信息,而 Accept 头部指定了浏览器接受的 MIME 类型。在任何头部之后,请求包含一个空行以指示头部部分的结束。如果请求适用于所使用的方法(例如 POST 方法,我们将很快讨论),请求还可以包含其他数据。如果请求不包含任何数据,则以空行结束。
Web 服务器接收请求,处理它,并发送响应。HTTP 响应的第一行如下所示:
HTTP/1.1 200 OK
此行指定协议版本、状态码及该代码的描述。在本例中,状态码为 200,表示请求成功(因此描述为 OK)。状态行之后,响应包含头部,为客户端提供有关响应的附加信息。例如:
Date: Sat, 29 June 2019 14:07:50 GMT
Server: Apache/2.2.14 (Ubuntu)
Content-Type: text/html
Content-Length: 1845
Server 头部提供有关 Web 服务器软件的信息,而 Content-Type 头部指定响应中包含的数据的 MIME 类型。在头部之后,如果请求成功,则响应包含一个空行,然后是请求的数据。
最常见的两种 HTTP 方法是 GET 和 POST。GET 方法用于从服务器检索信息,例如文档、图像或数据库查询的结果。POST 方法用于提交信息,例如信用卡号或要存储在数据库中的信息。当用户在浏览器中键入 URL 或点击链接时,使用 GET 方法。当用户提交表单时,可以使用 GET 或 POST 方法,由 form 标签的 method 属性指定。我们将在“处理表单”章节详细讨论 GET 和 POST 方法。
变量
从 PHP 脚本中可以以三种不同的方式访问服务器配置和请求信息,这些信息总称为 EGPCS(环境、GET、POST、cookies 和 server)。
PHP 创建了六个包含 EGPCS 信息的全局数组:
$_ENV
包含任何环境变量的值,其中数组的键是环境变量的名称。
$_GET
包含作为 GET 请求一部分的任何参数,其中数组的键是表单参数的名称。
$_COOKIE
包含作为请求的一部分传递的任何 cookie 值,其中数组的键是 cookie 的名称。
$_POST
包含作为 POST 请求一部分的任何参数,其中数组的键是表单参数的名称。
$_SERVER
包含关于 Web 服务器的有用信息,如下一节所述。
$_FILES
包含有关上传文件的信息。
这些变量不仅是全局的,而且在函数定义内部也是可见的。$_REQUEST 数组由 PHP 自动创建,并包含 $_GET、$_POST 和 $_COOKIE 数组的所有元素。
服务器信息
$_SERVER 数组包含来自 Web 服务器的许多有用信息,其中大部分来自 公共网关接口(CGI)规范 所需的环境变量。以下是来自 CGI 的完整 $_SERVER 条目列表,包括一些示例值:
PHP_SELF
当前脚本的名称,相对于文档根目录(例如 /store/cart.php)。在前几章的示例代码中已经见过这个用法。当创建自引用脚本时,这个变量非常有用,稍后我们会看到。
SERVER_SOFTWARE
标识服务器的字符串(例如 "Apache/1.3.33 (Unix) mod_perl/1.26 PHP/5.0.4")。
SERVER_NAME
用于自引用 URL 的主机名、DNS 别名或 IP 地址(例如 www.example.com)。
GATEWAY_INTERFACE
遵循的 CGI 标准版本(例如 CGI/1.1)。
SERVER_PROTOCOL
请求协议的名称和版本(例如 HTTP/1.1)。
SERVER_PORT
请求发送到的服务器端口号(例如 80)。
REQUEST_METHOD
客户端用来获取文档的方法(例如,GET)。
PATH_INFO
客户端提供的额外路径元素(例如,/list/users)。
PATH_TRANSLATED
PATH_INFO 的值,由服务器转换为文件名(例如,/home/httpd/htdocs/list/users)。
SCRIPT_NAME
当前页面的 URL 路径,对于自引用脚本很有用(例如,/~me/menu.php)。
QUERY_STRING
在 URL 中 ? 后面的所有内容(例如,name=Fred+age=35)。
REMOTE_HOST
请求此页面的机器的主机名(例如,http://dialup-192-168-0-1.example.com)。如果该机器没有 DNS,这将为空,REMOTE_ADDR 是唯一提供的信息。
REMOTE_ADDR
包含请求此页面的机器的 IP 地址的字符串(例如,"192.168.0.250")。
AUTH_TYPE
用于保护页面的认证方法,如果页面受密码保护的话(例如,basic)。
REMOTE_USER
客户端经过身份验证时使用的用户名,如果页面受密码保护的话(例如,fred)。请注意,无法找出使用的密码。
Apache 服务器还为请求中的每个 HTTP 头创建 $_SERVER 数组的条目。对于每个键,头名称转换为大写,连字符(-)转换为下划线(_),并在前面添加字符串 "HTTP_"。例如,User-Agent 头的条目键为 "HTTP_USER_AGENT"。两个最常见和有用的头部是:
HTTP_USER_AGENT
浏览器用来识别自身的字符串(例如,"Mozilla/5.0 (Windows 2000; U) Opera 6.0 [en]")。
HTTP_REFERER
浏览器声称它来自以获取当前页面的页面的 URL(例如,http://www.example.com/last_page.html)。
处理表单
使用 PHP 处理表单很容易,因为表单参数在 $_GET 和 $_POST 数组中可用。本节描述了一些技巧和技术,使处理更加容易。
方法
正如我们已经讨论过的,客户端可以使用两种 HTTP 方法将表单数据传递给服务器:GET 和 POST。特定表单使用的方法通过 form 标签的 method 属性指定。理论上,HTML 中方法不区分大小写,但实际上一些损坏的浏览器要求方法名必须全部大写。
GET 请求在 URL 中以 查询字符串 的形式编码表单参数,这由紧随 ? 的文本指示:
/path/to/chunkify.php?word=despicable&length=3
POST 请求通过 HTTP 请求的正文传递表单参数,保持 URL 不变。
GET 和 POST 最显著的区别是 URL 行。因为所有表单参数都通过 GET 请求编码在 URL 中,用户可以为 GET 查询创建书签。然而,他们不能使用 POST 请求做到这一点。
然而,GET 和 POST 请求之间最大的区别要微妙得多。HTTP 规范指出,GET 请求是幂等的——也就是说,对于特定 URL 的一个 GET 请求,包括表单参数,与对该 URL 进行两次或更多次请求是相同的。因此,Web 浏览器可以缓存 GET 请求的响应页面,因为响应页面不会因页面加载的次数而改变。由于幂等性,GET 请求应该仅用于查询,例如将单词分割成较小的块或将数字相乘,响应页面永远不会改变。
POST 请求不是幂等的。这意味着它们不能被缓存,服务器在每次显示页面时都会被联系。您可能在显示或重新加载某些页面之前看到您的 Web 浏览器提示您“重新提交表单数据?”。这使得 POST 请求成为适合查询的选择,其响应页面可能随时间变化,例如显示购物车的内容或公告板中的当前消息。
也就是说,幂等性在现实世界中经常被忽略。浏览器缓存通常实现得很差,而重新加载按钮很容易点击,程序员往往只是根据他们是否希望在 URL 中显示查询参数来使用 GET 和 POST。您需要记住的是,GET 请求不应该用于导致服务器更改的任何操作,例如下订单或更新数据库。
用于请求 PHP 页面的方法类型可以通过 $_SERVER['REQUEST_METHOD'] 获得。例如:
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
// handle a GET request
}
else {
die("You may only GET this page.");
}
参数
使用 $_POST、$_GET 和 $_FILES 数组从 PHP 代码中访问表单参数。键是参数名称,值是这些参数的值。因为 HTML 字段名称中允许出现句点,但 PHP 变量名称中不允许出现句点,所以字段名称中的句点在数组中被转换为下划线(_)。
示例 8-1 显示了一个 HTML 表单,用于将用户提供的字符串分块。表单包含两个字段:一个用于字符串(参数名 word),一个用于生成块的大小(参数名 number)。
示例 8-1. chunkify 表单(chunkify.html)
<html>
<head><title>Chunkify Form</title></head>
<body>
<form action="chunkify.php" method="POST">
Enter a word: <input type="text" name="word" /><br />
How long should the chunks be?
<input type="text" name="number" /><br />
<input type="submit" value="Chunkify!">
</form>
</body>
</html>
示例 8-2 列出了 PHP 脚本 chunkify.php,该脚本接收 示例 8-1 中的表单提交的参数值。该脚本将参数值复制到变量中并使用它们。
示例 8-2. chunkify 脚本(chunkify.php)
<?php
$word = $_POST['word'];
$number = $_POST['number'];
$chunks = ceil(strlen($word) / $number);
echo "The {$number}-letter chunks of '{$word}' are:<br />\n";
for ($i = 0; $i < $chunks; $i++) {
$chunk = substr($word, $i * $number, $number);
printf("%d: %s<br />\n", $i + 1, $chunk);
}
?>
图 8-1 显示了 chunkify 表单和生成的输出。
图 8-1. chunkify 表单及其输出
自处理页面
一个 PHP 页面可以用来生成表单,然后处理它。如果用GET方法请求示例 8-3 中显示的页面,它将打印一个接受华氏温度的表单。然而,如果用POST方法调用该页面,页面将计算并显示相应的摄氏温度。
示例 8-3. 自处理的温度转换页面(temp.php)
<html>
<head><title>Temperature Conversion</title></head>
<body>
<?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="POST">
Fahrenheit temperature:
<input type="text" name="fahrenheit" /><br />
<input type="submit" value="Convert to Celsius!" />
</form>
<?php }
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$fahrenheit = $_POST['fahrenheit'];
$celsius = ($fahrenheit - 32) * 5 / 9;
printf("%.2fF is %.2fC", $fahrenheit, $celsius);
}
else {
die("This script only works with GET and POST requests.");
} ?>
</body>
</html>
图 8-2 展示了温度转换页面及其输出。
图 8-2. 温度转换页面及其输出
脚本另一种判断是显示表单还是处理表单的方法是查看是否已提供其中一个参数。这样可以编写一个使用GET方法提交值的自处理页面。示例 8-4 展示了使用GET请求提交参数的温度转换页面的新版本。页面使用参数的存在或不存在来确定要执行的操作。
示例 8-4. 使用 GET 方法的温度转换(temp2.php)
<html>
<head>
<title>Temperature Conversion</title>
</head>
<body>
<?php
if (isset ( $_GET ['fahrenheit'] )) {
$fahrenheit = $_GET ['fahrenheit'];
} else {
$fahrenheit = null;
}
if (is_null ( $fahrenheit )) {
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
Fahrenheit temperature: <input type="text" name="fahrenheit" /><br />
<input type="submit" value="Convert to Celsius!" />
</form>
<?php
} else {
$celsius = ($fahrenheit - 32) * 5 / 9;
printf ( "%.2fF is %.2fC", $fahrenheit, $celsius );
}
?>
</body>
</html>
在示例 8-4 中,我们将表单参数值复制到$fahrenheit中。如果没有给出该参数,$fahrenheit将包含NULL,因此我们可以使用is_null()来测试我们是应该显示表单还是处理表单数据。
粘性表单
许多网站使用一种称为粘性表单的技术,查询结果显示一个搜索表单,其默认值是上一次查询的值。例如,如果在 Google 上搜索“Programming PHP”,结果页面顶部将包含另一个搜索框,其中已经包含“Programming PHP”。要将搜索精确到“Programming PHP from O’Reilly”,您只需添加额外的关键词。
这种粘性行为易于实现。示例 8-5 展示了我们从示例 8-4 中得到的温度转换脚本,其中表单被设置为粘性。基本技术是使用提交的表单值作为创建 HTML 字段时的默认值。
示例 8-5. 带有粘性表单的温度转换(sticky_form.php)
<html>
<head><title>Temperature Conversion</title></head>
<body>
<?php $fahrenheit = $_GET['fahrenheit']; ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
Fahrenheit temperature:
<input type="text" name="fahrenheit" value="<?php echo $fahrenheit; ?>" /><br />
<input type="submit" value="Convert to Celsius!" />
</form>
<?php if (!is_null($fahrenheit)) {
$celsius = ($fahrenheit - 32) * 5 / 9;
printf("%.2fF is %.2fC", $fahrenheit, $celsius);
} ?>
</body>
</html>
多值参数
使用select标签创建的 HTML 选择列表可以允许多项选择。为了确保 PHP 可以识别浏览器传递给表单处理脚本的多个值,您需要在 HTML 表单字段名称后使用方括号[]。例如:
<select name="languages[]">
<option name="c">C</option>
<option name="c++">C++</option>
<option name="php">PHP</option>
<option name="perl">Perl</option>
</select>
当用户提交表单时,$_GET['languages'] 包含一个数组,而不是一个简单的字符串。这个数组包含用户选择的数值。
示例 8-6 说明了 HTML 选择列表中的多个值选择。该表单为用户提供了一组人物属性。用户提交表单后,返回一个(不是很有趣的)用户个性描述。
示例 8-6. 使用选择框的多选值(select_array.php)
<html>
<head><title>Personality</title></head>
<body>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
Select your personality attributes:<br />
<select name="attributes[]" multiple>
<option value="perky">Perky</option>
<option value="morose">Morose</option>
<option value="thinking">Thinking</option>
<option value="feeling">Feeling</option>
<option value="thrifty">Spend-thrift</option>
<option value="shopper">Shopper</option>
</select><br />
<input type="submit" name="s" value="Record my personality!" />
</form>
<?php if (array_key_exists('s', $_GET)) {
$description = join(' ', $_GET['attributes']);
echo "You have a {$description} personality.";
} ?>
</body>
</html>
在示例 8-6 中,提交按钮的名称为"s"。我们检查是否存在此参数值以确定是否需要生成人物描述。图 8-3 展示了多选页面及其结果输出。
图 8-3. 多选页面及其输出
同样的技术适用于任何表单字段,其中可以返回多个值。示例 8-7 展示了我们的人物表单的修订版本,改用复选框而不是选择框。请注意,只有 HTML 发生了变化——处理表单的代码不需要知道多个值是来自复选框还是选择框。
示例 8-7. 复选框中的多选值(checkbox_array.php)
<html>
<head><title>Personality</title></head>
<body>
<form action="<?php $_SERVER['PHP_SELF']; ?>" method="GET">
Select your personality attributes:<br />
<input type="checkbox" name="attributes[]" value="perky" /> Perky<br />
<input type="checkbox" name="attributes[]" value="morose" /> Morose<br />
<input type="checkbox" name="attributes[]" value="thinking" /> Thinking<br />
<input type="checkbox" name="attributes[]" value="feeling" /> Feeling<br />
<input type="checkbox" name="attributes[]" value="thrifty" />Spend-thrift<br />
<input type="checkbox" name="attributes[]" value="shopper" /> Shopper<br />
<br />
<input type="submit" name="s" value="Record my personality!" />
</form>
<?php if (array_key_exists('s', $_GET)) {
$description = join (' ', $_GET['attributes']);
echo "You have a {$description} personality.";
} ?>
</body>
</html>
粘性多值参数
现在你可能会想,我能把多选表单元素设为粘性吗? 可以,但这并不容易。你需要检查表单中的每个可能值是否是提交的值之一。例如:
Perky: <input type="checkbox" name="attributes[]" value="perky"
<?php
if (is_array($_GET['attributes']) && in_array('perky', $_GET['attributes'])) {
echo "checked";
} ?> /><br />
你可以为每个复选框使用这种技术,但这样做很重复且容易出错。此时,编写一个函数以生成可能值的 HTML 并从提交的参数副本中工作会更容易。示例 8-8 展示了一个新版本的多选复选框,表单被设为粘性。尽管这个表单看起来与示例 8-7 中的一样,但在幕后,表单生成的方式有了实质性的变化。
示例 8-8. 粘性多值复选框(checkbox_array2.php)
<html>
<head><title>Personality</title></head>
<body>
<?php // fetch form values, if any
$attrs = $_GET['attributes'];
if (!is_array($attrs)) {
$attrs = array();
}
// create HTML for identically named checkboxes
function makeCheckboxes($name, $query, $options)
{
foreach ($options as $value => $label) {
$checked = in_array($value, $query) ? "checked" : '';
echo "<input type=\"checkbox\" name=\"{$name}\"
value=\"{$value}\" {$checked} />";
echo "{$label}<br />\n";
}
}
// the list of values and labels for the checkboxes
$personalityAttributes = array(
'perky' => "Perky",
'morose' => "Morose",
'thinking' => "Thinking",
'feeling' => "Feeling",
'thrifty' => "Spend-thrift",
'prodigal' => "Shopper"
); ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
Select your personality attributes:<br />
<?php makeCheckboxes('attributes[]', $attrs, $personalityAttributes); ?><br />
<input type="submit" name="s" value="Record my personality!" />
</form>
<?php if (array_key_exists('s', $_GET)) {
$description = join (' ', $_GET['attributes']);
echo "You have a {$description} personality.";
} ?>
</body>
</html>
这段代码的核心是makeCheckboxes()函数。它接受三个参数:复选框组的名称,默认启用的值数组,以及将值映射到描述的数组。复选框的选项列表在$personalityAttributes数组中。
文件上传
要处理文件上传(大多数现代浏览器都支持),使用$_FILES数组。使用各种身份验证和文件上传函数,您可以控制谁可以上传文件以及在文件进入系统后要执行的操作。有关需要注意的安全问题,请参阅第 14 章。
下面的代码显示了一个允许在同一页面上传文件的表单:
<form enctype="multipart/form-data"
action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="10240">
File name: <input name="toProcess" type="file" />
<input type="submit" value="Upload" />
</form>
文件上传的最大问题是风险,即收到太大以至于无法处理的文件。PHP 有两种方式来防止这种情况发生:硬限制和软限制。php.ini 中的 upload_max_filesize 选项为上传文件的硬上限设定了默认值为 2 MB。如果你的表单在任何文件字段参数之前提交一个名为 MAX_FILE_SIZE 的参数,PHP 将使用该值作为软上限。例如,在上一个示例中,上限设置为 10 KB。PHP 会忽略尝试将 MAX_FILE_SIZE 设置为大于 upload_max_filesize 的值的尝试。
此外,请注意,表单标签需要一个 enctype 属性,其值为 "multipart/form-data"。
$_FILES 中的每个元素本身都是一个数组,提供有关上传文件的信息。键包括:
name
浏览器提供的上传文件的名称。这很难进行有意义的使用,因为客户端机器可能具有不同的文件名约定,与运行 Unix 的 Web 服务器(例如,从运行 Windows 的客户端机器获取的 D:\PHOTOS\ME.JPG 文件路径对于 Web 服务器来说毫无意义)。
type
客户端猜测的上传文件的 MIME 类型。
size
上传文件的大小(以字节为单位)。如果用户尝试上传一个太大的文件,则大小将报告为 0。
tmp_name
服务器上保存上传文件的临时文件的名称。如果用户尝试上传一个太大的文件,则名称将显示为 "none"。
测试文件是否成功上传的正确方式是使用 is_uploaded_file() 函数,如下所示:
if (is_uploaded_file($_FILES['toProcess']['tmp_name'])) {
// successfully uploaded
}
文件存储在服务器的默认临时文件目录中,该目录在 php.ini 中使用 upload_tmp_dir 选项指定。要移动文件,请使用 move_uploaded_file() 函数:
move_uploaded_file($_FILES['toProcess']['tmp_name'], "path/to/put/file/{$file}");
调用 move_uploaded_file() 自动检查是否为上传文件。当脚本完成时,从临时目录中删除上传到该脚本的任何文件。
表单验证
当允许用户输入数据时,通常需要在使用或存储数据之前对其进行验证。有几种可用的验证数据的策略。首先是客户端的 JavaScript。然而,由于用户可以选择关闭 JavaScript,或者甚至可能使用不支持 JavaScript 的浏览器,因此这不应是你唯一的验证手段。
更安全的选择是使用 PHP 进行验证。示例 8-9 展示了一个带有表单的自处理页面。页面允许用户输入媒体项目;表单的三个元素——名称、媒体类型和文件名——是必填项。如果用户忽略了其中任何一个值,页面会重新显示,并显示详细的错误信息。用户已经填写的表单字段将保留最初输入的值。最后,作为对用户的额外提示,当用户正在纠正表单时,提交按钮的文本从“Create”更改为“Continue”。
示例 8-9. 表单验证(data_validation.php)
<?php
$name = $_POST['name'];
$mediaType = $_POST['media_type'];
$filename = $_POST['filename'];
$caption = $_POST['caption'];
$status = $_POST['status'];
$tried = ($_POST['tried'] == 'yes');
if ($tried) {
$validated = (!empty($name) && !empty($mediaType) && !empty($filename));
if (!$validated) { ?>
<p>The name, media type, and filename are required fields. Please fill
them out to continue.</p>
<?php }
}
if ($tried && $validated) {
echo "<p>The item has been created.</p>";
}
// was this type of media selected? print "selected" if so
function mediaSelected($type)
{
global $mediaType;
if ($mediaType == $type) {
echo "selected"; }
} ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
Name: <input type="text" name="name" value="<?php echo $name; ?>" /><br />
Status: <input type="checkbox" name="status" value="active"
<?php if ($status == "active") { echo "checked"; } ?> /> Active<br />
Media: <select name="media_type">
<option value="">Choose one</option>
<option value="picture" <?php mediaSelected("picture"); ?> />Picture</option>
<option value="audio" <?php mediaSelected("audio"); ?> />Audio</option>
<option value="movie" <?php mediaSelected("movie"); ?> />Movie</option>
</select><br />
File: <input type="text" name="filename" value="<?php echo $filename; ?>" /><br />
Caption: <textarea name="caption"><?php echo $caption; ?></textarea><br />
<input type="hidden" name="tried" value="yes" />
<input type="submit" value="<?php echo $tried ? "Continue" : "Create"; ?>" />
</form>
在这种情况下,验证仅仅是检查是否提供了一个值。只有当$name、$type和$filename都不为空时,我们才将$validated设置为true。其他可能的验证包括检查电子邮件地址是否有效或检查提供的文件名是否是本地存在的。
例如,要验证年龄字段以确保它包含非负整数,请使用以下代码:
$age = $_POST['age'];
$validAge = strspn($age, "1234567890") == strlen($age);
调用strspn()函数可以找到字符串开头的数字数量。在非负整数中,整个字符串应该由数字组成,因此如果整个字符串由数字组成,则它是一个有效的年龄。我们也可以使用正则表达式进行这种检查:
$validAge = preg_match('/^\d+$/', $age);
验证电子邮件地址是一项几乎不可能的任务。没有办法拿到一个字符串然后判断它是否对应一个有效的电子邮件地址。但是,你可以通过要求用户在两个不同的字段中输入电子邮件地址两次来捕捉输入错误。你还可以通过要求在@符号后的某个位置输入一个句点(.),以及为了额外加分,你可以检查你不希望发送邮件到的域(例如whitehouse.gov或竞争对手的网站)。例如:
$email1 = strtolower($_POST['email1']);
$email2 = strtolower($_POST['email2']);
if ($email1 !== $email2) {
die("The email addresses didn't match");
}
if (!preg_match('/@.+\..+$/', $email1)) {
die("The email address is malformed");
}
if (strpos($email1, "whitehouse.gov")) {
die("I will not send mail to the White House");
}
字段验证基本上是字符串处理。在本例中,我们使用了正则表达式和字符串函数来确保用户提供的字符串是我们期望的类型。
设置响应头
正如我们已经讨论过的那样,服务器发送给客户端的 HTTP 响应包含标识响应主体内容类型、发送响应的服务器、响应主体的字节数、发送响应的时间等头部信息。PHP 和 Apache 通常会为您处理头部信息(例如标识文档为 HTML、计算 HTML 页面的长度等)。大多数 Web 应用程序通常不需要自己设置头部信息。但是,如果您想发送非 HTML 内容、设置页面的过期时间、重定向客户端的浏览器或生成特定的 HTTP 错误,则需要使用header()函数。
设置头文件的唯一注意事项是必须在生成任何正文内容之前执行。这意味着所有对 header()(或者如果您设置 cookies 则为 setcookie())的调用都必须发生在文件的非常顶部,甚至在 <html> 标签之前。例如:
<?php header("Content-Type: text/plain"); ?>
Date: today
From: fred
To: barney
Subject: hands off!
My lunchbox is mine and mine alone. Get your own,
you filthy scrounger!
在文档开始后尝试设置头部会导致以下警告:
Warning: Cannot add header information - headers already sent
您还可以使用输出缓冲区;有关更多信息,请参阅 ob_start()、ob_end_flush() 和相关函数。
不同的内容类型
Content-Type 头部标识返回文档的类型。通常这是 "text/html",表示一个 HTML 文档,但还有其他有用的文档类型。例如,"text/plain" 强制浏览器将页面视为纯文本。这种类型就像自动的“查看源代码”,在调试时非常有用。
在 第十章 和 第十一章 中,我们将大量使用 Content-Type 头部生成实际上是图形图像和 Adobe PDF 文件的文档。
重定向
要将浏览器重定向到一个新的 URL,即重定向,您需要设置 Location 头。通常,随后会立即退出,以便脚本不会继续生成和输出代码清单的其余部分:
header("Location: http://www.example.com/elsewhere.html");
exit();
当提供部分 URL(例如 /elsewhere.html)时,Web 服务器会在内部处理重定向。这通常很少有用,因为浏览器通常不会意识到它没有获取到请求的页面。如果新文档中有相对 URL,则浏览器会将这些 URL 解释为相对于请求的文档,而不是最终发送的文档。一般情况下,您会希望重定向到绝对 URL。
过期
服务器可以明确告知浏览器及可能存在于服务器与浏览器之间的任何代理缓存文档的具体过期日期和时间。代理和浏览器缓存可以在那个时间之前持有文档或者提前过期。重复加载缓存文档不会联系服务器。但尝试获取已过期文档会联系服务器。
要设置文档的过期时间,请使用 Expires 头部:
header("Expires: Tue, 02 Jul 2019 05:30:00 GMT");
要使文档在生成页面后的三小时内过期,使用 time() 和 gmstrftime() 生成过期日期字符串:
$now = time();
$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT", $now + 60 * 60 * 3);
header("Expires: {$then}");
要指示文档“永不”过期,使用一年后的时间:
$now = time();
$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT", $now + 365 * 86440);
header("Expires: {$then}");
要标记文档已过期,使用当前时间或过去的时间:
$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT");
header("Expires: {$then}");
这是防止浏览器或代理缓存存储您的文档的最佳方法:
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
有关控制浏览器和 Web 缓存行为的更多信息,请参阅 Web Caching(O’Reilly 出版)由 Duane Wessels 的第六章。
认证
HTTP 身份验证通过请求头和响应状态工作。浏览器可以在请求头中发送用户名和密码(凭证)。如果凭证未发送或不符合要求,服务器将发送“401 未授权”响应,并通过WWW-Authenticate头标识身份验证的领域(例如"Mary's Pictures"或"Your Shopping Cart")。这通常会在浏览器上弹出一个“输入用户名和密码以 . . .”的对话框,并且页面会使用更新后的凭证重新请求。
要在 PHP 中处理身份验证,请检查用户名和密码($_SERVER的PHP_AUTH_USER和PHP_AUTH_PW项),并调用header()设置领域并发送“401 未授权”响应:
header('WWW-Authenticate: Basic realm="Top Secret Files"');
header("HTTP/1.0 401 Unauthorized");
您可以使用任何方法来验证用户名和密码;例如,您可以查询数据库,读取有效用户的文件,或查询 Microsoft 域服务器。
本示例检查确保密码是用户名的反向(确实不是最安全的身份验证方法!):
$authOK = false;
$user = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
if (isset($user) && isset($password) && $user === strrev($password)) {
$authOK = true;
}
if (!$authOK) {
header('WWW-Authenticate: Basic realm="Top Secret Files"');
header('HTTP/1.0 401 Unauthorized');
// anything else printed here is only seen if the client hits "Cancel"
exit;
}
<!-- your password-protected document goes here -->
如果您要保护多个页面,请将上述代码放入单独的文件中,并将其包含在每个受保护页面的顶部。
如果您的主机使用的是 PHP 的 CGI 版本而不是 Apache 模块,则无法设置这些变量,您需要使用其他形式的身份验证,例如通过 HTML 表单收集用户名和密码。
维护状态
HTTP 是一种无状态协议,这意味着一旦 Web 服务器完成客户端的 Web 页面请求,两者之间的连接就会中断。换句话说,服务器无法识别一系列请求是否来自同一个客户端。
尽管如此,状态是有用的。例如,如果无法跟踪单个用户的一系列请求,就无法构建购物车应用程序。您需要知道用户何时添加或删除项目,以及用户在决定结账时购物车中有什么物品。
为了解决网络缺乏状态的问题,程序员们想出了许多技巧来在请求之间跟踪状态信息(也称为会话跟踪)。其中一种技术是使用隐藏表单字段来传递信息。PHP 将隐藏表单字段视为普通表单字段,因此这些值可以在$_GET和$_POST数组中使用。使用隐藏表单字段,您可以传递整个购物车的内容。但是,更常见的做法是为每个用户分配一个唯一标识符,并使用单个隐藏表单字段传递该标识符。虽然隐藏表单字段在所有浏览器中都有效,但它们仅适用于一系列动态生成的表单,因此它们不如其他一些技术那样普遍有用。
另一种技术是 URL 重写,其中用户可能单击的每个本地 URL 都会动态修改以包含额外信息。这些额外信息通常作为 URL 的参数指定。例如,如果为每个用户分配一个唯一的 ID,可以在所有 URL 中包含该 ID,如下所示:
http://www.example.com/catalog.php?userid=123
如果确保动态修改所有本地链接以包含用户 ID,现在可以在应用程序中跟踪个别用户。URL 重写适用于所有动态生成的文档,不仅仅是表单,但实际执行重写可能很繁琐。
维护状态的第三种最普遍的技术是使用cookies。Cookie 是服务器可以给客户端的一小段信息。在随后的每个请求中,客户端将该信息返回给服务器,从而标识自己。Cookies 对于通过浏览器重复访问时保留信息很有用,但它们也不是没有问题的。主要问题在于,大多数浏览器允许用户禁用 cookies。因此,任何使用 cookies 进行状态维护的应用程序都需要使用另一种技术作为后备机制。我们稍后将更详细地讨论 cookies。
使用 PHP 维护状态的最佳方式是使用内置的会话跟踪系统。此系统允许您创建可从应用程序的不同页面访问的持久变量,以及同一用户在访问站点的不同次数时也可以访问这些变量。在幕后,PHP 的会话跟踪机制使用 cookies(或 URLs)优雅地解决大多数需要状态的问题,并为您处理所有细节。我们稍后将在本章中详细介绍 PHP 的会话跟踪系统。
Cookies
一个 cookie 基本上是一个包含多个字段的字符串。服务器可以在响应的头部中向浏览器发送一个或多个 cookies。某些 cookie 的字段指示浏览器应将 cookie 作为请求的一部分发送到哪些页面。cookie 的 value 字段是有效载荷——服务器可以在其中存储任何喜欢的数据(在限制内),如标识用户的唯一代码、偏好等。
使用 setcookie() 函数将 cookie 发送到浏览器:
setcookie(*`name`* [, *`value`* [, *`expires`* [, *`path`* [, *`domain`* [, *`secure`* [,
*`httponly`* ]]]]]]);
此函数从给定参数创建 cookie 字符串,并创建一个 Cookie 头部,其值为该字符串。因为 cookies 作为响应的头部发送,所以必须在发送文档的任何主体之前调用 setcookie()。setcookie() 的参数包括:
名称
特定 cookie 的唯一名称。您可以拥有多个具有不同名称和属性的 cookies。名称不能包含空格或分号。
值
此 cookie 附加的任意字符串值。最初的 Netscape 规范将 cookie 的总大小(包括名称、过期日期和其他信息)限制为 4 KB,因此虽然 cookie 值的大小没有具体限制,但它可能不会大于 3.5 KB。
过期
此 cookie 的过期日期。如果未指定过期日期,则浏览器将 cookie 保存在内存中而不是在磁盘上。当浏览器退出时,cookie 消失。过期日期是自 1970 年 1 月 1 日午夜(GMT)以来的秒数。例如,传递time() + 60 * 60 * 2将使 cookie 在两小时后过期。
path
浏览器只会为此路径下的 URL 返回 cookie。默认情况下是当前页面所在的目录。例如,如果*/store/front/cart.php设置了一个 cookie,并且没有指定路径,那么该 cookie 将在所有 URL 路径以/store/front/*开头的页面上发送回服务器。
domain
浏览器只会为此域名下的 URL 返回 cookie。默认是服务器主机名。
secure
浏览器只会在https连接下传输 cookie。默认情况下是false,意味着可以在不安全的连接下发送 cookie。
httponly
如果将此参数设置为TRUE,则 cookie 将仅通过 HTTP 协议可用,因此无法通过其他方式如 JavaScript 访问。关于这是否能提供更安全的 cookie,仍有争论,因此请谨慎使用此参数并进行充分测试。
setcookie()函数还有一种替代语法:
`setcookie` ($name [, $value = "" [, $options = [] ]] )
其中$options是一个数组,保存了跟在$value内容后面的其他参数。这样可以稍微节省setcookie()函数的代码行长度,但是在使用前必须先构建好$options数组,因此存在一定的权衡考量。
当浏览器将 cookie 发送回服务器时,您可以通过$_COOKIE数组访问该 cookie。键是 cookie 名称,值是 cookie 的value字段。例如,页面顶部的以下代码跟踪了客户端访问该页面的次数:
$pageAccesses = $_COOKIE['accesses'];
setcookie('accesses', ++$pageAccesses);
解码 cookie 时,cookie 名称中的任何句点(.)都会变成下划线。例如,名为tip.top的 cookie 可以通过$_COOKIE['tip_top']访问。
让我们看看 cookie 的实际效果。首先,示例 8-10 显示了一个 HTML 页面,提供了各种背景和前景颜色的选项。
示例 8-10. 偏好选择 (colors.php)
<html>
<head><title>Set Your Preferences</title></head>
<body>
<form action="prefs.php" method="post">
<p>Background:
<select name="background">
<option value="black">Black</option>
<option value="white">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select><br />
Foreground:
<select name="foreground">
<option value="black">Black</option>
<option value="white">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select></p>
<input type="submit" value="Change Preferences">
</form>
</body>
</html>
示例 8-10 中的表单提交到 PHP 脚本prefs.php,如示例 8-11 所示,该脚本会为表单中指定的颜色偏好设置 cookie。注意,在 HTML 页面启动后才调用setcookie()函数。
示例 8-11. 使用 cookie 设置偏好 (prefs.php)
<html>
<head><title>Preferences Set</title></head>
<body>
<?php
$colors = array(
'black' => "#000000",
'white' => "#ffffff",
'red' => "#ff0000",
'blue' => "#0000ff"
);
$backgroundName = $_POST['background'];
$foregroundName = $_POST['foreground'];
setcookie('bg', $colors[$backgroundName]);
setcookie('fg', $colors[$foregroundName]);
?>
<p>Thank you. Your preferences have been changed to:<br />
Background: <?php echo $backgroundName; ?><br />
Foreground: <?php echo $foregroundName; ?></p>
<p>Click <a href="prefs_demo.php">here</a> to see the preferences
in action.</p>
</body>
</html>
示例 8-11 生成的页面包含指向另一页的链接,如示例 8-12 所示,该页面通过访问$_COOKIE数组使用颜色偏好。
示例 8-12. 使用带有 cookie 的颜色偏好(prefs_demo.php)
<html>
<head><title>Front Door</title></head>
<?php
$backgroundName = $_COOKIE['bg'];
$foregroundName = $_COOKIE['fg'];
?>
<body bgcolor="<?php echo $backgroundName; ?>" text="<?php echo $foregroundName; ?>">
<h1>Welcome to the Store</h1>
<p>We have many fine products for you to view. Please feel free to browse
the aisles and stop an assistant at any time. But remember, you break it
you bought it!</p>
<p>Would you like to <a href="colors.php">change your preferences?</a></p>
</body>
</html>
关于 cookie 使用有许多注意事项。并非所有客户端(浏览器)都支持或接受 cookie,即使客户端支持 cookie,用户也可以关闭它们。此外,cookie 规范规定单个 cookie 大小不得超过 4 KB,每个域名最多允许 20 个 cookie,并且客户端上最多可以存储 300 个 cookie。某些浏览器可能有更高的限制,但不能依赖此点。最后,你无法控制浏览器何时实际过期 cookie —— 如果浏览器达到容量上限并需要添加新 cookie,则可能丢弃尚未过期的 cookie。你还应注意设置快速过期的 cookie。到期时间依赖于客户端时钟与你的时钟一样准确。许多人没有准确设置系统时钟,因此不能依赖于快速过期。
尽管存在这些限制,cookie 对于通过浏览器的重复访问保留信息非常有用。
会话
PHP 内置支持会话,处理所有的 cookie 操作,以提供可以从不同页面访问并跨多次访问站点的持久变量。会话使你能够轻松创建多页面表单(如购物车)、保存用户认证信息以及在站点上存储持久用户首选项。
每个首次访问者都会被分配一个唯一的会话 ID。默认情况下,会话 ID 存储在名为 PHPSESSID 的 cookie 中。如果用户的浏览器不支持 cookie 或已关闭 cookie,会话 ID 将通过网站内的 URL 传播。
每个会话都有一个关联的数据存储。你可以注册变量,以便在每个页面启动时从数据存储加载,并在页面结束时保存回数据存储。注册的变量在页面间持久存在,并且在一个页面上对变量的更改可以在其他页面上看到。例如,“将此商品加入购物车”的链接可以将用户带到一个页面,向购物车中注册的数组添加项目。然后可以在另一个页面上使用此注册的数组来显示购物车的内容。
会话基础
脚本开始运行时会自动启动会话。如有必要,会生成新的会话 ID,可能创建一个要发送到浏览器的 cookie,并从存储中加载任何持久变量。
你可以通过将变量名称传递给 $_SESSION[] 数组来将变量注册到会话中。例如,这里是一个基本的点击计数器:
session_start();
$_SESSION['hits'] = $_SESSION['hits'] + 1;
echo "This page has been viewed {$_SESSION['hits']} times.";
session_start() 函数将注册的变量加载到关联数组 $_SESSION 中。键是变量的名称(例如 $_SESSION['hits'])。如果你感兴趣,session_id() 函数返回当前会话 ID。
要结束会话,请调用 session_destroy()。这将删除当前会话的数据存储,但不会从浏览器缓存中删除 cookie。这意味着,在后续访问启用会话的页面时,用户将具有与调用 session_destroy() 之前相同的会话 ID,但没有数据。
示例 8-13 展示了从 示例 8-11 改写的代码,使用会话而不是手动设置 cookie。
示例 8-13. 使用会话设置首选项(prefs_session.php)
<?php session_start(); ?>
<html>
<head><title>Preferences Set</title></head>
<body>
<?php
$colors = `array`(
'black' => "#000000",
'white' => "#ffffff",
'red' => "#ff0000",
'blue' => "#0000ff"
);
$bg = $colors[`$_POST`['background']];
$fg = $colors[`$_POST`['foreground']];
`$_SESSION`['bg'] = $bg;
`$_SESSION`['fg'] = $fg;
?>
<p>Thank you. Your preferences have been changed to:<br /> Background: <?php `echo` `$_POST`['background']; ?><br /> Foreground: <?php `echo` `$_POST`['foreground']; ?></p>
<p>Click <a href="prefs_session_demo.php">here</a> to see the preferences
in action.</p>
</body>
</html>
示例 8-14 展示了从 示例 8-12 改写为使用会话。会话启动后,创建了 $bg 和 $fg 变量,脚本只需使用它们即可。
示例 8-14. 使用会话中的首选项(prefs_session_demo.php)
<?php
session_start() ;
$backgroundName = `$_SESSION`['bg'] ;
$foregroundName = `$_SESSION`['fg'] ;
?>
<html>
<head><title>Front Door</title></head>
<body bgcolor="<?php `echo` $backgroundName; ?>" text="<?php `echo` $foregroundName; ?>">
<h1>Welcome to the Store</h1>
<p>We have many fine products for you to view. Please feel free to browse
the aisles and stop an assistant at any time. But remember, you break it
you bought it!</p>
<p>Would you like to <a href="colors.php">change your preferences?</a></p>
</body></html>
要查看此更改,只需更新 colors.php 文件中的操作目标。默认情况下,PHP 会话 ID cookie 在浏览器关闭时过期。也就是说,会话在浏览器停止存在后不会持久保存。要更改此设置,您需要在 php.ini 中设置 session.cookie_lifetime 选项为 cookie 的生命周期(以秒为单位)。
替代方案为 cookie
默认情况下,会话 ID 通过 PHPSESSID cookie 从页面传递到页面。但是,PHP 的会话系统支持两种替代方案:表单字段和 URL。通过隐藏表单字段传递会话 ID 非常笨拙,因为它强制您将每个页面之间的每个链接都变成表单的提交按钮。我们将不在此处进一步讨论此方法。
传递会话 ID 的 URL 系统相对更加优雅。PHP 可以重写您的 HTML 文件,将会话 ID 添加到每个相对链接中。但是,要使此功能正常工作,PHP 在编译时必须配置 -enable-trans-id 选项。这会导致性能损失,因为 PHP 必须解析和重写每个页面。繁忙的站点可能希望使用 cookie,因为它们不会因页面重写而减慢速度。此外,这会暴露您的会话 ID,可能导致中间人攻击。
自定义存储
默认情况下,PHP 将会话信息存储在服务器临时目录中的文件中。每个会话变量存储在单独的文件中。每个变量以专有格式序列化到文件中。您可以在 php.ini 文件中更改所有这些值。
您可以通过在 php.ini 中设置 session.save_path 值来更改会话文件的位置。如果您在具有自己安装的 PHP 的共享服务器上,请将目录设置为您自己目录树中的某个位置,这样同一台机器上的其他用户就无法访问您的会话文件。
PHP 可以将会话信息存储在当前会话存储中的两种格式之一——PHP 的内置格式或 Web 分布数据交换(WDDX)格式。您可以通过在您的 php.ini 文件中设置 session.serialize_handler 值为 php(默认行为)或 wddx(WDDX 格式)来更改格式。
结合 Cookies 和 Sessions
使用 Cookies 和自定义的会话处理程序的组合,您可以跨访问保留状态。任何应该在用户离开站点时被遗忘的状态,例如用户正在访问的页面,可以交给 PHP 的内置会话处理。任何应该在用户访问之间保持的状态,例如唯一的用户 ID,可以存储在一个 Cookie 中。使用用户 ID,您可以从永久存储(如数据库)中检索用户更长期的状态(显示偏好、邮寄地址等)。
示例 8-15 允许用户选择文本和背景颜色,并将这些值存储在一个 Cookie 中。在接下来的一周内访问页面时,会将颜色值在 Cookie 中发送。
示例 8-15. 跨访问保存状态(save_state.php)
<?php
if($_POST['bgcolor']) {
setcookie('bgcolor', $_POST['bgcolor'], time() + (60 * 60 * 24 * 7));
}
if (isset($_COOKIE['bgcolor'])) {
$backgroundName = $_COOKIE['bgcolor'];
}
else if (isset($_POST['bgcolor'])) {
$backgroundName = $_POST['bgcolor'];
}
else {
$backgroundName = "gray";
} ?>
<html>
<head><title>Save It</title></head>
<body bgcolor="<?php echo $backgroundName; ?>">
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
<p>Background color:
<select name="bgcolor">
<option value="gray">Gray</option>
<option value="white">White</option>
<option value="black">Black</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="red">Red</option>
</select></p>
<input type="submit" />
</form>
</body>
</html>
SSL
安全套接字层(SSL)提供了一个安全的通道,使得常规的 HTTP 请求和响应可以流动。PHP 并不专门关注 SSL,因此您无法通过 PHP 控制加密方式。一个 https:// 的 URL 表示该文档的连接是安全的,而不像 http:// 的 URL。
如果 PHP 页面是在 SSL 连接的请求下生成的,$_SERVER 数组中的 HTTPS 条目将被设置为 'on'。要防止页面在非加密连接下生成,只需简单地使用:
if ($_SERVER['HTTPS'] !== 'on') {
die("Must be a secure connection.");
}
一个常见的错误是在安全连接下发送表单(例如,*www.example.com/form.html*)… action 设置为一个 http:// 的 URL。用户输入的任何表单参数都会通过不安全的连接发送,而简单的数据包嗅探器可以将其暴露出来。
接下来是什么
在现代 Web 开发中有许多技巧和陷阱,我们希望本章所指出的内容能帮助您构建出色的网站。下一章将讨论如何将数据保存到 PHP 中的数据存储中,我们将涵盖大多数常用的方法,如数据库、SQL 和 NoSQL 风格、SQLite,以及直接文件信息存储。
第九章:数据库
PHP 支持超过 20 种数据库,包括最流行的商业和开源种类。关系数据库系统,如 MariaDB、MySQL、PostgreSQL 和 Oracle,是大多数现代动态网站的支柱。这些系统存储着购物车信息、购买历史、产品评论、用户信息、信用卡号码,有时甚至是网页本身。
本章介绍如何从 PHP 访问数据库。我们重点介绍内置的PHP Data Objects(PDO)库,它允许你使用相同的函数访问任何数据库,而不是使用众多特定于数据库的扩展。在本章中,你将学习如何从数据库中获取数据、将数据存储到数据库中以及处理错误。最后,我们将展示一个示例应用程序,演示如何将各种数据库技术应用到实际中。
这本书无法详尽介绍使用 PHP 创建 Web 数据库应用程序的所有细节。想深入了解 PHP/MySQL 组合,请参阅《Web Database Applications with PHP and MySQL, Second Edition》(O’Reilly),作者是 Hugh Williams 和 David Lane,链接在这里。
使用 PHP 访问数据库
从 PHP 访问数据库有两种方式。一种是使用特定于数据库的扩展,另一种是使用独立于数据库的 PDO 库。每种方法都有其优缺点。
如果你使用特定于某种数据库的扩展,你的代码将与你使用的数据库密切相关。例如,MySQL 扩展的函数名称、参数、错误处理等完全不同于其他数据库扩展的函数。如果你想将数据库从 MySQL 迁移到 PostgreSQL,将涉及对你的代码进行重大更改。而 PDO 则通过抽象层将数据库特定的功能隐藏,因此在不同数据库系统之间移动可能只需要修改程序中的一行代码或你的php.ini文件。
抽象层(如 PDO 库)的可移植性是有代价的,因为使用它的代码通常比使用本地数据库特定扩展的代码略慢一些。
请记住,抽象层在确保你的实际 SQL 查询可移植性方面毫无帮助。如果你的应用程序使用任何非通用 SQL,你将需要大量工作将你的查询从一个数据库转换到另一个数据库。在本章中,我们将简要讨论数据库接口的两种方法,然后看看管理 Web 动态内容的其他方法。
关系数据库和 SQL
关系数据库管理系统(RDBMS)是一个为您管理数据的服务器。数据被结构化成表,每个表有若干列,每列都有一个名称和类型。例如,为了跟踪科幻书籍,我们可能有一个“books”表记录标题(字符串)、发布年份(数字)和作者。
表被组合到数据库中,所以科幻书数据库可能有用于时间段、作者和反派的表。关系数据库管理系统通常有其自己的用户系统,用于控制对数据库的访问权限(例如,“用户 Fred 可以更新数据库作者”)。
PHP 使用结构化查询语言(SQL)与关系数据库(如 MariaDB 和 Oracle)进行通信。您可以使用 SQL 创建、修改和查询关系数据库。
SQL 的语法分为两部分。第一部分是数据操作语言(DML),用于检索和修改现有数据库中的数据。DML 非常紧凑,只有四个操作或动词:SELECT、INSERT、UPDATE和DELETE。用于创建和修改保存数据的数据库结构的 SQL 命令集称为数据定义语言,或 DDL。DDL 的语法没有像 DML 那样标准化,但由于 PHP 只是将您提供的任何 SQL 命令发送给数据库,您可以使用数据库支持的任何 SQL 命令。
注意
用于创建此样本图书馆数据库的 SQL 命令文件可在名为library.sql的文件中找到。
假设您有一个名为books的表,这条 SQL 语句将插入一行新数据:
INSERT INTO books VALUES (null, 4, 'I, Robot', '0-553-29438-5', 1950, 1);
此 SQL 语句插入一行新数据,但指定了具有值的列:
INSERT INTO books (authorid, title, ISBN, pub_year, available)
VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1);
要删除所有 1979 年出版的书籍(如果有的话),我们可以使用这条 SQL 语句:
DELETE FROM books WHERE pub_year = 1979;
要将Roots的年份更改为 1983 年,请使用此 SQL 语句:
UPDATE books SET pub_year=1983 WHERE title='Roots';
要仅获取 1980 年代出版的书籍,请使用:
SELECT * FROM books WHERE pub_year > 1979 AND pub_year < 1990;
你还可以指定要返回的字段。例如:
SELECT title, pub_year FROM books WHERE pub_year > 1979 AND pub_year < 1990;
您可以发出将来自多个表的信息汇总的查询。例如,此查询将book和author表连接起来,让我们看到每本书的作者是谁:
SELECT authors.name, books.title FROM books, authors
WHERE authors.authorid = books.authorid;
你甚至可以像这样简写(或别名)表名:
SELECT a.name, b.title FROM books b, authors a WHERE a.authorid = b.authorid;
有关 SQL 的更多信息,请参阅SQL in a Nutshell,第三版(O'Reilly),作者 Kevin Kline。
PHP 数据对象
PHP 网站对 PDO 有以下介绍:
PHP 数据对象(PDO)扩展定义了一个轻量级、一致的接口,用于在 PHP 中访问数据库。每个实现 PDO 接口的数据库驱动程序都可以将特定于数据库的特性公开为常规扩展函数。请注意,您不能仅使用 PDO 扩展执行任何数据库函数;您必须使用特定于数据库的 PDO 驱动程序来访问数据库服务器。
PDO 的其他独特功能包括:
-
是一个本地 C 扩展
-
利用最新的 PHP 7 内部功能
-
使用结果集的缓冲读取数据
-
作为基础提供常见的数据库功能。
-
仍然能够访问特定于数据库的函数。
-
可以使用基于事务的技术。
-
可以与数据库中的大对象(LOBs)交互。
-
可以使用带绑定参数的准备和可执行 SQL 语句。
-
可以实现可滚动的游标。
-
提供了
SQLSTATE错误代码和非常灵活的错误处理能力。
由于这里涉及的功能有很多,我们只会触及其中一部分,以展示 PDO 可以有多么有益。
首先,介绍一下 PDO。它为几乎所有数据库引擎提供了驱动程序,而那些 PDO 未提供的驱动程序应通过 PDO 的通用 ODBC 连接进行访问。PDO 是模块化的,至少需要启用两个扩展才能激活:PDO 扩展本身和特定于您将进行接口的数据库的 PDO 扩展。请参阅在线文档)以设置连接到您选择的数据库的连接。例如,要在 Windows 服务器上为 MySQL 交互建立 PDO,只需将以下两行代码输入到您的php.ini文件中,并重新启动服务器:
extension=php_pdo.dll
extension=php_pdo_mysql.dll
PDO 库也是面向对象的扩展(正如您将在接下来的代码示例中看到的)。
建立连接
使用 PDO 的第一个要求是连接到所讨论的数据库,并将该连接保持在连接句柄变量中,如以下代码所示:
$db = new PDO($*`dsn`*, $*`username`*, $*`password`*);
$dsn代表数据源名称,另外两个参数是不言自明的。特别是对于 MySQL 连接,您会写如下代码:
$db = new PDO("mysql:host=localhost;dbname=library", "petermac", "abc123");
当然,您可以(应该)保持基于变量的用户名和密码参数,以便重用和灵活性原因。
与数据库交互
连接到数据库引擎并与要与之交互的数据库连接后,您可以使用该连接向服务器发送 SQL 命令。一个简单的UPDATE语句看起来像这样:
$db->query("UPDATE books SET authorid=4 WHERE pub_year=1982");
此代码仅更新图书表并释放查询。这允许您直接向数据库发送简单的 SQL 命令(例如UPDATE、DELETE、INSERT)。
使用 PDO 和准备语句
更典型的情况是,您将使用准备语句,分阶段或步骤地发出 PDO 调用。考虑以下代码:
$statement = $db->prepare("SELECT * FROM books");
$statement->execute();
// handle row results, one at a time
while($row = $statement->fetch()) {
print_r($row);
// ... or probably do something more meaningful with each returned row
}
$statement = null;
在这段代码中,我们首先“准备”SQL 代码,然后“执行”它。接下来,我们用while代码循环处理结果,最后通过将null赋给它来释放结果对象。在这个简单的示例中,这可能看起来并不那么强大,但是有其他可以与准备语句一起使用的功能。现在,考虑下面的代码:
$statement = $db->prepare("INSERT INTO books (authorid, title, ISBN, pub_year)"
. "VALUES (:authorid, :title, :ISBN, :pub_year)");
$statement->execute(array(
'authorid' => 4,
'title' => "Foundation",
'ISBN' => "0-553-80371-9",
'pub_year' => 1951),
);
在这里,我们使用四个命名占位符(authorid、title、ISBN 和 pub_year)准备了 SQL 语句。在这种情况下,这些恰好是数据库中列的名称,但这只是为了清晰起见——占位符名称可以是任何对您有意义的内容。在执行调用中,我们用想要在此特定查询中使用的实际数据替换这些占位符。准备语句的一个优点是,您可以多次执行相同的 SQL 命令,并通过数组每次传递不同的值。您还可以使用位置占位符(实际上不命名它们),用? 表示,这是要替换的位置项。看看前一个代码的以下变化:
$statement = $db->prepare("INSERT INTO books (authorid, title, ISBN, pub_year)"
. "VALUES (?, ?, ?, ?)");
$statement->execute(array(4, "Foundation", "0-553-80371-9", 1951));
这样做的效果是一样的,但代码更少,因为 SQL 语句的值区域不命名要替换的元素,因此在 execute 语句中,只需发送原始数据,而无需名称。您只需确保发送到准备语句的数据的位置。
处理事务
一些关系型数据库管理系统支持事务,在事务中,一系列数据库更改可以被提交(一次性应用)或回滚(丢弃,数据库中没有应用任何更改)。例如,当银行处理资金转账时,从一个账户取款并存入另一个账户必须同时发生——两者不能单独发生,两个操作之间也不应该有时间间隔。PDO 使用 try...catch 结构优雅地处理事务,例如 示例 9-1 中的这个。
示例 9-1. 使用 try...catch 代码结构
try {
// connection successful
$db = new PDO("mysql:host=localhost;dbname=banking_sys", "petermac", "abc123");
} catch (Exception $error) {
die("Connection failed: " . $error->getMessage());
}
try {
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->beginTransaction();
$db->exec("insert into accounts (account_id, amount) values (23, '5000')" );
$db->exec("insert into accounts (account_id, amount) values (27, '-5000')" );
$db->commit();
} catch (Exception $error) {
$db->rollback();
echo "Transaction not completed: " . $error->getMessage();
}
如果整个事务无法完成,那么它将不会完成,并且会抛出异常。
如果在不支持事务的数据库上调用 commit() 或 rollback(),则这些方法将返回 DB_ERROR。
注意
请确保检查您的底层数据库产品是否支持事务。
调试语句
PDO 接口提供了一种显示关于 PDO 语句的详细信息的方法,如果出现问题,这可能对调试非常有用。
$statement = $db->prepare("SELECT title FROM books WHERE authorid = ?)";
$statement->bindParam(1, "12345678", PDO::PARAM_STR);
$statement->execute();
$statement->debugDumpParams();
调用语句对象上的 debugDumpParams() 方法会打印有关调用的各种信息:
SQL: [35] SELECT title
FROM books
WHERE authorID = ?
Sent SQL: [44] SELECT title
FROM books
WHERE authorid = "12345678"
Params: 1
Key: Position #0:
paramno=0
name[0] ""
is_param=1
param_type=2
Sent SQL 部分仅在语句执行后显示;在此之前,只有 SQL 和 Params 部分可用。
MySQLi 对象接口
PHP 最流行的数据库平台是 MySQL 数据库。如果你查看 MySQL 网站,你会发现有几个不同的 MySQL 版本可供使用。我们将看看自由发布版本,称为社区服务器。PHP 还有许多不同的接口可以访问这个数据库工具,所以我们将看看对象导向接口,称为 MySQLi,又称MySQL Improved 扩展。
最近,MariaDB 开始超越 MySQL 成为 PHP 开发者首选的数据库。按设计,MariaDB 与 MySQL 兼容,这意味着你可以安装 MariaDB,卸载 MySQL,并将你的 PHP 配置指向 MariaDB,可能不需要其他更改。
如果你对面向对象的接口和概念不是很熟悉,请确保在深入学习本节之前阅读第六章。
由于这种面向对象的接口已经内置到 PHP 的标准安装配置中(只需在 PHP 环境中激活 MySQLi 扩展),你只需实例化其类,如下面的代码所示:
$db = new mysqli(*`host`*, *`user`*, *`password`*, *`databaseName`*);
在这个例子中,我们有一个名为library的数据库,我们将使用虚构的用户名petermac和密码1q2w3e9i8u7y。实际使用的代码如下:
$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");
这样我们就可以在 PHP 代码中访问数据库引擎本身;我们将稍后特别访问表和其他数据。一旦这个类被实例化为变量$db,我们就可以使用该对象的方法来进行数据库工作。
生成一些代码插入新书到library数据库的简短示例会看起来像这样:
$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");
$sql = "INSERT INTO books (authorid, title, ISBN, pub_year, available)
VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1)";
if ($db->query($sql)) {
echo "Book data saved successfully.";
} else {
echo "INSERT attempt failed, please try again later, or call tech support" ;
}
$db->close();
首先,我们将 MySQLi 类实例化为变量$db。接下来,我们构建我们的 SQL 命令字符串,并将其保存到名为$sql的变量中。然后我们调用类的 query 方法,同时测试其返回值以确定是否成功(TRUE),然后相应地注释到屏幕上。在这个阶段,你可能不想将内容echo到浏览器上,因为这只是一个例子。最后,我们在类上调用close()方法来清理和销毁类从内存中。
检索数据以进行显示
在你网站的另一个区域,你可能希望列出你的书籍清单,并显示它们的作者是谁。我们可以通过使用相同的 MySQLi 类,并处理从SELECT SQL 命令生成的结果集来实现这一点。有许多方法可以在浏览器中显示信息,我们将看一个例子来展示如何实现这一点。请注意,返回的结果是一个不同的对象,而不是我们首先实例化的$db。PHP 会为你实例化结果对象,并将其填充为任何返回的数据。
$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");
$sql = "SELECT a.name, b.title FROM books b, authors a WHERE
a.authorid=b.authorid";
$result = $db->query($sql);
while ($row = $result->fetch_assoc()) {
echo "{$row['name']} is the author of: {$row['title']}<br />";
}
$result->close();
$db->close();
在这里,我们使用 query() 方法调用,并将返回的信息存储到名为$result的变量中。然后我们使用结果对象的 fetch_assoc() 方法逐行提供数据,并将该单行存储到名为$row的变量中。只要有行要处理,这个过程就会继续。在那个while循环内,我们将内容输出到浏览器窗口。最后,我们关闭结果和数据库对象。
输出如下所示:
J.R.R. Tolkien is the author of: The Two Towers
J.R.R. Tolkien is the author of: The Return of The King
J.R.R. Tolkien is the author of: The Hobbit
Alex Haley is the author of: Roots
Tom Clancy is the author of: Rainbow Six
Tom Clancy is the author of: Teeth of the Tiger
Tom Clancy is the author of: Executive Orders...
注意
在 MySQLi 中最有用的方法之一是multi_query(),它允许您在同一语句中运行多个 SQL 命令。如果您想基于类似数据进行INSERT和UPDATE语句,您可以在一个方法调用中完成所有操作。
当然,我们只是浅尝辄止 MySQLi 类的功能。如果您查阅其文档,您将看到该类别的广泛方法列表,以及适当主题领域内的每个结果类别的详细记录。
SQLite
SQLite 是一个紧凑、高性能(适用于小数据集)的数据库,正如其名字所示,它是轻量级的。安装 PHP 时,SQLite 即可立即投入使用,因此如果它符合您的数据库需求,务必详细了解一下。
SQLite 中的所有数据库存储都是基于文件的,因此无需使用单独的数据库引擎就可以完成。如果您试图构建一个数据库占用空间小且除了 PHP 之外没有其他产品依赖的应用程序,这可能非常有利。要开始使用 SQLite,您只需在代码中引用它。
SQLite 还提供了面向对象的接口,因此您可以使用以下语句实例化一个对象:
$db = new SQLiteDatabase("library.sqlite");
这条语句的好处在于,如果找不到指定位置的文件,SQLite 会为您创建它。继续使用我们的library数据库示例,用于在 SQLite 中创建作者表并插入示例行的命令可能看起来像示例 9-2。
示例 9-2. SQLite 库作者表
$sql = "CREATE TABLE 'authors' ('authorid' INTEGER PRIMARY KEY, 'name' TEXT)";
if (!$database->queryExec($sql, $error)) {
echo "Create Failure - {$error}<br />";
} else {
echo "Table Authors was created <br />";
}
$sql = <<<SQL
INSERT INTO 'authors' ('name') VALUES ('J.R.R. Tolkien');
INSERT INTO 'authors' ('name') VALUES ('Alex Haley');
INSERT INTO 'authors' ('name') VALUES ('Tom Clancy');
INSERT INTO 'authors' ('name') VALUES ('Isaac Asimov');
SQL;
if (!$database->queryExec($sql, $error)) {
echo "Insert Failure - {$error}<br />";
} else {
echo "INSERT to Authors - OK<br />";
}
`Table` `Authors` `was` `createdINSERT` `to` `Authors` `-` `OK`
注意
在 SQLite 中,与 MySQL 不同,没有AUTO_INCREMENT选项。相反,SQLite 会使任何用INTEGER和PRIMARY KEY定义的列成为自动递增列。当执行INSERT语句时,您可以通过为列提供值来覆盖此默认行为。
请注意,这些数据类型与我们在 MySQL 中看到的相当不同。请记住,SQLite 是一个精简的数据库工具,因此它在数据类型上非常“轻量级”;请参阅表 9-1 获取其使用的数据类型列表。
表 9-1. SQLite 中可用的数据类型
| 数据类型 | 解释 |
|---|---|
| 文本 | 将数据存储为NULL、TEXT或BLOB内容。如果将数字提供给文本字段,则在存储之前将其转换为文本。 |
| 数值 | 可以存储整数或实数数据。如果提供文本数据,SQLite 尝试将信息转换为数值格式。 |
| 整数 | 表现与数值数据类型相同。但是,如果提供实数类型的数据,则将其存储为整数。这可能会影响数据存储的准确性。 |
| 实数 | 表现与数值数据类型相同,但会将整数值强制转换为浮点表示。 |
| None | 这是一个万能的数据类型;它不偏向于任何基本类型。数据被完全按照提供的方式存储。 |
在示例 9-3 中运行以下代码以创建书籍表并将一些数据插入到数据库文件中。
示例 9-3. SQLite 库书籍表
$db = new SQLiteDatabase("library.sqlite");
$sql = "CREATE TABLE 'books' ('bookid' INTEGER PRIMARY KEY,
'authorid' INTEGER,
'title' TEXT,
'ISBN' TEXT,
'pub_year' INTEGER,
'available' INTEGER,
)";
if ($db->queryExec($sql, $error) == FALSE) {
echo "Create Failure - {$error}<br />";
} else {
echo "Table Books was created<br />";
}
$sql = <<<SQL
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (1, 'The Two Towers', '0-261-10236-2', 1954, 1);
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (1, 'The Return of The King', '0-261-10237-0', 1955, 1);
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (2, 'Roots', '0-440-17464-3', 1974, 1);
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1);
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (4, 'Foundation', '0-553-80371-9', 1951, 1);
SQL;
if (!$db->queryExec($sql, $error)) {
echo "Insert Failure - {$error}<br />";
} else {
echo "INSERT to Books - OK<br />";
}
注意,我们可以同时执行多个 SQL 命令。我们也可以使用 MySQLi 来做到这一点,但您必须记住使用multi_query()方法;而在 SQLite 中,可以使用queryExec()方法来实现。在加载了一些数据到数据库后,请运行示例 9-4 中的代码。
示例 9-4. SQLite 选择书籍
$db = new SQLiteDatabase("c:/copy/library.sqlite");
$sql = "SELECT a.name, b.title FROM books b, authors a WHERE a.authorid=b.authorid";
$result = $db->query($sql);
while ($row = $result->fetch()) {
echo "{$row['a.name']} is the author of: {$row['b.title']}<br/>";
}
上述代码产生以下输出:
J.R.R. Tolkien is the author of: The Two Towers
J.R.R. Tolkien is the author of: The Return of The King
Alex Haley is the author of: Roots
Isaac Asimov is the author of: I, Robot
Isaac Asimov is the author of: Foundation
SQLite 几乎可以做到与“更大”的数据库引擎一样多的功能——“lite”并不是指其功能,而是指其对系统资源的需求较低。当您需要一个更便携且对资源要求较少的数据库时,您应始终考虑使用 SQLite。
注意
如果您刚开始接触 Web 开发的动态方面,您可以使用 PDO 与 SQLite 进行接口交互。这样,您可以从轻量级数据库开始,并在准备好时逐步转向更强大的 MySQL 数据库服务器。
直接文件级别操作
PHP 在其庞大的工具集中有许多隐藏的小特性。其中一个经常被忽视的特性是其处理复杂文件的不可思议能力。当然,每个人都知道 PHP 可以打开文件,但它究竟能做些什么呢?考虑以下示例,突显了其真正的可能性范围。本书的一位作者曾被一位“没钱”的潜在客户联系,但希望开发一个动态网络调查。当然,作者最初向客户展示了 PHP 与 MySQLi 数据库交互的奇迹。然而,在听到当地 ISP 的月费用后,客户问是否有其他(更便宜)的方法来完成工作。事实证明,如果您不想使用 SQLite,可以使用文件来管理和操作少量文本以便稍后检索。我们将在这里讨论的功能在单独使用时并不算特别——实际上,它们确实是每个人可能都熟悉的基本 PHP 工具集的一部分,正如您可以在表 9-2 中看到的那样。
表 9-2. 常用的 PHP 文件管理函数
| 函数名 | 使用描述 |
|---|---|
mkdir() | 用于在服务器上创建目录。 |
file_exists() | 用于确定提供位置是否存在文件或目录。 |
fopen() | 用于打开现有文件以供读取或写入(请查看正确使用的详细选项)。 |
fread() | 用于将文件内容读取到 PHP 变量中以供使用。 |
flock() | 用于在写入时对文件获取独占锁定。 |
fwrite() | 用于将变量的内容写入文件。 |
filesize() | 在读取文件时,用于确定一次读取多少字节。 |
fclose() | 用于一旦文件的有用性结束后关闭文件。 |
有趣的部分在于将所有功能绑定在一起以实现您的目标。例如,让我们创建一个涵盖两页问题的小型网络表单调查。用户可以输入一些意见,并在以后的日期返回以完成调查,从他们离开的地方继续。我们将勾勒出我们小应用程序的逻辑,并希望您能看到它的基本前提可以扩展到完整的生产类型的使用。
我们首先要做的是允许用户随时返回此调查并提供额外的输入。为此,我们需要一个唯一的标识符来区分每个用户。一般来说,一个人的电子邮件地址是唯一的(其他人可能知道并使用它,但这涉及网站安全和/或控制身份盗窃的问题)。为了简单起见,在这里我们假设使用电子邮件地址是诚实的,不必理会密码系统。因此,一旦我们获得了用户的电子邮件地址,我们需要将该信息存储在与其他网站访问者不同的位置。为此,我们将为服务器上的每个访问者创建一个目录文件夹(当然,这假设您可以访问并具有适当权限的服务器位置来允许文件的读写)。由于访客的电子邮件地址是相对唯一的标识符,我们将简单地使用该标识符命名新的目录位置。一旦我们创建了一个目录(测试用户是否从上一次会话返回),我们将读取任何已经存在的文件内容,并在 <textarea> 表单控件中显示它们,以便访客可以查看他或她之前写过的内容(如果有的话)。然后,在表单提交时保存访客的评论,并继续下一个调查问题。示例 9-5 展示了第一页的代码(这里包含 <?php 标签,因为在列表中的某些位置它们被打开和关闭)。
示例 9-5. 文件级访问
session_start();
if (!empty($_POST['posted']) && !empty($_POST['email'])) {
$folder = "surveys/" . strtolower($_POST['email']);
// send path information to the session
$_SESSION['folder'] = $folder;
if (!file_exists($folder)) {
// make the directory and then add the empty files
mkdir($folder, 0777, true);
}
header("Location: 08_6.php");
} else { ?>
<html>
<head>
<title>Files & folders - On-line Survey</title>
</head>
<body bgcolor="white" text="black">
<h2>Survey Form</h2>
<p>Please enter your e-mail address to start recording your comments</p>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
<input type="hidden" name="posted" value="1">
<p>Email address: <input type="text" name="email" size="45" /><br />
<input type="submit" name="submit" value="Submit"></p>
</form>
</body>
</html>
<?php }
图 9-1 显示了要求访客提交电子邮件地址的网页。
图 9-1. 调查登录界面
正如您所见,我们首先打开一个新的会话,以便将访客的信息传递给后续页面。然后我们测试确认代码中下方的表单是否已经提交,以及电子邮件地址字段是否有输入。如果测试失败,表单将简单地重新显示。当然,此功能的生产版本会发送错误消息告知用户输入有效文本。
一旦此测试通过(假设表单已正确提交),我们将创建一个$folder变量,其中包含我们要保存调查信息的目录结构,并将用户的电子邮件地址附加到其中;我们还将这个新创建的变量($folder)的内容保存到会话中以供以后使用。在这里,我们只是取出电子邮件地址并使用它(再次强调,如果这是一个安全站点,我们会采取适当的安全措施保护数据)。
接下来,我们要检查目录是否已经存在。如果不存在,我们使用mkdir()函数创建它。此函数接受路径和要创建的目录的名称作为参数,并尝试创建它。
注意
在 Linux 环境中,mkdir()函数有其他选项可以控制新创建的目录的访问级别和权限,因此如果适用于您的环境,请务必查阅这些选项。
在验证目录存在后,我们简单地将浏览器重定向到调查的第一页。
现在我们在调查的第一页(见图 9-2)上,表单已准备好使用了。
图 9-2. 调查的第一页
然而,这是一个动态生成的表单,正如您在示例 9-6 中所看到的。
示例 9-6. 文件级访问,继续
<?php
session_start();
$folder = $_SESSION['folder'];
$filename = $folder . "/question1.txt";
// open file for reading then clean it out
$file_handle = fopen($filename, "a+");
// pick up any text in the file that may already be there
$comments = file_get_contents($filename) ;
fclose($file_handle); // close this handle
if (!empty($_POST['posted'])) {
// create file if first time and then
//save text that is in $_POST['question1']
$question1 = $_POST['question1'];
$file_handle = fopen($filename, "w+");
// open file for total overwrite
if (flock($file_handle, LOCK_EX)) {
// do an exclusive lock
if (fwrite($file_handle, $question1) == FALSE) {
echo "Cannot write to file ($filename)";
}
// release the lock
flock($file_handle, LOCK_UN);
}
// close the file handle and redirect to next page ?
fclose($file_handle);
header( "Location: page2.php" );
} else { ?>
<html>
<head>
<title>Files & folders - On-line Survey</title>
</head>
<body>
<table border="0">
<tr>
<td>Please enter your response to the following survey question:</td>
</tr>
<tr bgcolor=lightblue>
<td>
What is your opinion on the state of the world economy?<br/>
Can you help us fix it ?
</td>
</tr>
<tr>
<td>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
<input type="hidden" name="posted" value="1"><br/>
<textarea name="question1" rows=12 cols=35><?= $comments ?></textarea>
</td>
</tr>
<tr>
<td><input type="submit" name="submit" value="Submit"></form></td>
</tr>
</table>
<?php } ?>
在这里,让我们突出几行代码,因为这是文件管理和操作真正发生的地方。在获取所需的会话信息并将文件名追加到$filename变量之后,我们就可以开始处理文件了。请记住,这个过程的目的是显示可能已保存在文件中的任何信息,并允许用户输入信息(或修改已输入的信息)。因此,在代码的顶部附近,您会看到以下命令:
$file_handle = fopen($filename, "a+");
使用文件打开函数fopen(),我们要求 PHP 为我们提供一个文件句柄,并将其存储在名为$file_handle的变量中。请注意,此处还向函数传递了另一个参数:a+选项。PHP 网站提供了这些选项字母的完整列表及其含义。a+选项使文件以读写方式打开,并将文件指针置于任何现有文件内容的末尾。如果文件不存在,PHP 将尝试创建它。查看接下来的两行代码,您会看到整个文件被读取(使用file_get_contents()函数)到$comments变量中,然后关闭文件:
$comments = file_get_contents($filename);
fclose($file_handle);
接下来,我们想看看此程序文件的表单部分是否已执行,如果是,我们必须保存输入到文本区域的任何信息。这一次,我们再次打开相同的文件,但使用w+选项,这会导致解释器只打开文件进行写入—如果文件不存在,则创建它,如果存在,则清空它。然后,文件指针被放置在文件的开头。本质上,我们想清空文件的当前内容,并用完全新的文本内容替换它。为此,我们使用fwrite()函数:
// do an exclusive lock
if (flock($file_handle, LOCK_EX)) {
if (fwrite($file_handle, $question1) == FALSE){
echo "Cannot write to file ($filename)";
}
// release the lock
flock($file_handle, LOCK_UN);
}
我们必须确保这些信息确实保存到指定的文件中,因此我们在文件写入操作周围包裹了几个条件语句,以确保一切顺利进行。首先,我们尝试在问题文件上获得独占锁定(使用flock()函数);这将确保在我们操作文件时,没有其他进程可以访问该文件。写入完成后,我们释放文件上的锁定。这只是一种预防措施,因为文件管理是基于第一个网页表单中输入的电子邮件地址,并且每个调查都有自己的文件夹位置,因此除非两个人恰好使用相同的电子邮件地址,否则不应发生使用冲突。
正如你所见,文件写入函数使用$file_handle变量将$question1变量的内容添加到文件中。然后当我们完成后,我们简单地关闭文件,转到调查的下一页,如图 9-3 所示。
图 9-3. 调查第二页
正如你在例子 9-7 中所见,处理这个文件(称为question2.txt)的代码与前一个代码相同,除了文件名不同。
例子 9-7. 文件级访问,续
<?php
session_start();
$folder = $_SESSION['folder'];
$filename = $folder . "/question2.txt" ;
// open file for reading then clean it out
$file_handle = fopen($filename, "a+");
// pick up any text in the file that may already be there
$comments = fread($file_handle, filesize($filename));
fclose($file_handle); // close this handle
if ($_POST['posted']) {
// create file if first time and then save
//text that is in $_POST['question2']
$question2 = $_POST['question2'];
// open file for total overwrite
$file_handle = fopen($filename, "w+");
if(flock($file_handle, LOCK_EX)) { // do an exclusive lock
if(fwrite($file_handle, $question2) == FALSE) {
echo "Cannot write to file ($filename)";
}
flock($file_handle, LOCK_UN); // release the lock
}
// close the file handle and redirect to next page ?
fclose($file_handle);
header( "Location: last_page.php" );
} else { ?>
<html>
<head>
<title>Files & folders - On-line Survey</title>
</head>
<body>
<table border="0">
<tr>
<td>Please enter your comments to the following survey statement:</td>
</tr>
<tr bgcolor="lightblue">
<td>It's a funny thing freedom. I mean how can any of us <br/>
be really free when we still have personal possessions.
How do you respond to the previous statement?</td>
</tr>
<tr>
<td>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method=POST>
<input type="hidden" name="posted" value="1"><br/>
<textarea name="question2" rows="12" cols="35"><?= $comments ?></textarea>
</td>
</tr>
<tr>
<td><input type="submit" name="submit" value="Submit"></form></td>
</tr>
</table>
<?php } ?>
这种文件处理可以持续进行,时间长短由你决定,因此你的调查可以无限延续。为了增加趣味性,你可以在同一页上询问多个问题,并简单地给每个问题分配一个独立的文件名。这里要指出的唯一独特项是,一旦提交了此页面并存储了文本,它将被重定向到一个名为last_page.php的 PHP 文件。这个页面不包含在代码示例中,因为它仅仅是感谢用户填写调查的页面。
当然,在几页之后,每页多达五个问题,你可能会发现自己有大量的单个文件需要管理。幸运的是,PHP 还有其他文件处理函数供你使用。例如,file()函数是fread()函数的替代品,它将文件的整个内容读取到一个数组中,每行一个元素。如果你的信息格式正确——每行以换行符\n结尾——那么你可以非常容易地将多个信息存储在单个文件中。当然,这也需要使用适当的循环控制来处理 HTML 表单的创建以及记录表单中的条目。
当涉及文件处理时,你还可以在 PHP 网站上查看更多选项。如果你访问“文件系统”,你会找到一个包含超过 70 个函数的列表——当然,这里讨论的函数也包括在内。你可以使用is_readable()或is_writable()函数检查文件是否可读或可写。你可以检查文件权限、空闲磁盘空间或总磁盘空间,你可以删除文件、复制文件等等。归根结底,如果你有足够的时间和愿望,甚至可以编写一个完整的 Web 应用程序而不需要或使用数据库系统。
当有一天到来,这种情况很可能会发生,即你遇到一位客户并不想花大笔钱使用数据库引擎时,你可以提供一种替代方案。
MongoDB
我们将要看的最后一种数据库类型是 NoSQL 数据库。NoSQL 数据库因其在系统资源上的轻量级特性而备受青睐,更重要的是,它们不受传统 SQL 命令结构的限制。NoSQL 数据库也因同样两个原因而在移动设备如平板电脑和智能手机中变得越来越流行。
NoSQL 数据库世界的佼佼者之一就是 MongoDB。在这里我们只会浅尝 MongoDB 的表面,只是为了让你体验一下它的潜力。如需更详细的内容,请参阅《MongoDB 与 PHP》(O’Reilly)由 Steve Francia 编著。
关于 MongoDB,首先要理解的是它不是传统的数据库。它有自己的设置和术语。习惯与它一起工作会花费传统 SQL 数据库用户一些时间。表 9-3 试图与“标准”SQL 术语进行一些类比。
表 9-3. 典型的 MongoDB/SQL 对应关系
| 传统 SQL 术语 | MongoDB 术语 |
|---|---|
| 数据库 | 数据库 |
| 表格 | 集合 |
| 行 | 文档。没有关联,不像数据库的“行”;相反,可以看作是数组。 |
在 MongoDB 范例中,没有确切等价的数据库行。在集合内部处理数据的最佳方式之一是将其视为多维数组,稍后我们将重新调整我们的 library 数据库示例时会看到。
如果你只是想在本地主机上尝试 MongoDB(推荐用于熟悉它),你可以使用诸如Zend Server CE之类的一体化工具来设置本地环境,并安装了 Mongo 驱动程序。你仍然需要从MongoDB 网站下载服务器本身,并按照说明为你自己的本地环境设置数据库服务器引擎。
一个非常有用的基于 Web 的工具,用于浏览 MongoDB 数据并操作集合和文档,是Genghis。只需下载项目并将其放入本地主机的自己文件夹中,然后调用 genghis.php。如果数据库引擎正在运行,它将被检测到并显示给你(见图 9-4)。
图 9-4. Genghis MongoDB web interface 示例
现在让我们来看一些示例代码。看看示例 9-8,看看 Mongo 数据库正在形成的开端。
示例 9-8. MongoDB 库
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;
$author = array('authorid' => 1, 'name' => "J.R.R. Tolkien");
$authors->insert($author);
$author = array('authorid' => 2, 'name' => "Alex Haley");
$authors->insert($author);
$author = array('authorid' => 3, 'name' => "Tom Clancy");
$authors->save($author);
$author = array('authorid' => 4, 'name' => "Isaac Asimov");
$authors->save($author);
第一行创建了与 MongoDB 引擎的新连接,并创建了一个对象接口。接下来的行连接到 library “集合”;如果此集合不存在,Mongo 将为您创建它(因此在 Mongo 中不需要预先创建集合)。然后,我们使用 $db 连接到 library 数据库创建了一个对象接口,并创建了一个集合,用于存储我们的作者数据。接下来的四组代码将文档添加到 authors 集合中,使用了两种不同的方法。前两个示例使用 insert() 方法,后两个示例使用 save() 方法。这两种方法之间唯一的区别是,save() 方法会更新已存在的文档值,如果存在 _id 键的话(稍后详细讨论 _id)。
在浏览器中执行此代码,你应该会看到图 9-5 中显示的样本数据。正如你所见,一个名为 _id 的实体被创建并与插入的数据关联。这是自动分配给所有创建的集合的主键。如果我们想依赖这个键——除了显而易见的复杂性外——我们不必在前面的代码中添加自己的 authorid 信息。
图 9-5. 作者 Mongo 文档数据示例
检索数据
数据存储后,我们现在可以开始查看访问它的方法。示例 9-9 展示了其中一种选项。
示例 9-9. MongoDB 数据选择示例
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;
$data = $authors->findone(array('authorid' => 4));
echo "Generated Primary Key: {$data['_id']}<br />";
echo "Author name: {$data['name']}";
前三行代码与之前相同,因为我们仍然想连接到同一个数据库,并利用同一个集合(library)和文档(authors)。之后,我们使用 findone() 方法,将其传递给一个包含可用于查找我们想要的信息的唯一数据片段的数组——在本例中是 Isaac Asimov 的 authorid,即 4。我们将返回的信息存储到一个名为 $data 的数组中。
注意
作为一个很好的简化,您可以将 Mongo 文档中的信息视为基于数组的。
然后,我们可以根据需要使用该数组来显示文档返回的数据。以下是前面代码的结果输出。注意 Mongo 创建的主键大小。
Generated Primary Key: 4ff43ef45b9e7d300c000007
Author name: Isaac Asimov
插入更复杂的数据
接下来,我们希望继续通过向文档添加一些书籍来扩展我们的 library 示例数据库,这些书籍与特定作者有关。这是不同数据库中不同表的类比可以崩溃的地方。考虑示例 9-10,它向 authors 文档添加了四本书,实质上是作为多维数组。
示例 9-10. MongoDB 简单数据更新/插入
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;
$authors->update(
array('name' => "Isaac Asimov"),
array('$set' =>
array('books' =>
array(
"0-425-17034-9" => "Foundation",
"0-261-10236-2" => "I, Robot",
"0-440-17464-3" => "Second Foundation",
"0-425-13354-0" => "Pebble In The Sky",
)
)
)
);
在此之后,我们通过建立必要的连接,使用 update() 方法,并使用数组的第一个元素(update() 方法的第一个参数)作为唯一的查找标识符,并使用一个称为 $set 的定义运算符作为第二个参数,将书籍数据附加到提供的第一个参数的键上。
注意
在生产环境中使用这些特殊操作符 $set 和 $push(本文未涉及)之前,您应该进行调查并完全理解它们。请参阅MongoDB 文档 以获取更多信息和这些操作符的完整列表。
示例 9-11 提供了另一种实现相同目标的方法,不同之处在于我们提前准备要插入和附加的数组,并使用 Mongo 创建的 _id 作为位置键。
示例 9-11. MongoDB 数据更新/插入
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;
$data = $authors->findone(array('name' => "Isaac Asimov"));
$bookData = array(
array(
"ISBN" => "0-553-29337-0",
"title" => "Foundation",
"pub_year" => 1951,
"available" => 1,
),
array(
"ISBN" => "0-553-29438-5",
"title" => "I, Robot",
"pub_year" => 1950,
"available" => 1,
),
array(
"ISBN" => "0-517-546671",
"title" => "Exploring the Earth and the Cosmos",
"pub_year" => 1982,
"available" => 1,
),
array(
"ISBN' => "0-553-29336-2",
'title" => "Second Foundation",
"pub_year" => 1953,
"available" => 1,
),
);
$authors->update(
array("_id" => $data["_id"]),
array("$set" => array("books" => $bookData))
);
在我们之前的两个代码示例中,我们没有向书籍数据数组添加任何键。我们可以这样做,但是允许 Mongo 将该数据管理为多维数组同样简单。图 9-6 显示了当在 Genghis 中显示来自示例 9-11 的数据时的样子。
图 9-6. 向作者添加书籍数据
示例 9-12 展示了我们的 Mongo 数据库中存储的更多数据。它在示例 9-9 的基础上再添加了几行代码;在这里,我们引用了前一段代码中插入书籍详细信息的自动生成的自然键。
示例 9-12. MongoDB 数据查找和显示
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;
$data = $authors->findone(array("authorid" => 4));
echo "Generated Primary Key: {$data['_id']}<br />";
echo "Author name: {$data['name']}<br />";
echo "2nd Book info - ISBN: {$data['books'][1]['ISBN']}<br />";
echo "2nd Book info - Title: {$data['books'][1]['title']<br />";
前面代码生成的输出如下(请记住数组是从零开始的):
Generated Primary Key: 4ff43ef45b9e7d300c000007
Author name: Isaac Asimov
2nd Book info - ISBN: 0-553-29438-5
2nd Book info - Title: I, Robot
关于如何在 PHP 中使用和操作 MongoDB 的更多信息,请参阅PHP 网站上的文档。
接下来的内容
在下一章中,我们将探讨在由 PHP 生成的页面中包含图形媒体的各种技术,以及在 Web 服务器上动态生成和操作图形的方法。