PHP8-解决方案-六-

137 阅读39分钟

PHP8 解决方案(六)

原文:PHP 8 Solutions

协议:CC BY-NC-SA 4.0

十三、使用 PHP 和 SQL 连接到数据库

PHP 8 提供了两种连接和交互 MySQL 数据库的方式:MySQL 改进版(MySQLi)和 PHP 数据对象(PDO)。您选择哪一个是一个重要的决定,因为它们使用不兼容的代码。您不能在同一个数据库连接中混合使用它们。同样重要的是,不要将 MySQL 与最初的 MySQL 扩展混淆,后者不再受支持。在大多数情况下,MySQLi 函数名称的唯一区别是添加了字母 i (例如,mysqli_query()而不是mysql_query())。然而,参数的顺序通常是不同的,所以转换一个旧的脚本不仅仅是在函数名中插入一个 i

顾名思义,MySQL 是专门为与 MySQL 协同工作而设计的。它也完全兼容 MariaDB。另一方面,PDO 是数据库系统中立的。至少在理论上,只需修改几行 PHP 代码,就可以将网站从 MySQL 切换到 Microsoft SQL Server 或不同的数据库系统。实际上,您通常需要至少重写一些 SQL 查询,因为每个数据库供应商都在标准 SQL 的基础上添加了自定义函数。

我个人的偏好是用 PDO;但是为了完整起见,剩下的章节涵盖了 MySQLi 和 PDO。如果你想只关注其中一个,就忽略与另一个相关的部分。尽管您使用 PHP 连接到数据库并存储任何结果,但数据库查询需要用 SQL 编写。本章教你检索存储在表中的信息的基本知识。

在本章中,我们将介绍以下内容:

  • 用 MySQL 和 PDO 连接到 MySQL 和 MariaDB

  • 计算表中的记录数

  • 使用选择查询检索数据并将其显示在网页上

  • 使用准备好的语句和其他技术保护数据安全

检查您的远程服务器设置

XAMPP 和 MAMP 都支持 MySQLi 和 PDO,但是你需要检查你的远程服务器的 PHP 配置来验证它提供的支持程度。在您的远程服务器上运行phpinfo(),向下滚动配置页面,并查找以下部分。它们是按字母顺序排列的,所以您需要向下滚动很长一段距离才能找到它们:

img/332054_5_En_13_Figa_HTML.jpg

所有托管公司都要有第一节(mysqli)。如果只列出了mysql(没有最后的 i ),那么你的服务器已经过时了。让你的托管公司尽快把你转移到运行最新版本 PHP 8.x 的服务器上(你可以在 https://php.net/supported-versions.php 查看目前支持哪些版本的 PHP)。如果您计划使用 PDO,您不仅需要检查 PDO 是否已启用,还必须确保pdo_mysql已列出。PDO 要求每种类型的数据库使用不同的驱动程序。

PHP 如何与数据库通信

不管你使用 MySQLi 还是 PDO,这个过程总是遵循这个顺序:

  1. 使用主机名、用户名、口令和数据库名连接到数据库。

  2. 准备一个 SQL 查询。

  3. 执行查询并保存结果。

  4. 从结果中提取数据(通常使用循环)。

用户名和密码是您在第十二章中创建的帐户的用户名和密码,或者是您的托管公司给您的帐户的用户名和密码。但是主机名呢?在本地测试环境中,它是localhost。令人惊讶的是,即使在远程服务器上也经常是localhost。这是因为在许多情况下,数据库服务器与您的网站位于同一服务器上。换句话说,显示页面的 web 服务器和数据库服务器位于彼此的本地。然而,如果数据库服务器在一台单独的机器上,你的主机公司会告诉你使用的地址。重要的一点是,主机名是而不是,通常与你网站的域名相同。

让我们快速看一下如何使用每种方法连接到数据库。

连接 MySQL 改进的扩展

MySQLi 有两个接口:过程的和面向对象的。过程接口的设计是为了简化从最初的 MySQL 函数的转换。因为面向对象的版本更紧凑,所以这里采用的就是这个版本。

要连接到 MySQL 或 MariaDB,可以通过向构造函数方法传递四个参数来创建一个mysqli对象:主机名、用户名、密码和数据库名称。这是您连接到phpsols数据库的方式:

$conn = new mysqli($hostname, $username, $password, 'phpsols');

这将连接对象存储为$conn

如果您的数据库服务器使用非标准端口,您需要将端口号作为第五个参数传递给mysqli构造函数。

Tip

MAMP 使用套接字连接 MySQL,所以即使 MySQL 正在监听端口 8889,也不需要添加端口号。这适用于 MySQLi 和 PDO。

连接 PDO

PDO 需要一个稍微不同的方法。最重要的区别是,如果连接失败,PDO 会抛出一个异常。如果您没有捕捉到异常,调试信息会显示所有的连接细节,包括您的用户名和密码。因此,您需要将代码包装在一个try块中,并捕捉异常以防止敏感信息被显示。

PDO构造函数方法的第一个参数是一个数据源名称 (DSN)。这是一个由 PDO 驱动程序名称后跟一个冒号,再后跟特定于 PDO 驱动程序的连接详细信息组成的字符串。

要连接到 MySQL 或 MariaDB,DSN 需要采用以下格式:

'mysql:host=hostname;dbname=databaseName'

如果您的数据库服务器使用非标准端口,DSN 也应该包含端口号,如下所示:

'mysql:host=hostname;port=portNumber;dbname=databaseName'

在 DSN 之后,您将用户名和密码传递给PDO()构造函数方法。因此,连接到phpsols数据库的代码如下所示:

try {
 $conn = new PDO("mysql:host=$hostname;dbname=phpsols", $username, $password);
} catch (PDOException $e) {
 echo $e->getMessage();
}

在测试期间使用echo来显示异常产生的消息是可以接受的,但是当你在一个活动的网站上部署脚本时,你需要将用户重定向到一个错误页面,如 PHP 解决方案 5-9 中所述。

Tip

为了连接到不同的数据库系统,DSN 是 PHP 代码中唯一需要修改的部分。所有剩余的 PDO 代码都是完全数据库中立的。有关如何为 PostgreSQL、Microsoft SQL Server、SQLite 和其他数据库系统创建 DSN 的详细信息,请访问 www.php.net/manual/en/pdo.drivers.php

PHP 解决方案 13-1:制作一个可重用的数据库连接器

连接到数据库是一项日常工作,从现在开始需要在每个页面中执行。这个 PHP 解决方案创建了一个存储在连接到数据库的外部文件中的简单函数。它主要是为测试剩余章节中不同的 MySQLi 和 PDO 脚本而设计的,不需要每次都重新输入连接细节,也不需要在不同的连接文件之间切换。

  1. includes文件夹中创建一个名为connection.php的文件,并插入以下代码(在ch13文件夹中有一份完整脚本的副本):
<?php
function dbConnect($usertype, $connectionType = 'mysqli') {
    $host = 'localhost';
    $db = 'phpsols';
    if ($usertype == 'read') {
        $user = 'psread';
        $pwd = 'K1yoMizu^dera';
    } elseif ($usertype == 'write') {
        $user = 'pswrite';
        $pwd = '0Ch@Nom1$u';
    } else {
        exit('Unrecognized user');
    }
    // Connection code goes here
}

该函数有两个参数:用户类型和连接类型。第二个参数默认为mysqli。如果您想专注于使用 PDO,请将第二个参数的默认值设置为pdo

函数中的前两行存储了您想要连接的主机服务器和数据库的名称。

条件语句检查第一个参数的值,并根据需要在psreadpswrite用户名和密码之间切换。如果用户帐户未被识别,exit()功能会暂停脚本并显示Unrecognized user

  1. 用以下内容替换Connection code goes here注释:
if ($connectionType == 'mysqli') {
    $conn = @ new mysqli($host, $user, $pwd, $db);
    if ($conn->connect_error) {
        exit($conn->connect_error);
    }
    return $conn;
} else {
    try {
        return new PDO("mysql:host=$host;dbname=$db", $user, $pwd);
    } catch (PDOException $e) {
        echo $e->getMessage();
    }
}

如果第二个参数设置为mysqli,就会创建一个名为$conn的 MySQLi 连接对象。错误控制运算符(@)防止构造函数方法显示错误消息。如果连接失败,原因存储在对象的connect_error属性中。如果为空,则被视为false,因此跳过下一行,返回$conn对象。但是如果有问题,exit()会显示connect_error的值,并暂停脚本。

否则,该函数返回一个 PDO 连接对象。没有必要在PDO构造函数中使用错误控制操作符,因为如果有问题,它会抛出一个PDOExceptioncatch块使用异常的getMessage()方法来显示问题的原因。

Tip

如果您的数据库服务器使用非标准端口,不要忘记将端口号作为第五个参数添加到mysqli()构造函数中,并将其包含在 PDO DSN 中,如前面几节所述。如果数据库使用套接字连接,这是不必要的,这在 macOS 和 Linux 上很常见。

  1. phpsols站点根文件夹中创建一个名为connection_test.php的文件,并插入以下代码:
<?php
require_once './includes/connection.php';
if ($conn = dbConnect('read')) {
    echo 'Connection successful';
}

这包括连接脚本,并用psread用户帐户和 MySQLi 进行测试。

  1. 保存页面并将其加载到浏览器中。如果你看到Connection successful,一切都好。如果收到错误消息,请参考下一节中的故障排除提示。

  2. 测试与pswrite用户和 MySQLi 的连接:

    if ($conn = dbConnect('write')) {
        echo 'Connection successful';
    }
    
    
  3. 通过将'pdo'作为第二个参数添加到dbConnect()来测试 PDO 的两个用户帐户。

  4. 假设一切顺利,您就可以开始与phpsols数据库交互了。如果您遇到了问题,请查看下一部分。

数据库连接问题疑难解答

连接数据库时最常见的失败原因是用户名或密码错误。密码和用户名区分大小写。仔细检查拼写。例如,下面的截图显示了将psread改为Psread会发生什么:

img/332054_5_En_13_Figb_HTML.jpg

访问被拒绝,因为没有这样的用户。用户名的第一个大写字母非常重要。但是,即使用户名是正确的,您也可能会得到相同的错误消息,如下所示:

img/332054_5_En_13_Figc_HTML.jpg

这完全让很多人困惑。错误信息确认您正在使用密码。那么,为什么访问被拒绝呢?密码不对。这就是原因。

如果错误信息显示using password: NO,这意味着您忘记了提供密码。短语using password是问题与登录凭证有关的线索。

如果缺少该短语,则表明存在不同的问题,如下一个屏幕截图所示:

img/332054_5_En_13_Figd_HTML.jpg

这里的问题是数据库的名称不正确。如果你拼错了主机,你会得到一个消息,没有这样的主机是已知的。

本节截图由 MySQLi 生成。PDO 生成相同的消息,但也包括错误号和代码。

净化来自数据库的文本结果

当显示 SQL 查询的结果时,您可以确信存储在某些类型的列中的值将采用特定的格式。例如,数字列类型只能存储数字。类似地,与日期和时间相关的列仅以 ISO 日期-时间格式存储值。然而,与文本相关的列可以存储任何类型的字符串,包括 HTML、JavaScript 和其他可执行代码。当从与文本相关的列中输出值时,您应该始终对它们进行清理,以防止执行任意代码。

净化文本输出的简单方法是将其传递给htmlspecialchars()。这个函数与htmlentities()相关,但是它将更有限范围的字符转换成等价的 HTML 字符实体。具体来说,它转换&符号、引号和尖括号;但是它保留了句点(点)不变。这具有中和在浏览器中显示代码时执行代码的企图的效果,因为<script>和 PHP 标签的尖括号被转换了。重要的是不要转换点,因为它们用在我们想要显示的文件名中。

htmlspecialchars()的缺点是,默认情况下,它对现有的字符实体进行双重编码。结果,&被转换为&amp;。您可以通过将命名参数double_encode传递给htmlspecialchars()并将其值设置为false来关闭这个默认行为。

每次您想要调用htmlspecialchars()时,键入指定的参数是乏味的。因此,我在ch13文件夹中的一个名为utility_funcs.php的文件中定义了以下自定义函数:

function safe($text) {
    return htmlspecialchars($text, double_encode: false);
}

这只是将$text传递给htmlspecialchars(),设置可选参数,并返回结果。将utility_funcs.php复制到includes文件夹,并将其包含在从数据库输出文本的脚本中。

作为对htmlspecialchars()的替代,您可以将文本值传递给strip_tags(),这允许您指定允许的 HTML 标签(参见第七章中的“访问远程文件”)。

查询数据库并显示结果

在尝试显示数据库查询的结果之前,最好先了解有多少个结果。如果没有任何结果,您将没有什么可显示的。这对于创建一个浏览一长串结果的导航系统也是必要的(你将在下一章学习如何做)。在用户认证中(在第十九章中介绍),搜索用户名和密码时没有结果意味着登录应该失败。

MySQLi 和 PDO 使用不同的方法来计算和显示结果。接下来的两个 PHP 解决方案展示了如何用 MySQLi 实现这一点。对于 PDO,直接跳到 PHP 解决方案 13-4。

PHP 解决方案 13-2:计算结果集中的记录数(MySQLi)

这个 PHP 解决方案展示了如何提交一个 SQL 查询来选择images表中的所有记录,并将结果存储在一个MySQLi_Result对象中。对象的num_rows属性包含查询检索到的记录数。

  1. php8sols站点根目录下创建一个名为mysqli的新文件夹,然后在文件夹内创建一个名为mysqli.php的新文件。该页面最终将用于显示一个表格,因此它应该有一个DOCTYPE声明和一个 HTML 框架。

  2. 将连接文件包含在 PHP 块中的DOCTYPE声明之上,并使用具有只读权限的帐户连接到phpsols数据库,如下所示:

    require_once '../includes/connection.php';
    $conn = dbConnect('read');
    
    
  3. 接下来,准备 SQL 查询。在前一步之后(但在结束 PHP 标记之前)立即添加以下代码:

$sql = 'SELECT * FROM images';

这意味着“从images表中选择所有内容”星号(*)是“所有列”的简写

  1. 现在通过调用 connection 对象上的query()方法执行查询,并将 SQL 查询作为参数传递,如下所示:
                  $result = $conn->query($sql);

结果存储在一个变量中,我将它富有想象力地命名为$result

  1. 如果有问题,$result就会是false。为了找出问题所在,我们需要获取错误消息,该消息存储为mysqli连接对象的error属性。在前一行之后添加以下条件语句:

  2. 假设没有问题,$result现在持有一个MySQLi_Result对象,该对象有一个名为num_rows的属性。要获得查询找到的记录数,向条件语句添加一个else块,并将值赋给一个变量,如下所示:

    if (!$result) {
        $error = $conn->error;
    } else {
       $numRows = $result->num_rows;
    }
    
    
  3. 现在,您可以在页面正文中显示结果,如下所示:

    <?php
    if (isset($error)) {
        echo "<p>$error</p>";
    } else {
        echo "<p>A total of $numRows records were found.</p>";
    }
    ?>
    
    
if (!$result) {
    $error = $conn->error;
}

如果有问题,$error将被设置,所以它被显示。否则,else块显示找到的记录数。两个字符串都嵌入了变量,所以它们用双引号括起来。

  1. 保存mysqli.php并将其加载到浏览器中。您应该会看到以下结果:

img/332054_5_En_13_Fige_HTML.jpg

如有必要,用ch13文件夹中的mysqli_01.php检查您的代码。

PHP 解决方案 13-3:使用 MySQLi 显示图像表

显示SELECT查询结果的最常见方式是使用循环从结果集中一次提取一行。MySQLi_Result有一个名为fetch_assoc()的方法,该方法以关联数组的形式检索当前行,以便显示在网页上。数组中的每个元素都以表格中相应的列命名。

这个 PHP 解决方案展示了如何遍历一个MySQLi_Result对象来显示一个SELECT查询的结果。继续使用 PHP 解决方案 13-2 中的文件。

  1. utility_funcs.phpch13文件夹复制到includes文件夹,并将其包含在脚本的顶部:

    require_once '../includes/connection.php';
    require_once '../includes/utility_funcs.php';
    
    
  2. 移除页面主体中else块末尾的右花括号(应该在第 24 行左右)。尽管显示images表的大部分代码是 HTML,但它需要在else块中。

  3. 在结束 PHP 标记后插入一个空行,并在单独的 PHP 代码块中的下一行添加结束括号。修改后的代码应该如下所示:

  4. mysqli.php的主体中的两个 PHP 块之间添加下表,以便它由else块控制。这样做的原因是为了防止 SQL 查询失败时出错。显示结果集的 PHP 代码以粗体突出显示:

               } else {
echo "<p>A total of $numRows records were found.</p>";
              ?>
<?php } ?>
               </body>

<table>
     <tr>
         <th>image_id</th>
         <th>filename</th>
         <th>caption</th>
     </tr>
 <?php while ($row = $result->fetch_assoc()) { ?>
     <tr>
         <td><?= $row['image_id'] ?></td>
         <td><?= safe($row['filename']) ?></td>
         <td><?= safe($row['caption']) ?></td>
     </tr>
 <?php } ?>
</table>

提示while循环遍历数据库结果,使用fetch_assoc()方法将每条记录提取到$row中。$row的每个元素都显示在一个表格单元中。循环继续,直到fetch_assoc()到达结果集的末尾。

没有必要净化image_id的值,因为它在一个只存储整数的列中。

  1. 保存mysqli.php并在浏览器中查看。您应该会看到显示的images表的内容,如下图所示:

img/332054_5_En_13_Figf_HTML.jpg

如有必要,您可以将您的代码与ch13文件夹中的mysql_02.php进行比较。

MySQLi 连接婴儿床表

表 13-1 总结了 MySQLi 的连接和数据库查询的基本细节。

表 13-1

通过 MySQL 改进的面向对象接口连接到 MySQL/MariaDB

|

行动

|

使用

|

评论

| | --- | --- | --- | | 连接 | $conn = new mysqli($h,$u,$p,$d); | 所有参数都是可选的;实践中总是需要前四个:主机名、用户名、密码、数据库名。创建连接对象。 | | 选择数据库 | $conn->select_db('dbName'); | 用于选择不同的数据库。 | | 提交查询 | $result = $conn->query($sql); | 返回结果对象。 | | 计数结果 | $numRows = $result->num_rows; | 返回结果对象中的行数。 | | 提取记录 | $row = $result->fetch_assoc(); | 从结果对象中提取当前行作为关联数组。 | | 提取记录 | $row = $result->fetch_row(); | 从结果对象中提取当前行作为索引(编号)数组。 |

PHP 解决方案 13-4:计算结果集中的记录数(PDO)

PDO 没有直接等同于 MySQLi num_rows的属性。对于大多数数据库,您需要执行一个 SQL 查询来计算表中的项数,然后获取结果。然而,PDO rowCount()方法实现了 MySQL 和 MariaDB 的双重目的。通常,它只报告受插入、更新或删除记录影响的行数,但是对于 MySQL 和 MariaDB,它还报告由SELECT查询找到的记录数。

  1. php8sols站点中创建一个名为pdo的新文件夹。然后在刚刚创建的文件夹中创建一个名为pdo.php的文件。该页面最终将用于显示一个表格,因此它应该有一个DOCTYPE声明和一个 HTML 框架。

  2. 将连接文件包含在 PHP 块中的DOCTYPE声明之上,然后使用只读帐户创建一个到phpsols数据库的 PDO 连接,如下所示:

    require_once '../includes/connection.php';
    $conn = dbConnect('read', 'pdo');
    
    
  3. 接下来,准备 SQL 查询:

$sql = 'SELECT * FROM images';

这意味着“选择images表中的每一条记录”星号(*)是“所有列”的简写

  1. 现在执行查询并将结果存储在一个变量中,如下所示:

  2. 要检查查询是否有问题,可以使用 connection 对象的errorInfo()方法从数据库中获取一组错误信息。数组的第三个元素包含出错时问题的简要描述。添加以下代码:

$result = $conn->query($sql);

$error = $conn->errorInfo()[2];

我们只对第三个元素感兴趣,所以我们可以使用在 PHP 解决方案 7-1 中遇到的数组解引用技术(“获取文本文件的内容”),方法是在调用$conn->errorInfo()后立即在一对方括号中添加数组索引,并将值赋给$error

  1. 如果查询执行成功,$error将是null,PHP 将其视为false。因此,如果没有错误,我们可以通过调用$result对象上的rowCount()方法来获得结果集中的行数,如下所示:

    if (!$error) {
        $numRows = $result->rowCount();
    }
    
    
  2. 现在,您可以在页面正文中显示查询结果,如下所示:

    <?php
    if ($error) {
        echo "<p>$error</p>";
    } else {
        echo "<p>A total of $numRows records were found.</p>";
    }
    ?>
    
    
  3. 保存页面并将其加载到浏览器中。你应该会看到和 PHP 解决方案 13-2 的第 8 步相同的结果。如有必要,用pdo_01.php检查您的代码。

统计其他数据库中 PDO 的记录

使用 PDO rowCount()来报告一个SELECT查询找到的条目数量,这在 MySQL 和 MariaDB 上都适用,但不能保证在所有其他数据库上都适用。如果rowCount()不起作用,用下面的代码代替:

// prepare the SQL query
$sql = 'SELECT COUNT(*) FROM images';
// submit the query and capture the result
$result = $conn->query($sql);
$error = $conn->errorInfo()[2];
if (!$error) {
    // find out how many records were retrieved
    $numRows = $result->fetchColumn();
    // free the database resource
    $result->closeCursor();
}

这使用带星号的 SQL COUNT()函数来计算表中的所有项目。只有一个结果,所以可以用fetchColumn()方法检索,该方法从数据库结果中获取第一列。在将结果存储在$numRows中之后,您必须调用closeCursor()方法来释放数据库资源以供任何进一步的查询使用。

PHP 解决方案 13-5:使用 PDO 显示图像表

要用 PDO 显示一个SELECT查询的结果,可以在一个foreach循环中使用query()方法来提取当前行作为一个关联数组。数组中的每个元素都以表格中相应的列命名。

继续使用与前面的 PHP 解决方案相同的文件。

  1. utility_funcs.phpch13文件夹复制到includes文件夹,并将其包含在脚本的顶部:

    require_once '../includes/connection.php';
    require_once '../includes/utility_funcs.php';
    
    
  2. 移除页面主体中else块末尾的右花括号(应该在第 26 行左右)。尽管显示images表的大部分代码是 HTML,但它需要在else块中。

  3. 在结束 PHP 标记后插入一个空行,然后在单独的 PHP 代码块中的下一行添加右括号。修改后的代码应该如下所示:

  4. pdo.php的主体中的两个 PHP 块之间添加下表,以便它由else块控制。这是为了防止 SQL 查询失败时出错。显示结果集的 PHP 代码以粗体显示:

                   } else {
    echo "<p>A total of $numRows records were found.</p>";
?>
<?php } ?>
                   </body>

  1. 保存页面并在浏览器中查看。它应该看起来像 PHP 解决方案 13-3 中的截图。您可以将您的代码与ch13文件夹中的pdo_02.php进行比较。
<table>
     <tr>
         <th>image_id</th>
         <th>filename</th>
         <th>caption</th>
     </tr>
 <?php foreach ($conn->query($sql) as $row) { ?>
     <tr>
         <td><?= $row['image_id'] ?></td>
         <td><?= safe($row['filename']) ?></td>
         <td><?= safe($row['caption']) ?></td>
     </tr>
 <?php } ?>
</table>

PDO 连接婴儿床表

表 13-2 总结了与 PDO 连接和数据库查询的基本细节。有些命令将在后面的章节中使用,但为了便于参考,在此列出。

表 13-2

与 PDO 的数据库连接

|

行动

|

使用

|

评论

| | --- | --- | --- | | 连接 | $conn = new PDO($DSN,$u,$p); | 在实践中,需要三个参数:数据源名称(DSN)、用户名和密码。必须包装在 try/catch 块中。 | | 提交SELECT查询 | $result = $conn->query($sql); | 将结果作为一个PDOStatement对象返回。 | | 提取记录 | foreach($conn->query($sql) as $row) { | 提交SELECT查询并在单个操作中获取当前行作为关联数组。 | | 计数结果 | $numRows = $result->rowCount() | 在 MySQL/MariaDB 中,返回来自SELECT的结果数。大多数其他数据库都不支持。 | | 获得单一结果 | $item = $result->fetchColumn(); | 获取结果第一列中的第一条记录。要从其他列获得结果,请使用列号(从 0 开始计数)作为参数。 | | 获取下一条记录 | $row = $result->fetch(); | 从结果集中获取下一行作为关联数组。 | | 释放数据库资源 | $result->closeCursor(); | 释放连接以允许新查询。 | | 提交非SELECT查询 | $affected = $conn->exec($sql); | 虽然query()可以用于非SELECT查询,但是exec()会返回受影响的行数。 |

使用 SQL 与数据库交互

正如您刚才看到的,PHP 连接到数据库,发送查询,并接收结果,但查询本身需要用 SQL 编写。虽然 SQL 是一个通用标准,但是 SQL 有许多不同的方言。每个数据库供应商,包括 MySQL,都为标准语言添加了扩展。这提高了效率和功能,但通常与其他数据库不兼容。本书中的 SQL 适用于 MySQL 5.1 或更高版本以及 MariaDB,但不一定会转移到 Microsoft SQL Server、Oracle 或其他数据库。

编写 SQL 查询

SQL 语法没有太多的规则,而且都很简单。

SQL 关键字不区分大小写

images表中检索所有记录的查询如下所示:

SELECT * FROM images

大写的单词是 SQL 关键字。这纯粹是约定俗成。以下内容同样正确:

SELECT * FROM images
select * from images
SeLEcT * fRoM images

尽管 SQL 关键字不区分大小写,但同样的不适用于数据库列名。对关键字使用大写字母的好处是,它使 SQL 查询更容易阅读。你可以自由选择最适合你的风格,但是最好避免最后一个例子中的勒索信风格。

空白被忽略

这允许您将 SQL 查询分散到几行中,以增加可读性。不允许空白的地方是在函数名和左括号之间。以下内容会生成一个错误:

SELECT COUNT (*) FROM images /* BAD EXAMPLE */

空间需要像这样封闭起来:

SELECT COUNT(*) FROM images /* CORRECT */

正如您可能从这些示例中总结的那样,您可以通过将注释放在//之间来为 SQL 查询添加注释。

字符串必须用引号括起来

SQL 查询中的所有字符串都必须用引号括起来。使用单引号还是双引号并不重要,只要它们成对出现就行。然而,通常最好使用 MySQLi 或 PDO 预准备语句,这将在本章后面解释。

处理数字

一般来说,数字不应该加引号,因为加引号的都是字符串。然而,MySQL 接受用引号括起来的数字,并将它们视为等价的数字。请注意区分实数和任何其他由数字组成的数据类型。例如,日期由数字组成,但应该用引号括起来,并存储在与日期相关的列类型中。类似地,电话号码应该用引号括起来,并存储在与文本相关的列类型中。

Note

SQL 查询通常以分号结束,分号是数据库执行查询的指令。当使用 PHP 时,分号必须从 SQL 中省略。因此,在本书中,SQL 的独立例子没有以分号结尾。

细化由选择查询检索的数据

到目前为止,您运行的惟一一个 SQL 查询从images表中检索所有记录。很多时候,你想更有选择性。

选择特定列

使用星号选择所有列是一种方便的快捷方式,但是通常应该只指定那些需要的列。在SELECT关键字后列出用逗号分隔的列名。例如,该查询只为每条记录选择了filenamecaption字段:

SELECT filename, caption FROM images

你可以在ch13文件夹的mysqli_03.phppdo_03.php中测试这个。

更改结果的顺序

为了控制排序顺序,添加一个ORDER BY子句,按照优先级顺序列出列名。用逗号分隔多列。以下查询按字母顺序对images表中的标题进行排序(代码在mysqli_04.phppdo_04.php)中):

$sql = 'SELECT * FROM images ORDER BY caption';

Note

这个分号是 PHP 语句的一部分,而不是 SQL 查询的一部分。

上述查询会产生以下输出:

img/332054_5_En_13_Figg_HTML.jpg

要颠倒排序顺序,添加DESC(表示“降序”)关键字,如下所示(在mysqli_05.phppdo_05.php中有示例):

$sql = 'SELECT * FROM images ORDER BY caption DESC';

img/332054_5_En_13_Figh_HTML.jpg

还有一个ASC(代表“升序”)关键字。这是默认的排序顺序,所以通常会被忽略。

但是,当同一表中的列以不同的顺序排序时,指定ASC会增加清晰度。例如,如果您每天发布多篇文章,您可以使用以下查询按字母顺序显示标题,但按发布日期排序,最新的文章排在最前面:

SELECT * FROM articles
ORDER BY published DESC, title ASC

搜索特定值

要搜索特定的值,请在SELECT查询中添加一个WHERE子句。WHERE子句跟在表名后面。例如,mysqli_06.phppdo_06.php中的查询如下所示:

$sql = 'SELECT * FROM images
WHERE image_id = 6';

Note

SQL 使用一个等号来测试相等性,不像 PHP 使用两个。

它会产生以下结果:

img/332054_5_En_13_Figi_HTML.jpg

除了测试相等性之外,WHERE子句还可以使用比较运算符,例如大于(>)和小于(<)。我将根据需要介绍其他选项,而不是现在介绍所有选项。第十五章全面总结了四个主要的 SQL 命令,SELECTINSERTUPDATEDELETE,包括与WHERE一起使用的主要比较运算符列表。

如果与ORDER BY结合使用,WHERE子句必须放在前面。例如(代码在mysqli_07.phppdo_07.php):

$sql = 'SELECT * FROM images
WHERE image_id > 5
ORDER BY caption DESC';

这将选择三个image_id大于 5 的图像,并按照标题以相反的顺序对它们进行排序。

搜索带有通配符的文本

在 SQL 中,百分号(%)是一个通配符,匹配任何内容或不匹配任何内容。它与LIKE关键字一起用在WHERE子句中。

mysqli_08.phppdo_08.php中的查询如下所示:

$sql = 'SELECT * FROM images
WHERE caption LIKE "%Kyoto%"';

它搜索images表中caption列包含“京都”的所有记录,并产生以下结果:

img/332054_5_En_13_Figj_HTML.jpg

如前面的屏幕截图所示,它在images表的八条记录中找到了六条。所有标题都以“Kyoto”结尾,所以结尾的通配符不匹配任何内容,而开头的通配符匹配每个标题的其余部分。

如果省略前导通配符("Kyoto%"),查询将搜索以“Kyoto”开头的标题他们都不知道,所以你不会从搜索中得到任何结果。

mysqli_09.phppdo_09.php中的查询如下所示:

$sql = 'SELECT * FROM images
WHERE caption LIKE "%maiko%"';

它会产生以下结果:

img/332054_5_En_13_Figk_HTML.jpg

该查询拼写的“maiko”都是小写的,但该查询也发现它有一个首字母大写。使用LIKE的搜索不区分大小写。

要执行区分大小写的搜索,您需要像这样添加关键字BINARY(代码在mysqli_10.phppdo_10.php):

$sql = 'SELECT * FROM images
WHERE caption LIKE BINARY "%maiko%"';

到目前为止,您看到的所有示例都是硬编码的,但是大多数时候,SQL 查询中使用的值需要来自用户输入。除非您非常小心,否则这将使您面临被称为 SQL 注入的恶意利用的风险。本章的其余部分解释了这种危险以及如何避免它。

了解 SQL 注入的危险

SQL 注入非常类似于我在第六章中警告过你的邮件头注入。注入攻击试图在 SQL 查询中插入虚假条件,试图暴露或破坏您的数据。以下查询的含义应该很容易理解:

SELECT * FROM users WHERE username = 'xyz' AND pwd = 'abc'

这是登录应用的基本模式。如果查询发现一条记录,其中usernamexyzpwdabc,那么您知道已经提交了用户名和密码的正确组合,因此登录成功。攻击者需要做的只是注入一个额外的条件,就像这样:

SELECT * FROM users WHERE username = 'xyz' AND pwd = 'abc' OR 1 = 1

OR意味着只需要其中一个条件为真,所以即使没有正确的用户名和密码,登录也能成功。当查询的一部分来自变量或用户输入时,SQL 注入依赖于引号和其他控制字符不能被正确转义。

根据具体情况,你可以采取几种策略来预防 SQL 注入:

  • 如果变量是一个整数(例如,记录的主键),使用is_numeric()(int)转换操作符来确保插入查询是安全的。

  • 使用准备好的语句。在预处理语句中,SQL 查询中的占位符表示来自用户输入的值。PHP 代码自动将字符串用引号括起来,并对嵌入的引号和其他控制字符进行转义。MySQLi 和 PDO 的语法不同。

  • 前面的策略都不适合列名,列名不能用引号括起来。要将变量用于列名,请创建一个可接受值的数组,并在将提交的值插入查询之前检查它是否在数组中。

让我们来看看如何使用这些技术。

Note

我没有将 MySQLi real_escape_string()或 PDO quote()方法作为预防 SQL 注入的技术,因为它们都不能提供防弹保护。使用预处理语句将用户输入的值嵌入到 SQL 查询中。

PHP 解决方案 13-6:将用户输入的整数插入到查询中

这个 PHP 解决方案展示了如何净化用户输入的变量,以确保在将值插入 SQL 查询之前它只包含一个整数。MySQLi 和 PDO 的技术是一样的。

  1. ch13文件夹中的mysqli_integer_01.phppdo_integer_01.php复制到mysqlipdo文件夹中,并删除文件名中的_01。每个文件包含一个 SQL 查询,该查询从images表中选择image_idfilename列。在页面主体中,有一个带有下拉菜单的表单,该表单由一个循环填充,该循环遍历 SQL 查询的结果。MySQLi 版本如下所示:
<form action="mysqli_integer.php" method="get">
    <select name="image_id">
    <?php while ($row = $images->fetch_assoc()) { ?>
        <option value="<?= $row['image_id'] ?>"
        <?php if (isset($_GET['image_id']) &&
            $_GET['image_id'] == $row['image_id']) {
            echo 'selected';
         } ?>
         ><?= safe($row['filename']) ?></option>
    <?php } ?>
    </select>
    <input type="submit" name="go" value="Display">
</form>

该表单使用了get方法,并将image_id赋给了<option>标签的value属性。如果$_GET['image_id']$row['image_id']具有相同的值,则当前的image_id与通过页面的查询字符串传递的值相同,因此selected属性被添加到开始的<option>标签中。$row['filename']的值被插入到开始和结束<option>标签之间。

除了使用 PDO fetch()方法在foreach循环中直接运行查询之外,PDO 版本是相同的。

如果您将页面加载到浏览器中,您会看到一个下拉菜单,其中列出了images文件夹中的文件,如下所示:

img/332054_5_En_13_Figl_HTML.jpg

  1. 在结束的</form>标签后立即插入以下代码。MySQLi 和 PDO 的代码是一样的,除了一行代码(这是 MySQLi 版本):
<?php
if (isset($_GET['image_id'])) {
    $image_id = (int) $_GET['image_id'];
    $error = ($image_id === 0) ? true : false;
    if (!$error) {
        $sql = "SELECT filename, caption FROM images
        WHERE image_id = $image_id";
        $result = $conn->query($sql);
        $row = $result->fetch_assoc();
        ?>
        <figure><img src="../img/<?= safe($row['filename']) ?>">
            <figcaption><?= safe($row['caption']) ?></figcaption>
        </figure>
     <?php }
     if ($error) {
         echo '<p>Image not found</p>';
    }
} ?>

条件语句检查image_id是否已经通过$_GET数组发送。如果有,使用(int)转换操作符将其分配给$image_id。使用 casting 操作符有两个目的:防止通过提交浮点数来探查脚本中的错误消息,以及将非数字值转换为 0。

下一行使用三元运算符将$error设置为truefalse,这取决于$image_id 是否为 0。

如果$errorfalse,脚本查询数据库并显示选中的图像和标题。因为您知道$image_id是一个整数,所以直接插入 SQL 查询是安全的。因为是数字,所以不需要用引号括起来,但是赋给$sql的字符串需要用双引号来保证$image_id的值被插入到查询中。

新的查询通过query()方法提交给 MySQL,结果存储在$row中。最后,$row['filename']$row['caption']用于显示页面中的图片及其标题。

但是,如果$errortrue,最后的条件语句显示“找不到图像”

Tip

我选择了一个单独的条件语句来显示“Image not found ”,因为我计划稍后检查另一个错误,并且我希望对两者使用相同的错误消息。

  1. 如果您使用的是 PDO 版本,请找到以下代码行:
$row = $result->fetch_assoc();

改成这样:

  1. 保存页面并将其加载到浏览器中。当页面第一次加载时,只显示下拉菜单。

  2. 从下拉菜单中选择一个文件名,然后单击Display。应该会显示您选择的图像,如下面的屏幕截图所示:

$row = $result->fetch();

img/332054_5_En_13_Figm_HTML.jpg

如果你遇到问题,对照ch13文件夹中的mysqli_integer_02.phppdo_integer_02.php检查你的代码。

  1. 在浏览器中编辑查询字符串,将image_id的值更改为字符串。您应该看到“找不到图像”但是,如果字符串以 1 到 8 之间的数字开头,您将看到与该数字相关的图像和标题。

  2. 尝试 1.0 到 8.9 之间的浮点数。相关图像正常显示。

  3. 请尝试 1-8 范围之外的数字。在 PHP 的旧版本中,不会显示错误消息,因为查询没有问题。它只是在寻找一个不存在的值。然而,在 PHP 8 中,如果你已经将 php.ini 中的 error_reporting 设置为第二章中推荐的级别,你会得到一条类似这样的警告消息:

img/332054_5_En_13_Fign_HTML.jpg

  1. 为了避免这种情况,您应该使用 MySQLi 的num_rows属性或 PDO 的rowCount()方法来检查查询返回的行数。

为 MySQLi 更改如下代码:

    $result = $conn->query($sql);
     if ($result->num_rows) {
         $row = $result->fetch_assoc();
         ?>
         <figure><img src="../img/<?= safe($row['filename']) ?>">
             <figcaption><?= safe($row['caption']) ?></figcaption>
         </figure>
     <?php } else { ?>
         $error = true;
        }
    }
    if ($error) {
        echo '<p>Image not found</p>';
    }
} ?>

对于 PDO,用$result->rowCount()代替$result->num_rows

如果查询没有返回任何行,PHP 将 0 视为隐式的false,因此条件失败,而是执行else子句,将$error设置为true

显示“找不到图像”的条件语句可以移动到else块中,但是这个脚本有几个嵌套的条件。将它分开可以更容易阅读脚本和遵循条件逻辑。

  1. 再次测试页面。当您从下拉菜单中选择图像时,它会像以前一样正常显示。但是,如果您尝试在查询字符串中输入一个超出范围的值,您将看到错误消息。

修改后的代码在ch13文件夹的mysqli_integer_03.phppdo_integer_03.php中。

为用户输入使用准备好的语句

MySQLi 和 PDO 都支持预准备语句,这提供了重要的安全特性。预准备语句是 SQL 查询的模板,其中包含每个可变值的占位符。这不仅使在 PHP 代码中嵌入变量变得更加容易,还可以防止 SQL 注入攻击,因为引号和其他字符会在查询执行前自动转义。

使用预准备语句的其他优点是,当同一个查询被多次使用时,它们会更有效。此外,您可以将来自SELECT查询的每一列的结果绑定到命名变量,从而更容易显示输出。

MySQLi 和 PDO 都使用问号作为匿名占位符,就像这样:

$sql = 'SELECT image_id, filename, caption FROM images WHERE caption LIKE ?';

PDO 也支持使用命名占位符。命名占位符以冒号开头,后跟标识符,如下所示:

$sql = 'SELECT image_id, filename, caption FROM images WHERE caption LIKE :search';

Note

占位符不用引号括起来,即使它们代表的值是字符串。这使得构建 SQL 查询变得容易得多,因为不需要担心如何获得单引号和双引号的正确组合。

占位符只能用于列值。它们不能用于 SQL 查询的其他部分,如列名或运算符。这是因为在执行 SQL 时,包含非数字字符的值会被自动转义并括在引号中。列名和运算符不能用引号括起来。

准备好的语句比直接提交查询涉及的代码稍微多一点,但是占位符使 SQL 更容易读写,并且这个过程更安全。

MySQLi 和 PDO 的语法是不同的,所以下面几节将分别讨论它们。

在 MySQLi 预准备语句中嵌入变量

使用 MySQLi 预准备语句包括几个阶段。

初始化语句

要初始化准备好的语句,在数据库连接上调用stmt_init()方法,并将其存储在一个变量中,如下所示:

$stmt = $conn->stmt_init();

准备声明

然后将 SQL 查询传递给语句的prepare()方法。这将检查您是否在错误的位置使用了问号占位符,以及当所有内容放在一起时,查询是否是有效的 SQL。

如果有任何错误,prepare()方法将返回false,因此通常将接下来的步骤包含在条件语句中,以确保它们仅在一切正常的情况下运行。

可以通过语句的error属性访问错误消息。

将值绑定到占位符

用变量中保存的实际值替换问号在技术上被称为绑定参数。正是这一步骤保护了您的数据库免受 SQL 注入的攻击。

按照您希望将变量插入 SQL 查询的顺序,将变量传递给语句的bind_param()方法,同时传递指定每个变量的数据类型的第一个参数,同样按照变量的顺序。数据类型必须由以下四个字符之一指定:

  • b:二进制(如图像、Word 文档或 PDF 文件)

  • d : Double(浮点数)

  • i:整数(整数)

  • s:字符串(文本)

传递给bind_param()的变量数量必须与问号占位符的数量完全相同。例如,要将单个值作为字符串传递,请使用:

$stmt->bind_param('s', $_GET['words']);

为了传递两个值,SELECT查询需要两个问号作为占位符,两个变量都需要用bind_param()绑定,如下所示:

$sql = 'SELECT * FROM products WHERE price < ? AND type = ?';
$stmt = $conn->stmt_init();
$stmt->prepare($sql);
$stmt->bind_param('ds', $_GET['price'], $_GET['type']);

bind_param()'ds'的第一个参数将$_GET['price']指定为浮点数,将$_GET['type']指定为字符串。

执行语句

一旦准备好语句并且值已经绑定到占位符,调用语句的execute()方法。然后可以从语句对象中获取一个SELECT查询的结果。对于其他类型的查询,这是过程的结尾。

绑定结果(可选)

可选地,您可以使用bind_result()方法将SELECT查询的结果绑定到变量。这避免了提取每一行然后以$row['column_name']的形式访问结果的需要。

为了绑定结果,您必须在SELECT查询中专门命名每一列。以同样的顺序列出您想要使用的变量,并将它们作为参数传递给bind_result()。例如,假设您的 SQL 如下所示:

$sql = 'SELECT image_id, filename, caption FROM images WHERE caption LIKE ?';

要绑定查询结果,请使用以下代码:

$stmt->bind_result($image_id, $filename, $caption);

这允许您直接访问结果,如$image_id$filename$caption

存储结果(可选)

当您为SELECT查询使用准备好的语句时,结果是无缓冲的。这意味着它们会保留在数据库服务器上,直到您提取它们。这具有需要较少内存的优点,尤其是当结果集包含大量行时。但是,无缓冲结果会带来以下限制:

  • 一旦获取了结果,它们就不再存储在内存中。因此,不能多次使用同一个结果集。

  • 在获取或清除所有结果之前,不能在同一数据库连接上运行另一个查询。

  • 您不能使用num_rows属性来确定结果集中有多少行。

  • 不能使用data_seek()移动到结果集中的特定行。

为了避免这些限制,您可以选择使用语句的store_result()方法存储结果集。但是,如果您只是想立即显示结果,而不是在以后重用,就没有必要先存储它。

Note

要清除未缓冲的结果,调用语句的free_result()方法。

获取结果

要循环通过用准备好的语句执行的SELECT查询的结果,请使用fetch()方法。如果您已经将结果绑定到变量,请这样做:

while ($stmt->fetch()) {
 // display the bound variables for each row
}

如果您没有将结果绑定到变量,那么使用$row = $stmt->fetch()并以$row['column_name']的形式访问每个变量。

关闭语句

当您完成一个准备好的语句时,close()方法释放所使用的内存。

PHP 解决方案 13-7:在搜索中使用 MySQLi 预准备语句

这个 PHP 解决方案展示了如何使用 MySQLi 预准备语句和一个SELECT查询;它还演示了将结果绑定到命名变量。

  1. ch13文件夹中复制mysqli_prepared_01.php并在mysqli文件夹中保存为mysqli_prepared.php。该文件包含一个搜索表单和一个用于显示结果的表格。

  2. DOCTYPE声明上方的 PHP 代码块中,创建一个包含connection.phputility_funcs.php的条件语句,并在提交搜索表单时创建一个只读连接。代码如下所示:

  3. 接下来,在条件语句中添加 SQL 查询。该查询需要命名您想要从images表中检索的三列。使用问号作为搜索词的占位符,如下所示:

    $sql = 'SELECT image_id, filename, caption FROM images
               WHERE caption LIKE ?';
    
    
  4. 在将用户提交的搜索词传递给bind_param()方法之前,您需要添加通配符并将其赋给一个新变量,如下所示:

if (isset($_GET['go'])) {
    require_once '../includes/connection.php';
    require_once '../includes/utility_funcs.php';
    $conn = dbConnect('read');
}

  1. 现在可以创建预准备语句了。DOCTYPE声明上方的 PHP 块中完成的代码如下所示:
$searchterm = '%'. $_GET['search'] .'%';

if (isset($_GET['go'])) {
    require_once '../includes/connection.inc.php';
    $conn = dbConnect('read');
    $sql = 'SELECT image_id, filename, caption FROM images
               WHERE caption LIKE ?';
    $searchterm = '%'. $_GET['search'] .'%';
    $stmt = $conn->stmt_init();
    if ($stmt->prepare($sql)) {
        $stmt->bind_param('s', $searchterm);
        $stmt->execute();
        $stmt->bind_result($image_id, $filename, $caption);
        $stmt->store_result();
        $numRows = $stmt->num_rows;
    } else {
        $error = $stmt->error;
    }
}

这将初始化准备好的语句,并将其分配给$stmt。然后,SQL 查询被传递给prepare()方法,该方法检查查询语法的有效性。如果语法有问题,else块将错误消息分配给$error。如果语法没有错误,则执行条件语句中的其余脚本。

条件语句中的第一行将$searchterm绑定到SELECT查询,替换问号占位符。第一个参数告诉准备好的语句将其视为字符串。

执行准备好的语句后,下一行将把SELECT查询的结果绑定到$image_id$filename$caption。这些需要与查询中的顺序相同。我根据变量所代表的列来命名变量,但是您可以使用任何想要的变量。

然后存储结果。注意,您只需通过调用语句对象的store_result()方法来存储结果。与使用query()不同,你不用将store_result()的返回值赋给一个变量。如果您这样做了,它只是truefalse,这取决于结果是否被成功存储。

最后,查询检索的行数从 statement 对象的num_rows属性中获得,并存储在$numRows中。

  1. 在开始的<body>标记后添加一个条件语句,以便在出现问题时显示错误消息:

    <?php
    if (isset($error)) {
        echo "<p>$error</p>";
    }
    ?>
    
    
  2. 在搜索表单后添加以下代码以显示结果:

<?php if (isset($numRows)) { ?>
     <p>Number of results for <b><?= safe($_GET['search']) ?></b>:
         <?= $numRows ?></p>
    <?php if ($numRows) { ?>
        <table>
            <tr>
               <th>image_id</th>
               <th>filename</th>
               <th>caption</th>
           </tr>
           <?php while ($stmt->fetch()) { ?>
               <tr>
                   <td><?= $image_id ?></td>
                   <td><?= safe($filename) ?></td>
                   <td><?= safe($caption) ?></td>
               </tr>
           <?php } ?>
        </table>
 <?php }
} ?>

第一个条件语句包裹在段落和表格周围,防止它们在$numRows不存在的情况下显示,这种情况发生在页面第一次加载时。如果表单已经提交,$numRows将被设置,所以搜索词被重新显示,$numRows的值报告匹配的数量。

如果查询没有返回结果,$numRows为 0,被视为false,因此不显示该表。如果$numRows包含除 0 以外的任何内容,则显示表格。显示结果的while循环对准备好的语句调用fetch()方法。不需要将当前记录存储为$row,因为来自每一列的值已经被绑定到$image_id$filename$caption

  1. 保存页面并将其加载到浏览器中。在搜索字段中输入一些文本,然后点击Search。结果的数量与包含搜索词的标题一起显示,如下面的屏幕截图所示:

img/332054_5_En_13_Figo_HTML.jpg

您可以将您的代码与ch13文件夹中的mysqli_prepared_02.php进行比较。

在 PDO 预准备语句中嵌入变量

PDO 准备的声明提供了匿名和命名占位符的选择。

使用匿名占位符

匿名占位符使用问号的方式与 MySQLi 完全相同:

$sql = 'SELECT image_id, filename, caption FROM images WHERE caption LIKE ?';

使用命名占位符

命名占位符以冒号开头,如下所示:

$sql = 'SELECT image_id, filename, caption FROM images WHERE caption LIKE :search';

使用命名占位符使代码更容易理解,特别是如果您选择的名称基于包含要嵌入 SQL 的值的变量。

准备声明

准备和初始化语句只需一步(不像 MySQLi 需要两步)。您将带有占位符的 SQL 直接传递给连接对象的prepare()方法,该方法返回准备好的语句,如下所示:

$stmt = $conn->prepare($sql);

将值绑定到占位符

将值绑定到占位符有几种不同的方法。当使用匿名占位符时,最简单的方法是按照占位符的顺序创建一个值数组,然后将该数组传递给语句的execute()方法。即使只有一个占位符,也必须使用数组。例如,要将$searchterm绑定到单个匿名占位符,必须用一对方括号将它括起来,如下所示:

$stmt->execute([$searchterm]);

您也可以用类似的方式将值绑定到命名占位符,但是传递给execute()方法的参数必须是一个关联数组,使用命名占位符作为每个值的键。因此,下面的代码将$searchterm绑定到名为占位符的:search:

$stmt->execute([':search' => $searchterm]);

或者,在调用execute()方法之前,可以使用语句的bindParam()bindValue()方法绑定值。当与匿名占位符一起使用时,两种方法的第一个参数都是一个数字,从 1 开始计数,表示占位符在 SQL 中的位置。对于命名占位符,第一个参数是字符串形式的命名占位符。第二个参数是要在查询中插入的值。

然而,这两种方法之间有一个微妙的区别:

  • 对于bindParam(),第二个参数必须是一个变量。它不能是字符串、数字或任何其他类型的表达式。

  • 对于bindValue(),第二个参数应该是字符串、数字或表达式。但它也可以是一个变量。

因为bindValue()接受任何类型的值,bindParam()可能看起来是多余的。区别在于传递给bindValue()的参数值必须是已知的,因为它绑定了实际值,而bindParam()只绑定了变量。因此,该值可以在以后赋给变量。

为了说明区别,让我们使用“使用命名占位符”中的SELECT查询:search占位符跟在LIKE关键字之后,因此该值需要与通配符结合使用。尝试执行以下操作会产生错误:

// This will NOT work
$stmt->bindParam(':search', '%'. $_GET['search'] .'%');

不能用bindParam()将通配符连接到变量。在变量作为参数传递之前,需要添加通配符,如下所示:

$searchterm = '%'. $_GET['search'] .'%';
$stmt->bindParam(':search', $searchterm);

或者,您可以构建表达式作为bindValue()的参数:

// This WILL work
$stmt->bindValue(':search', '%'. $_GET['search'] .'%');

bindParam()bindValue()方法接受可选的第三个参数:指定数据类型的常数。主要常数如下:

  • PDO::PARAM_INT:整数(整数)

  • PDO::PARAM_LOB:二进制(如图像、Word 文档或 PDF 文件)

  • PDO::PARAM_STR:字符串(文本)

  • PDO::PARAM_BOOL:布尔型(真或假)

  • PDO::PARAM_NULL : 零

如果您想将数据库列的值设置为null,那么PDO::PARAM_NULL非常有用。例如,如果主键是自动递增的,那么在插入新记录时,您需要传递null作为值。这就是如何用bindValue()将名为:id的命名参数设置为null:

$stmt->bindValue(':id', NULL, PDO::PARAM_NULL);

Note

浮点数没有 PDO 常数。

执行语句

如果使用bindParam()bindValue()将值绑定到占位符,只需调用不带参数的execute()方法:

$stmt->execute();

否则,按照上一节所述传递一个值数组。在这两种情况下,查询的结果都存储在$stmt中。

可以用与 PDO 连接相同的方式访问错误消息。然而,不是在连接对象上调用errorInfo()方法,而是在 PDO 语句上使用它,就像这样:

$error = $stmt->errorInfo()[2];

如果没有错误,$error将会是null。否则,它将包含描述问题的字符串。

绑定结果(可选)

为了将SELECT查询的结果绑定到变量,需要使用bindColumn()方法单独绑定每一列,该方法有两个参数。第一个参数可以是列名,也可以是从 1 开始的列号。这个数字来自于它在SELECT查询中的位置,而不是它在数据库表中出现的顺序。因此,在我们一直使用的 SQL 示例中,要将来自filename列的结果绑定到$filename,以下任一方法都是可接受的:

$stmt->bindColumn('filename', $filename);
$stmt->bindColumn(2, $filename);

因为每一列都是单独绑定的,所以不需要全部绑定。然而,这样做更方便,因为它避免了将fetch()方法的结果赋给数组的需要。

获取结果

要获取一个SELECT查询的结果,调用语句的fetch()方法。如果您已经使用bindColumn()将输出绑定到变量,您可以直接使用变量。否则,它返回当前行的数组,该数组由列名和零索引列号索引。

Note

您可以通过传递一个常量作为参数来控制 PDO fetch()方法的输出类型。www.php.net/manual/en/pdostatement.fetch.php见。

PHP 解决方案 13-8:在搜索中使用 PDO 准备好的语句

这个 PHP 解决方案展示了如何使用 PDO 语句将用户提交的值从搜索表单嵌入到一个SELECT查询中。它使用与 PHP 解决方案 13-7 中 MySQLi 版本相同的搜索形式。

  1. ch13文件夹中复制pdo_prepared_01.php并在pdo文件夹中保存为pdo_prepared.php

  2. 在 PHP 块中的DOCTYPE声明上方添加以下代码:

if (isset($_GET['go'])) {
    require_once '../includes/connection.php';
    require_once '../includes/utility_funcs.php';
    $conn = dbConnect('read', 'pdo');
    $sql = 'SELECT image_id, filename, caption FROM images
               WHERE caption LIKE :search';
    $stmt = $conn->prepare($sql);
    $stmt->bindValue(':search', '%' . $_GET['search'] . '%');
    $stmt->execute();
    $error = $stmt->errorInfo()[2];
    if (!$error) {
        $stmt->bindColumn('image_id', $image_id);
        $stmt->bindColumn('filename', $filename);
        $stmt->bindColumn(3, $caption);
        $numRows = $stmt->rowCount();
    }
}

提交表单时,这包括连接文件并创建一个 PDO 只读连接。准备好的语句使用:search作为命名参数来代替用户提交的值。

在将搜索词绑定到准备好的语句的同时,%通配符与搜索词连接在一起。所以用bindValue()代替bindParam()

在执行该语句后,调用该语句的errorInfo()方法,查看是否生成了错误消息并存储在$errorInfo[2]中。

如果没有问题,使用bindColumn()方法将结果绑定到$image_id$filename$caption。前两个使用列名,但是caption列是通过它在SELECT查询中的位置(从 1 开始计数)来引用的。

  1. 显示结果的代码与 PHP 解决方案 13-7 中的步骤 6 和 7 相同。您可以在ch13文件夹的pdo_prepared_02.php中查看完成的代码。
PHP 解决方案 13-9:调试 PDO 准备好的语句

有时,数据库查询不会产生您期望的结果。当这种情况发生时,查看您的脚本发送到数据库服务器的确切内容是非常有用的。对于 MySQLi,没有一种简单的方法来检查由预处理语句插入到 SQL 查询中的值。但是对于 PDO 来说,这再简单不过了。这个功能是在 PHP 7.2 中引入的。

  1. 继续使用之前 PHP 解决方案中的pdo_prepared.php。或者,将ch13文件夹中的pdo_prepared_02.php复制到pdo文件夹中,并重命名为pdo_prepared.php

  2. 修改结束标签</table>后的代码,如下所示:

</table>
 <?php }
 echo '<pre>';
 $stmt->debugDumpParams();
 echo '</pre>';
 }
?>

这插入了一对<pre>标签,使得对PDOStatement对象的debugDumpParams()方法的调用的输出更具可读性。

  1. 保存文件,将其加载到浏览器中,并进行搜索。除了搜索结果之外,您还应该看到类似于以下屏幕截图的输出:

img/332054_5_En_13_Figp_HTML.jpg

SQL 查询显示两次。第一次显示了 PHP 代码中出现的查询——在本例中,包括命名参数:search。第二次,它显示发送到数据库服务器的实际值。

在这种情况下,搜索“temp”会返回两个包含“temple”的标题。如果这是你所期望的,那没关系。但是假设你只想要一个精确的匹配。看到%通配符可以解释错误的结果,使调试没有产生预期结果的准备好的语句的问题变得更加容易。

您可以将您的代码与ch13文件夹中的pdo_prepared_03.php进行比较。

Caution

在调用debugDumpParams()之前调用execute()方法很重要。

PHP 解决方案 13-10:通过用户输入改变列选项

这个 PHP 解决方案展示了如何通过用户输入在一个SELECT查询中更改 SQL 关键字的名称。SQL 关键字不能用引号括起来,所以使用预处理语句是行不通的。相反,您需要确保用户输入与一组预期值相匹配。如果找不到匹配,则使用默认值。MySQLi 和 PDO 的技术是一样的。

  1. ch13文件夹中复制mysqli_order_01.phppdo_order_01.php并保存在mysqlipdo文件夹中。两个版本都从images表中选择所有记录,并在表格中显示结果。这些页面还包含一个表单,该表单允许用户选择一个列的名称,以升序或降序对结果进行排序。在初始状态下,表单是不活动的。页面显示按image_id升序排序的详细信息,如下所示:

img/332054_5_En_13_Figq_HTML.jpg

  1. 将 PHP 块中的代码修改到DOCTYPE声明之上,如下所示(下面的清单显示的是 PDO 版本,但是粗体显示的变化对于 MySQLi 是一样的):
require_once '../includes/connection.php';
require_once '../includes/utility_funcs.php';
// connect to database
$conn = dbConnect('read', 'pdo');
// set default values
$col = 'image_id';
$dir = 'ASC';
// create arrays of permitted values
$columns = ['image_id', 'filename', 'caption'];
$direction = ['ASC', 'DESC'];
// if the form has been submitted, use only expected values
if (isset($_GET['column']) && in_array($_GET['column'], $columns)) {
    $col = $_GET['column'];
}

if (isset($_GET['direction']) && in_array($_GET['direction'], $direction)) {
    $dir = $_GET['direction'];
}
// prepare the SQL query using sanitized variables
$sql = "SELECT * FROM images
            ORDER BY $col $dir";
// submit the query and capture the result
$result = $conn->query($sql);
$error = $conn->errorInfo()[2];

新代码定义了两个变量,$col$dir,它们被直接嵌入到SELECT查询中。因为它们被赋予了默认值,所以当页面第一次加载时,查询显示按image_id列升序排序的结果。

两个数组$columns$direction定义了允许的值:列名和ASCDESC关键字。这些数组由检查columndirection$_GET数组的条件语句使用。只有当提交的值分别与$columns$direction数组中的值匹配时,它们才会被重新分配给$col$dir。这可以防止任何向 SQL 查询中注入非法值的企图。

  1. 编辑下拉菜单中的<option>标签,使其显示为$col$dir选择的值,如下所示:

  2. 保存页面并在浏览器中测试。您可以通过选择下拉菜单中的值并点击Change来更改显示的排序顺序。但是,如果您试图通过查询字符串注入非法值,页面会使用默认值$col$dir来显示按image_id升序排序的结果。

<select name="column" id="column">
    <option <?php if ($col == 'image_id') echo 'selected'; ?>
        >image_id</option>
    <option <?php if ($col == 'filename') echo 'selected'; ?>
        >filename</option>
    <option <?php if ($col == 'caption') echo 'selected'; ?>
        >caption</option>
</select>
<select name="direction" id="direction">
    <option value="ASC" <?php if ($dir == 'ASC') echo 'selected'; ?>
        >Ascending</option>
    <option value="DESC" <?php if ($dir == 'DESC') echo 'selected'; ?>
        >Descending</option>
</select>

你可以对照ch13文件夹中的mysqli_order_02.phppdo_order_02.php来检查你的代码。

第三章回顾

PHP 8 提供了两种与 MySQL 通信的方法:

  • MySQL 改进的(MySQLi)扩展:推荐所有新的 MySQL 项目使用。它比不再受支持的原始 MySQL 扩展更有效。它增加了预准备语句的安全性,并且与 MariaDB 完全兼容。

  • PHP 数据对象(PDO)抽象层,它是数据库中立的:这是我与数据库通信的首选方法。它不仅是数据库中立的;它的优点是对准备好的语句使用命名参数,使代码更容易阅读和理解。此外,调试准备好的语句非常容易。虽然代码是数据库中立的,但 PDO 要求为您选择的数据库安装正确的驱动程序。MySQL 的驱动程序与 MariaDB 完全兼容,并且通常是安装的。其他驱动不太常见。但是,如果安装了正确的驱动程序,从一个数据库切换到另一个数据库时,只需更改连接字符串中的数据源名称(DSN)。

尽管 PHP 与数据库通信并存储结果,但查询需要用 SQL 编写,SQL 是用于查询关系数据库的标准语言。本章展示了如何使用SELECT语句检索存储在数据库表中的信息,使用WHERE子句细化搜索,以及使用ORDER BY改变排序顺序。您还了解了几种防止 SQL 注入查询的技术,包括预处理语句,它使用占位符而不是直接在查询中嵌入变量。

在下一章中,你将通过创建一个在线图片库将这些知识付诸实践。

十四、创建动态图片库

前一章主要集中在将images表的内容提取为文本。本章基于这些技术来开发如图 14-1 所示的迷你图片库。

img/332054_5_En_14_Fig1_HTML.jpg

图 14-1

这个迷你图片库是通过从数据库中提取信息来驱动的

该图库还展示了一些您希望整合到文本驱动页面中的酷功能。例如,左侧的缩略图网格每行显示两个图像。只需更改两个数字,您就可以根据自己的喜好,让网格有多宽多行。单击其中一个缩略图会替换主图像和标题。这是重新加载的同一个页面,但完全相同的技术用于创建在线目录,将您带到另一个页面,其中包含有关产品的更多详细信息。缩略图网格底部的下一个链接显示了下一组图片,使用的技术与浏览一长串搜索结果完全相同。这个画廊不仅仅是一两张漂亮的脸…本章涵盖

  • 理解为什么在数据库中存储图像是一个坏主意,以及你应该怎么做

  • 规划动态画廊的布局

  • 在表格行中显示固定数量的结果

  • 限制一次检索的记录数量

  • 翻阅一长串结果

为什么不将图像存储在数据库中?

images表格包含文件名和标题,但不包含图像本身。尽管您可以在数据库中存储二进制对象,比如图像,但我不打算这么做,原因很简单,这通常会带来更多麻烦,不值得这么做。主要问题如下:

  • 如果不单独存储文本信息,就不能对图像进行索引或搜索。

  • 图像通常很大,增大了表格的大小。如果数据库中的存储量有限制,就有耗尽空间的风险。

  • 如果频繁删除图像,表碎片会影响性能。

  • 从数据库中检索图像需要将图像传递给一个单独的脚本,这会降低网页的显示速度。

将图像存储在网站上的普通文件夹中并使用数据库来获取图像信息会更有效。您只需要两条信息——文件名和标题,标题也可以用作alt文本。一些开发人员将图像的完整路径存储在数据库中,但是我认为只存储文件名会给你带来更大的灵活性。到images文件夹的路径将被嵌入到 HTML 中。没有必要存储图像的高度和宽度。正如您在第 5 和 10 章中看到的,您可以使用 PHP 的getimagesize()函数动态生成这些信息。

规划画廊

我发现设计数据库驱动网站的最好方法是从静态页面开始,用占位符文本和图像填充它。然后我创建我的 CSS 样式规则来得到我想要的页面外观,最后我用 PHP 代码替换每个占位符元素。每次我更换东西时,我都会在浏览器中检查页面,以确保所有东西都还在一起。

图 14-2 显示了我制作的图库的静态模型,并指出了需要转换成动态代码的元素。这些图像与第五章中用于随机图像发生器的图像相同,并且大小不同。我尝试通过缩放图像来创建缩略图,但认为结果看起来太不整洁,所以我将缩略图设为标准大小(80 × 54 像素)。此外,为了方便起见,我给每个缩略图取了与大图相同的名字,并将它们存储在images文件夹中一个名为thumbs的独立子文件夹中。

img/332054_5_En_14_Fig2_HTML.jpg

图 14-2

找出将静态画廊转变为动态画廊需要做的事情

在前一章中,显示images表的内容很容易。您创建了单个表格行,每个字段的内容位于单独的表格单元格中。通过遍历结果集,每条记录都显示在自己的一行上,模拟了数据库表的列结构。这一次,缩略图网格的两列结构不再与数据库结构匹配。在创建下一行之前,您需要计算一行中插入了多少个缩略图。

一旦我确定了需要做什么,我就去掉了缩略图 2-6 和导航链接的代码。下面的清单显示了gallery.php<main>元素中剩下的内容,需要转换成 PHP 代码的元素以粗体突出显示(您可以在ch14文件夹的gallery_01.php中找到代码):

<main>
    <h2>Images of Japan</h2>
    <p id="picCount">Displaying 1 to 6 of 8</p>
    <div id="gallery">
        <table id="thumbs">
            <tr>
                <!-- This row needs to be repeated -->
                <td><a href="gallery.php"><img src="img/thumbs/basin.jpg" alt=""
                    width="80" height="54"></a></td>
            </tr>
             <!-- Navigation link needs to go here -->
        </table>
        <figure id="main_image">
            <img src="img/basin.jpg" alt="" width="350" height="237">
            <figcaption>Water basin at Ryoanji temple, Kyoto</figcaption>
        </figure>
  </div>
</main>

将图库元素转换为 PHP

在显示图库的内容之前,您需要连接到phpsols数据库并检索存储在images表中的所有记录。这样做的过程与上一章相同,使用以下简单的 SQL 查询:

SELECT filename, caption FROM images

然后,您可以使用第一条记录来显示第一幅图像及其关联的标题和缩略图。不需要image_id

PHP 解决方案 14-1:显示第一张图片

如果您在第五章建立了日本之旅网站,您可以直接使用原始的gallery.php进行工作。或者,从ch14文件夹中复制gallery_01.php,并将其作为gallery.php保存在php8sols站点根目录下。您还需要将title.phpmenu.phpfooter.php复制到php8sols站点的includes文件夹中。如果编辑程序询问您是否要更新文件中的链接,请选择不更新选项。

img/332054_5_En_14_Fig3_HTML.jpg

图 14-3

准备转换的静态图库的精简版本

  1. gallery.php加载到浏览器中,确保其正确显示。页面的主要部分应该如图 14-3 所示,有一个缩略图和同一图像的放大版本。

  2. 图库依赖于到数据库的连接,因此包含connection.php,创建到phpsols数据库的只读连接,并定义 SQL 查询。在gallery.php中的DOCTYPE声明上方的结束 PHP 标签之前添加以下代码(新代码以粗体突出显示):

include './includes/title.php';
require_once './includes/connection.php';
require_once './includes/utility_funcs.php';
$conn = dbConnect('read');
$sql = 'SELECT filename, caption FROM images';

如果使用 PDO,将'pdo'作为第二个参数添加到dbConnect()中。

  1. 提交查询和从结果中提取第一条记录的代码取决于您使用的连接方法。对于 MySQLi,使用这个:
// submit the query
$result = $conn->query($sql);
if (!$result) {
    $error = $conn->error;
} else {
    // extract the first record as an array
    $row = $result->fetch_assoc();
}

对于 PDO,使用这个:

// submit the query
$result = $conn->query($sql);
// get any error messages
$error = $conn->errorInfo()[2];
if (!$error) {
    // extract the first record as an array
    $row = $result->fetch();
}

要在页面加载时显示第一个图像,您需要在创建最终显示缩略图网格的循环之前检索第一个结果。MySQLi 和 PDO 的代码提交查询,提取第一条记录,并将其存储在$row中。

  1. 您现在已经将第一个记录图像的细节存储为$row['filename']$row['caption']。除了文件名和标题,您还需要大版本的尺寸,这样您就可以在页面的主体中显示它。在获取第一个结果的代码之后,立即在else块中添加以下代码:
// get the name and caption for the main image
$mainImage = safe($row['filename']);
$caption = safe($row['caption']);
// get the dimensions of the main image
$imageSize = getimagesize('img/'.$mainImage)[3];

使用上一章定义的safe()函数对数据库中的文本值进行清理。

正如第十章所解释的,getimagesize()返回一个数组,数组的第四个元素包含一个字符串,该字符串具有准备插入到<img>标签中的图像的宽度和高度。我们只对第四个元素感兴趣,所以我们可以使用在第七章中介绍的数组解引用技术。在getimagesize()的右括号后添加[3]只会返回数组的第四个元素,该元素被赋值给$imageSize

  1. 现在,您可以使用这些信息来动态显示缩略图、主图像和标题。主图像和缩略图具有相同的名称,但是您最终希望通过循环整个结果集来显示所有缩略图。因此,表格单元格中的动态代码需要引用当前记录——换句话说,引用$row['filename']$row['caption'],而不是$mainImage$caption。它们还需要通过传递给safe()函数来净化。稍后您会看到为什么我将第一条记录中的值分配给不同的变量。如下修改表中的代码:

                   <td><a href="gallery.php">
    <img src="img/thumbs/<?= safe($row['filename']); ?>"
                           alt="<?= safe($row['caption']); ?>" width="80" height="54"></a></td>
    
    
  2. 如果查询有问题,您需要检查$error是否等于true,并阻止图库显示。在日本的<h2>图片标题后添加一个包含以下条件语句的 PHP 块:

    <?php if (isset($error)) {
        echo "<p>$error</p>";
        } else {
    ?>
    
    

    Tip

    尽管 PDO 版本的脚本在第 3 步中给$error赋值,但是您可以在这里使用isset($error),因为如果查询成功执行,这个值就是null。通过nullisset()返回false

  3. 在结束的</main>标签之前插入一个新行(在第 55 行周围),并添加一个带有else块的结束花括号的 PHP 块:

  4. 保存gallery.php并在浏览器中查看。它看起来应该和图 14-3 一样。唯一的区别是缩略图和它的alt文本是动态生成的。您可以通过查看源代码来验证这一点。最初的静态版本有一个空的alt属性,但是如下图所示,它现在包含了第一条记录的标题:

<?php } ?>

img/332054_5_En_14_Figa_HTML.jpg

如果出现问题,确保图像的src属性中静态和动态生成的文本之间没有间隙。还要检查您是否使用了与您创建的数据库连接类型相对应的正确代码。你可以对照ch14文件夹中的gallery_mysqli_02.phpgallery_pdo_02.php来检查你的代码。

  1. 一旦确认从数据库中获取了细节,就可以转换主图像的代码了。修改如下(新代码以粗体显示):
<figure id="main_image">
    <img src="img/<?= $mainImage ?>" alt="<?= $caption ?>"
        <?= $imageSize ?>></p>
    <figcaption><?= $caption ?></figcaption>
</figure>

$mainImage$caption不需要传递给safe()函数,因为它们已经在步骤 4 中被清理了。

$imageSize为主图像插入一个包含正确的widthheight属性的字符串。

  1. 再次测试页面。它看起来应该和图 14-3 中的一样,但是图像和标题是从数据库中动态绘制的,getimagesize()正在计算主图像的正确尺寸。你可以对照ch14文件夹中的gallery_mysqli_03.phpgallery_pdo_03.php来检查你的代码。

构建动态元素

转换静态页面后的第一个任务是显示所有缩略图,然后构建动态链接,使您能够显示任何被单击的缩略图的大版本。显示所有的缩略图很容易——只需遍历它们(我们稍后将研究如何以两行显示它们)。激活每个缩略图的链接需要更多的思考。您需要一种方式来告诉页面显示哪个大图像。

通过查询字符串传递信息

在上一节中,您使用了$mainImage来标识大图像,因此您需要一种每当单击缩略图时更改其值的方法。解决方案是将图像的文件名添加到链接中 URL 末尾的查询字符串中,如下所示:

<a href="gallery.php?image=filename">

然后可以检查$_GET数组是否包含一个名为image的元素。如果是,改变$mainImage的值。如果没有,将结果集中第一条记录的文件名保留为$mainImage

PHP 解决方案 14-2:激活缩略图

继续使用与上一节相同的文件。或者,将gallery_mysqli_03.phpgallery_pdo_03.php复制到php8sols站点根目录,并保存为gallery.php

  1. 找到缩略图周围链接的开始<a>标签。看起来是这样的:
<a href="gallery.php">

改成这样:

<a href="gallery.php?image=<?= safe($row['filename']) ?>">

这会在href属性的末尾添加一个查询字符串,将当前文件名赋给一个名为image的变量。重要的是?image=周围没有空格。

  1. 保存页面并将其加载到浏览器中。将鼠标指针悬停在缩略图上,检查状态栏中显示的 URL。应该是这样的:
http://localhost/php8sols/gallery.php?image=basin.jpg

如果状态栏中没有显示任何内容,请单击缩略图。页面不应该改变,但是地址栏中的 URL 现在应该包含查询字符串。检查 URL 或查询字符串中是否有空白。

  1. 要显示所有缩略图,您需要将表格单元格环绕成一个循环。在关于重复该行的 HTML 注释后插入一个新行,并创建一个像这样的do... while循环的前半部分(参见第四章了解不同类型循环的详细信息):

    <!-- This row needs to be repeated -->
    <?php do { ?>
    
    
  2. 您已经有了结果集中第一条记录的详细信息,所以获取后续记录的代码需要放在结束的</td>标记之后。在结束标签</td></tr>之间留出一些空间,并插入以下代码。每种数据库连接方法都略有不同。

对于 MySQLi,使用这个:

</td>
    <?php } while ($row = $result->fetch_assoc()); ?>
</tr>

对于 PDO,使用这个:

</td>
    <?php } while ($row = $result->fetch()); ?>
</tr>

这将获取结果集中的下一行,并将循环发送回顶部。因为$row['filename']$row['caption']具有不同的值,所以下一个缩略图及其关联的alt文本被插入到新的表格单元格中。查询字符串也用新文件名更新。

  1. 保存页面并在浏览器中测试。现在,您应该可以在画廊顶部的一行中看到所有八个缩略图,如下面的屏幕截图所示:

img/332054_5_En_14_Figb_HTML.jpg

将鼠标指针悬停在每个缩略图上,您应该会看到显示文件名的查询字符串。您可以对照gallery_mysqli_04.phpgallery_pdo_04.php检查您的代码。

  1. 单击缩略图仍然没有任何作用,因此您需要创建逻辑来更改主图像及其相关标题。在DOCTYPE声明上方的块中找到这段代码:
// get the name and caption for the main image
$mainImage = safe($row['filename']);
$caption = safe($row['caption']);

突出显示定义$caption的行,并将其剪切到剪贴板。在条件语句中换行,如下所示:

// get the name for the main image
if (isset($_GET['image'])) {
    $mainImage = safe($_GET['image']);
} else {
    $mainImage = safe($row['filename']);
}

$_GET数组包含通过查询字符串传递的值,因此如果已经设置(定义)了$_GET['image'],它将从查询字符串中获取文件名,并将其存储为$mainImage。如果$_GET['image']不存在,则像以前一样,从结果集中的第一条记录中获取值。

  1. 最后,您需要获取主图像的标题。它不再每次都是相同的,所以您需要将它移动到在thumbs表中显示缩略图的循环中。它就在循环的左花括号后面(第 48 行周围)。将光标放在大括号后,插入几行,然后粘贴在上一步中剪切的标题定义。您希望标题与主图像匹配,所以如果当前记录的文件名与$mainImage相同,那就是您要找的那个。将刚刚粘贴的代码包装在条件语句中,如下所示:

    <?php
    do {
        // set caption if thumbnail is same as main image
        if ($row['filename'] == $mainImage) {
            $caption = safe($row['caption']); // this is the line you pasted
        }
    ?>
    
    
  2. 保存页面并在浏览器中重新加载。这一次,当您单击缩略图时,主图像和标题将会改变。不要担心一些图片和标题被页脚隐藏。当缩略图移到主图像的左边时,它会自动修正。

    Note

    通过这样的查询字符串传递信息是处理 PHP 和数据库结果的一个重要方面。虽然表单信息通常通过$_POST数组传递,但是$_GET数组经常用于传递您想要显示、更新或删除的记录的详细信息。它也常用于搜索,因为查询字符串可以很容易地加入书签。

  3. 在这种情况下没有 SQL 注入的危险。但是如果有人更改了通过查询字符串传递的文件名的值,如果图像找不到并且display_errors打开,您将得到难看的错误消息。在调用getimagesize()之前,我们先了解一下镜像是否存在。用这样的条件语句包装它:

  4. 尝试将查询字符串中的值image更改为除现有文件值之外的任何值。当您加载页面时,您应该会看到Image not found

if (file_exists('img/'.$mainImage)) {
    // get the dimensions of the main image
    $imageSize = getimagesize('img/'.$mainImage)[3];
} else {
    $error = 'Image not found.';
}

如有必要,对照gallery_mysqli_05.phpgallery_pdo_05.php检查您的代码。

创建多列表格

只有八个图像,画廊顶部的单排缩略图看起来还不错。但是,如果能够通过使用循环动态地构建表格,这是很有用的,该循环在移动到下一行之前在一行中插入特定数量的表格单元格。这是通过记录插入了多少个单元格来实现的。当达到该行的限制时,代码需要为当前行插入一个结束标记,如果还有缩略图,还需要为下一行插入一个开始标记。使它易于实现的是模数运算符%,它返回除法的余数。

这就是它的工作原理。假设您希望每行有两个单元格。插入第一个单元后,计数器被设置为1。如果用模数运算符(1 % 2)将1除以2,结果是1。当插入下一个单元格时,计数器增加到22 % 2的结果是0。下一个单元格产生这个计算:3 % 2,结果是1。但是第四个细胞产生4 % 2,又是0。因此,每次计算结果为0时,您知道——或者更确切地说,PHP 知道——您在一行的末尾。

那么,您如何知道是否还有剩余的行呢?通过将插入结束和开始<tr>标签的代码放在循环的顶部,必须始终至少有一个图像。然而,第一次循环运行时,剩余的也是0,所以问题是您需要防止标签被插入,直到至少一个图像被显示。唷…让我们试试吧。

PHP 解决方案 14-3:水平和垂直循环

这个 PHP 解决方案展示了如何控制一个循环,以便在一个表格中显示特定数量的列。列数通过设置一个常数来控制。继续使用上一节中的文件。或者,使用gallery_mysqli_05.phpgallery_pdo_05.php

  1. 您可能会在稍后阶段决定要更改表中的列数,因此在脚本的顶部创建一个容易找到的常数是一个好主意,而不是将数字隐藏在代码中。在创建数据库连接之前插入以下代码:

    // define number of columns in table
    define('COLS', 2);
    
    

常量类似于变量,除了它的值不能被脚本的另一部分改变。使用define()函数创建一个常量,该函数有两个参数:常量的名称和值。按照惯例,常数总是大写并且区分大小写。与变量不同,它们不以美元符号开头。

  1. 您需要在循环之外初始化单元计数器。还要创建一个变量来指示它是否是第一行。在刚刚定义的常数后立即添加以下代码:

  2. 记录列数的代码放在显示缩略图的循环开始处的 PHP 块中。像这样修改代码:

    <?php do {
      // set caption if thumbnail is same as main image
      if ($row['filename'] == $mainImage) {
          $caption = safe($row['caption']);
      }
      // if remainder is 0 and not first row, close row and start new one
      if ($pos++ % COLS === 0 && !$firstRow) {
          echo '</tr><tr>';
      }
      // once loop begins, this is no longer true
      $firstRow = false;
    ?>
    
    
define('COLS', 2);
// initialize variables for the horizontal looper
$pos = 0;
$firstRow = true;

因为增量运算符(++)被放在$pos之后,所以它的值在被1递增之前被除以列数。循环第一次运行,余数是0,但是$firstRowtrue,所以条件语句失败。但是,$firstRow在条件语句后被重置为false。在以后的循环迭代中,条件语句会关闭当前表行,并在每次余数为 0 时开始一个新行。

  1. 如果没有更多的记录,您需要检查是否在表的底部有一个不完整的行。在现有的do. . . while回路后增加一个while回路。在 MySQLi 版本中,它看起来像这样:
<?php } while ($row = $result->fetch_assoc());
  while ($pos++ % COLS) {
      echo '<td>&nbsp;</td>';
  }
?>

新代码与 PDO 版本完全相同。唯一不同的是前面一行使用了$result->fetch()而不是$result->fetch_assoc()

第二个循环继续递增$pos,而$pos++ % COLS产生一个余数(被解释为true)并插入一个空单元格。

Caution

第二个循环没有嵌套在第一个循环中。它仅在第一个循环结束后运行。

img/332054_5_En_14_Fig4_HTML.jpg

图 14-4

缩略图现在排列整齐

  1. 保存页面并在浏览器中重新加载。画廊顶部的单行缩略图现在应该两个两个整齐排列,如图 14-4 所示。

尝试更改COLS的值并重新加载页面。主图像将被替换,因为页面只设计了两列,但是您可以看到,只需更改一个数字,就可以轻松控制每行的单元格数量。你可以对照gallery_mysqli_06.phpgallery_pdo_06.php来检查你的代码。

翻阅一长串记录

八个缩略图的网格非常适合画廊,但是如果你有 28 个或 48 个呢?答案是限制每个页面上显示的结果数量,然后构建一个导航系统,让您可以在结果中来回翻页。在使用搜索引擎时,你已经无数次地见过这种技术;现在你要学习如何自己建造它。这项任务可以分为以下两个阶段:

  1. 选择要显示的记录子集

  2. 创建浏览子集的导航链接

这两个阶段都相对容易实现,尽管它们涉及到应用一点条件逻辑。保持冷静,你会轻松度过难关的。

选择记录的子集

限制页面上的结果数量很简单——只需向 SQL 查询添加关键字LIMIT,如下所示:

SELECT filename, caption FROM images LIMIT startPosition, maximum

LIMIT关键字后面可以跟一个或两个数字。如果只使用一个数字,它将设置要检索的最大记录数。这很有用,但不适合寻呼系统。为此,您需要使用两个数字:第一个指示从哪个记录开始,第二个规定要检索的最大记录数。MySQL 从 0 开始计数记录,因此要显示前六张图像,您需要以下 SQL:

SELECT filename, caption FROM images LIMIT 0, 6

要显示下一个集合,SQL 需要改为:

SELECT filename, caption FROM images LIMIT 6, 6

images表中只有八条记录,但是第二个数字只是一个最大值,所以它检索记录 7 和 8。

要构建导航系统,您需要一种生成这些数字的方法。第二个数字永远不变,所以我们定义一个常数叫做SHOWMAX。生成第一个数字(称之为$startRecord)也很容易。从 0 开始对页码进行编号,并将第二个数字乘以当前页码。所以,如果你调用当前页面$curPage,公式看起来是这样的:

$startRecord = $curPage * SHOWMAX;

对于 SQL,它变成这样:

SELECT filename, caption FROM images LIMIT $startRecord, SHOWMAX

如果$curPage为 0,$startRecord也为 0 (0 × 6),但当$curPage增加到 1 时,$startRecord变为 6 (1 × 6),以此类推。

因为在images表中只有八条记录,所以您需要一种方法来找出记录的总数,以防止导航系统检索空的结果集。在上一章中,您使用了 MySQLi num_rows属性和 PDO 的rowCount()。但是,这一次不行,因为您想知道记录的总数,而不是在当前的结果集中有多少。答案是像这样使用 SQL COUNT()函数:

SELECT COUNT(*) FROM images

当像这样与星号结合使用时,COUNT()获取表中记录的总数。因此,要构建一个导航系统,您需要运行两个 SQL 查询:一个用于查找记录总数,另一个用于检索所需的子集。这些是简单的查询,所以结果几乎是即时的。

稍后我将处理导航链接。让我们从限制第一页上缩略图的数量开始。

PHP 解决方案 14-4:显示记录的子集

这个 PHP 解决方案展示了如何选择一个记录子集,为创建一个浏览更长记录集的导航系统做准备。它还演示了如何显示当前选择的数字以及记录的总数。

像以前一样继续处理同一个文件。或者,使用gallery_mysqli_06.phpgallery_pdo_06.php

  1. 定义SHOWMAX和 SQL 查询来查找表中的记录总数。修改页面顶部的代码,如下所示(新代码以粗体显示):

  2. 您现在需要运行新的 SQL 查询。该代码紧跟在前面步骤中的代码之后,但根据 MySQL 连接的类型而有所不同。对于 MySQLi,使用这个:

    // submit query and store result as $totalPix
    $total = $conn->query($getTotal);
    $totalPix = $total->fetch_row()[0];
    
    
// initialize variables for the horizontal looper
$pos = 0;
$firstRow = true;
// set maximum number of records
define('SHOWMAX', 6);
$conn = dbConnect('read');
// prepare SQL to get total records
$getTotal = 'SELECT COUNT(*) FROM images';

它提交查询,然后使用fetch_row()方法,该方法从MySQLi_Result对象中获取一行作为索引数组。结果中只有一列,所以我们可以通过在对fetch_row()的调用后添加方括号中的0来使用数组解引用获得images表中记录的总数。

对于 PDO,使用这个:

// submit query and store result as $totalPix
$total = $conn->query($getTotal);
$totalPix = $total->fetchColumn();

它提交查询,然后使用fetchColumn()获得一个结果,这个结果存储在$totalPix中。

  1. 接下来,设置$curPage的值。您稍后将创建的导航链接将通过查询字符串传递所需页面的值,因此您需要检查curPage是否在$_GET数组中。如果是,就使用那个值,但是要确保它是一个整数,在它前面加上(int)转换操作符。否则,将当前页面设置为 0。在上一步中的代码后立即插入以下代码:

    // set the current page
    $curPage = (isset($_GET['curPage'])) ? (int) $_GET['curPage'] : 0;
    
    
  2. 现在,您已经获得了计算起始行和构建 SQL 查询以检索记录子集所需的所有信息。在前面步骤中的代码后立即添加以下代码:

    // calculate the start row of the subset
    $startRow = $curPage * SHOWMAX;
    
    
  3. 但是有一个问题。$curPage的值来自查询字符串。如果有人在浏览器地址栏中手动更改数字,$startRow可能会大于数据库中的记录数。如果$startRow的值超过$totalPix,则需要将$startRow$curPage都重置为0。将此条件语句添加到上一步中的代码之后:

    if ($startRow > $totalPix) {
        $startRow = 0;
        $curPage = 0;
    }
    
    
  4. 原始的 SQL 查询现在应该在下一行。修改成这样:

    // prepare SQL to retrieve subset of image details
    $sql = "SELECT filename, caption FROM images LIMIT $startRow," . SHOWMAX;
    
    

这次我使用了双引号,因为我希望 PHP 处理$startRow。与变量不同,常量不会在双引号字符串中处理。所以用串联操作符(句号)将SHOWMAX添加到 SQL 查询的末尾。右引号中的逗号是 SQL 的一部分,用于分隔LIMIT子句的两个参数。

img/332054_5_En_14_Fig5_HTML.jpg

图 14-5

缩略图的数量受 SHOWMAX 常量的限制

  1. 保存页面并将其重新加载到浏览器中。你应该只看到六个缩略图,而不是八个,如图 14-5 所示。

更改SHOWMAX的值以查看不同数量的缩略图。

  1. 缩略图网格上方的文本不会更新,因为它仍然是硬编码的,所以让我们来解决这个问题。在页面正文中找到以下代码行:
<p id="picCount">Displaying 1 to 6 of 8</p>

替换为以下内容:

<p id="picCount">Displaying <?php echo $startRow+1;
if ($startRow+1 < $totalPix) {
    echo ' to ';
    if ($startRow+SHOWMAX < $totalPix) {
        echo $startRow+SHOWMAX;
    } else {
        echo $totalPix;
    }
}
echo " of $totalPix";
?></p>

让我们一行一行来。$startRow的值是从零开始的,所以需要加 1 才能得到一个更加用户友好的数字。所以$startRow+1在第一页显示 1,在第二页显示 7。

在第二行中,$startRow+1与记录总数进行比较。如果小于这个值,则意味着当前页面显示的是一系列记录,因此第三行显示文本“to ”,两边各有一个空格。

然后,您需要计算出该范围的最大值,因此嵌套的if ... else条件语句将开始行的值添加到页面上显示的最大记录数中。如果结果小于记录总数,$startRow+SHOWMAX给出页面上最后一条记录的编号。但是,如果它等于或大于总数,则显示$totalPix

最后,您退出两个条件语句并显示“of ”,后跟记录总数。

  1. 保存页面并在浏览器中重新加载。您仍然只能得到缩略图的第一个子集,但是无论何时您改变SHOWMAX的值,您都会看到第二个数字动态变化。如有必要,对照gallery_mysqli_07.phpgallery_pdo_07.php检查你的代码。
浏览记录子集

正如我在上一节的步骤 3 中提到的,所需页面的值通过一个查询字符串传递给 PHP 脚本。当页面第一次加载时,没有查询字符串,所以$curPage的值被设置为0。虽然当您点按缩略图以显示不同图像时会生成查询字符串,但它仅包括主图像的文件名,因此缩略图的原始子集保持不变。为了显示下一个子集,您需要创建一个将$curPage的值增加1的链接。因此,要返回到前面的子集,您需要另一个将$curPage的值减少1的链接。

这很简单,但是您还需要确保这些链接只在有有效的子集可供导航时才显示。例如,在第一页显示一个返回链接是没有意义的,因为没有之前的子集。同样,你不应该在显示最后一个子集的页面上显示一个前向链接,因为没有什么可以导航的。

使用条件语句可以很容易地解决这两个问题。还有最后一件事你需要处理。您还必须在单击缩略图时生成的查询字符串中包含当前页面的值。如果您没有这样做,$curPage会自动设置回0,并显示第一组缩略图,而不是当前子集。

PHP 解决方案 14-5:创建导航链接

这个 PHP 解决方案展示了如何创建导航链接,以便在每个记录子集之间来回翻页。像以前一样继续处理同一个文件。或者,使用gallery_mysqli_07.phpgallery_pdo_07.php

  1. 我已经将导航链接放置在缩略图表格底部的额外一行中。在占位符注释和结束</table>标记之间插入以下代码:
<!-- Navigation link needs to go here -->
<tr><td>
<?php

// create a back link if current page greater than 0
if ($curPage > 0) {
    echo '<a href="gallery.php?curPage=' . ($curPage-1) . '"> < Prev</a>';
} else {
    // otherwise leave the cell empty
    echo '&nbsp;';
}
?>
</td>
<?php
// pad the final row with empty cells if more than 2 columns
if (COLS-2 > 0) {
    for ($i = 0; $i < COLS-2; $i++) {
        echo '<td>&nbsp;</td>';
    }
}
?>
<td>
<?php
// create a forward link if more records exist
if ($startRow+SHOWMAX < $totalPix) {
    echo '<a href="gallery.php?curPage=' . ($curPage+1) . '"> Next ></a>';
} else {
    // otherwise leave the cell empty
    echo '&nbsp;';
}
?>
</td></tr>
</table>

看起来很多,但是代码分成三部分:第一部分在$curPage大于0时创建一个反向链接;如果有两列以上,第二个用空单元格填充最后一个表格行;第三个使用与前面相同的公式($startRow+SHOWMAX < $totalPix)来决定是否显示一个前向链接。

确保链接中的引号组合正确。另一点需要注意的是,$curPage-1$curPage+1的计算用括号括起来,以避免数字后面的句点被误解为小数点。它在这里用作连接操作符,连接查询字符串的各个部分。

  1. 现在,您需要将当前页面的值添加到缩略图周围链接中的查询字符串中。找到这段代码(在第 96 行周围):
<a href="gallery.php?image=<?= safe($row['filename']) ?>">

像这样改变它:

<a href="gallery.php?image=<?= safe($row['filename']) ?>&amp;curPage=<?= $curPage ?>">

您希望在单击缩略图时显示相同的子集,所以只需通过查询字符串传递当前的值$curPage

Caution

所有代码必须在同一行,结束 PHP 标签和&amp;之间没有空格。这段代码创建 URL 和查询字符串,其中不能有空格。

img/332054_5_En_14_Fig6_HTML.jpg

图 14-6

页面导航系统现在已经完成

  1. 保存页面并测试它。点击下一个链接,你应该会看到剩下的缩略图子集,如图 14-6 所示。没有更多的图像显示,所以下一个链接消失了,但在缩略图网格的左下方有一个上一个链接。画廊顶部的记录计数器现在反映了正在显示的缩略图的范围,如果您单击右边的缩略图,相同的子集将保留在屏幕上,同时显示适当的大图像。你完蛋了!

您可以对照gallery_mysqli_08.phpgallery_pdo_08.php检查您的代码。

章节回顾

在短短的几页中,你已经把一个枯燥的文件名列表变成了一个动态的在线图库,并配有一个页面导航系统。所有需要做的就是为每个主要图像创建一个缩略图,将两个图像上传到适当的文件夹,并将文件名和标题添加到数据库中的images表中。只要数据库不断更新imagesthumbs文件夹的内容,你就拥有了一个动态图库。不仅如此,您还学习了如何选择记录子集、通过查询字符串链接到相关信息,以及构建页面导航系统。

你使用 PHP 越多,你越会意识到技巧不在于记住如何使用许多晦涩的函数,而在于找出让 PHP 做你想做的事情所需的逻辑。这是一个如果这个,做那个的问题;如果是别的,做点不一样的。一旦您可以预见到某个情况可能发生的情况,您通常就可以构建代码来处理它。

到目前为止,您已经专注于从一个简单的数据库表中提取记录。在下一章,我将向您展示如何插入、更新和删除材料。