第 5 章:PHP 连接 MySQL:使用 mysqli 执行数据库操作
章节介绍
学习目标
- 理解 PHP 与 MySQL 交互的必要性,掌握 mysqli 扩展的两种基本使用方式。
- 能够独立编写 PHP 脚本,建立并管理与 MySQL 数据库的连接。
- 熟练使用 PHP 执行
SELECT查询,并能将结果集处理为可用的 PHP 数据(如关联数组)。 - 掌握使用 PHP 执行
INSERT、UPDATE、DELETE等数据操作语句的方法。 - 深刻理解 SQL 注入攻击的原理,并强制掌握使用预处理语句(Prepared Statements)进行安全防范的标准实践。
在本教程中的作用
本章是理论走向实战的枢纽 和安全基石 。前几章我们学会了在 MySQL 环境中直接操作数据,而本章将赋予 PHP 程序"生命力",使其能够动态地存取后端数据库中的数据,从而构建出真正的动态网站。此外,本章重点灌输的安全编程思想,是每一位合格开发者必须刻入 DNA 的准则,直接关系到应用的安全性。
与前面章节的衔接
本章将直接应用第 2、3 章所学的 SQL 知识。您将不再于 MySQL 命令行中手动输入 SQL,而是改为在 PHP 脚本中编写这些 SQL 语句,并通过 PHP 发送给 MySQL 服务器执行,最后在 PHP 中处理返回的结果。第 4 章搭建的环境是本章所有代码的运行基础。
本章主要内容概览
我们将从建立数据库连接开始,逐步学习执行查询与操作语句,最终聚焦于防范 SQL 注入的预处理语句。本章将以一个完整的"用户管理"实战项目贯穿始终,巩固 CRUD 操作的安全实现流程。
核心概念讲解
1. 为什么需要 PHP 连接数据库?
静态网站的內容是固定的 HTML 文件。而动态网站(如新闻站、电商平台)的内容(如文章、商品信息)存储在数据库中。PHP 的角色是"中间人"或"大脑",它接收用户请求(如访问某个文章页面),根据请求向数据库查询对应的数据,然后将数据和 HTML 模板结合,生成最终的、包含动态内容的网页发送给用户。没有数据库连接的 PHP,只能处理静态逻辑;没有 PHP 的数据库,只是一潭无法与 Web 交互的"死水"。
2. mysqli 扩展简介
PHP 提供了多种与 MySQL 交互的扩展,其中mysqli(MySQL Improved)是当前推荐的方式之一,它支持 MySQL 4.1 及以上版本,提供了面向对象和面向过程两种编程接口。
- 面向对象风格:代码更清晰、易于维护,是现代 PHP 开发的主流方式,本章将以此为重点。
- 面向过程风格 :类似于旧的
mysql扩展,函数式调用,在某些简单场景或遗留代码中可见。
3. 数据库连接与连接参数
建立连接是第一步,需要四个关键参数:
- 主机名(Host) :MySQL 服务器地址。本地开发通常是
localhost或127.0.0.1。 - 用户名(Username) :拥有数据库访问权限的用户,如
root。 - 密码(Password):对应用户的密码。
- 数据库名(Database) :要操作的具体数据库名称。
最佳实践 :永远不要在代码中硬编码这些敏感信息!应将其存储在配置文件(如config.php)中,并通过include引入,或使用环境变量管理。
4. 执行 SQL 语句的流程
一个典型的数据库操作遵循以下流程:
- 建立连接 :创建
mysqli对象。 - 准备 SQL:编写 SQL 语句字符串。对于用户输入的数据,必须使用预处理语句。
- 执行 SQL:
SELECT查询:使用query()或预处理语句的execute()方法,返回一个结果集对象。INSERT/UPDATE/DELETE:使用相同方法执行,返回布尔值表示成功与否,可通过方法获取受影响行数。
- 处理结果:
- 对于查询:遍历结果集,提取数据。
- 对于操作:检查是否成功,进行相应业务处理(如提示用户)。
- 释放资源与关闭连接:显式释放结果集内存,并最终关闭数据库连接(或依赖脚本结束自动关闭)。
5. SQL 注入与预处理语句(核心安全概念)
- SQL 注入原理 :攻击者通过在 Web 表单、URL 参数等输入点插入恶意的 SQL 代码,欺骗服务器执行非预期的 SQL 命令。例如,登录时输入用户名
admin' --,可能绕过密码验证。 - 预处理语句(Prepared Statements):这是防御 SQL 注入的终极武器。其原理是将 SQL 语句的结构(模板)与数据分离发送。
- 准备阶段 :发送一个 SQL 模板,其中变量用占位符
?表示。例如:SELECT * FROM users WHERE username = ? AND password = ?。 - 绑定阶段 :将用户输入的数据"绑定"到对应的占位符上。
mysqli会确保这些数据被严格地当作数据处理,而非 SQL 代码的一部分。 - 执行阶段 :执行已绑定数据的语句。
这个过程从根本上杜绝了用户输入"污染"SQL 语句结构的可能性。
代码示例
示例 1:建立数据库连接(面向对象风格)
php
<?php
// config.php - 数据库配置文件,应置于Web目录外或通过.htaccess保护
define('DB_HOST', 'localhost');
define('DB_USER', 'your_username'); // 替换为你的数据库用户名
define('DB_PASS', 'your_password'); // 替换为你的数据库密码
define('DB_NAME', 'php_tutorial'); // 替换为你在第1章创建的数据库名
// connect.php - 建立数据库连接并处理错误
header('Content-Type: text/html; charset=utf-8'); // 确保中文正确显示
// 通过mysqli构造函数建立连接
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// 检查连接是否成功 (面向对象风格使用 connect_error 属性)
if ($mysqli->connect_error) {
// 在实际生产环境中,应记录错误日志,并向用户展示友好信息
// die() 会终止脚本执行,仅用于开发调试
die('数据库连接失败: (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
} else {
echo "数据库连接成功!<br>";
// 设置客户端字符集为UTF-8,避免中文乱码
if (!$mysqli->set_charset("utf8mb4")) {
printf("加载字符集utf8mb4失败: %s<br>", $mysqli->error);
} else {
echo "当前字符集: " . $mysqli->character_set_name() . "<br>";
}
}
// 后续的数据库操作代码将放在这里...
// 脚本结束时连接会自动关闭,但显式关闭是好习惯
// $mysqli->close();
?>
预期输出:
数据库连接成功!
当前字符集: utf8mb4
注意 :请将 DB_USER, DB_PASS, DB_NAME 替换为您自己环境的实际值。
示例 2:执行 SELECT 查询并处理结果集
假设我们有一个在第 2 章创建的 users 表。
php
<?php
// 包含数据库连接文件
require_once 'connect.php'; // 假设connect.php已包含config.php并创建了$mysqli对象
// 要执行的SQL查询语句
$sql = "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC";
// 执行查询
$result = $mysqli->query($sql);
// 检查查询是否成功执行
if ($result === false) {
die('查询失败: ' . $mysqli->error); // 输出错误信息
}
// 检查是否有数据返回
if ($result->num_rows > 0) {
echo "<h2>用户列表</h2>";
echo "<table border='1' cellpadding='10'>";
echo "<tr><th>ID</th><th>用户名</th><th>邮箱</th><th>注册时间</th></tr>";
// 使用 fetch_assoc() 逐行获取关联数组
while ($row = $result->fetch_assoc()) {
echo "<tr>";
echo "<td>" . htmlspecialchars($row['id']) . "</td>"; // 使用htmlspecialchars防止XSS
echo "<td>" . htmlspecialchars($row['username']) . "</td>";
echo "<td>" . htmlspecialchars($row['email']) . "</td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
echo "</tr>";
}
echo "</table>";
echo "共找到 " . $result->num_rows . " 条记录。";
} else {
echo "没有找到任何用户。";
}
// 释放结果集占用的内存
$result->free();
// 关闭连接 (connect.php中可能已关闭,这里演示)
// $mysqli->close();
?>
示例 3:执行 INSERT 操作(不安全的方式 - 用于对比演示风险)
php
<?php
// 不安全示例:直接拼接用户输入到SQL中
require_once 'connect.php';
// 模拟从表单接收的数据
$_POST['username'] = 'new_user';
$_POST['email'] = 'user@example.com';
// 假设密码已使用password_hash()加密,这里简单演示
$_POST['password_hash'] = password_hash('mypassword', PASSWORD_DEFAULT);
// !!! 危险操作:直接拼接变量 !!!
$username = $_POST['username'];
$email = $_POST['email'];
$password_hash = $_POST['password_hash'];
$sql = "INSERT INTO users (username, email, password_hash) VALUES ('$username', '$email', '$password_hash')";
if ($mysqli->query($sql) === true) {
echo "新用户插入成功,ID为: " . $mysqli->insert_id; // 获取自增ID
} else {
echo "插入失败: " . $mysqli->error;
}
?>
示例 4:使用预处理语句执行 INSERT(安全的方式)
php
<?php
// 安全示例:使用预处理语句
require_once 'connect.php';
// 模拟从表单接收的数据
$username = 'safe_user';
$email = 'safe@example.com';
$password_hash = password_hash('secure123', PASSWORD_DEFAULT);
// 1. 准备SQL语句模板,使用 ? 作为占位符
$sql = "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)";
// 2. 准备预处理语句
$stmt = $mysqli->prepare($sql);
if ($stmt === false) {
die('准备语句失败: ' . $mysqli->error);
}
// 3. 绑定参数('sss'表示三个参数都是字符串类型)
// 类型说明: i-整数, d-浮点数, s-字符串, b-二进制数据
$stmt->bind_param('sss', $username, $email, $password_hash);
// 4. 执行语句
if ($stmt->execute()) {
echo "用户安全插入成功,ID为: " . $stmt->insert_id;
} else {
echo "执行失败: " . $stmt->error;
}
// 5. 关闭预处理语句
$stmt->close();
?>
示例 5:使用预处理语句执行 SELECT 与 UPDATE(综合)
php
<?php
// 综合示例:查询用户并更新其邮箱
require_once 'connect.php';
// --- 第一部分:使用预处理语句查询特定用户 ---
$search_username = 'safe_user';
$sql_select = "SELECT id, email FROM users WHERE username = ?";
$stmt_select = $mysqli->prepare($sql_select);
$stmt_select->bind_param('s', $search_username);
$stmt_select->execute();
// 将结果绑定到变量
$stmt_select->bind_result($user_id, $old_email);
$stmt_select->fetch(); // 获取一行数据
echo "找到用户: ID=$user_id, 旧邮箱=$old_email <br>";
$stmt_select->close();
// --- 第二部分:使用预处理语句更新该用户的邮箱 ---
if ($user_id) {
$new_email = 'updated@example.com';
$sql_update = "UPDATE users SET email = ? WHERE id = ?";
$stmt_update = $mysqli->prepare($sql_update);
$stmt_update->bind_param('si', $new_email, $user_id); // 注意类型:字符串和整数
if ($stmt_update->execute()) {
echo "邮箱更新成功,影响行数: " . $stmt_update->affected_rows;
} else {
echo "更新失败: " . $stmt_update->error;
}
$stmt_update->close();
}
$mysqli->close();
?>
实战项目:简易用户管理后台(CRUD)
本项目将创建一个具有基本增删改查功能的用户管理页面,强制使用预处理语句确保安全。
项目需求分析
- 功能需求:
- 列表展示 (Read):在一个页面以表格形式展示所有用户信息(ID, 用户名, 邮箱, 注册时间)。
- 新增用户 (Create):通过表单提交,将新用户安全地插入数据库。
- 编辑用户 (Update):点击列表中的"编辑"链接,跳转到表单页预填用户信息,修改后提交更新。
- 删除用户 (Delete):点击列表中的"删除"链接,弹出确认框后删除对应用户。
- 非功能需求:
- 所有数据库操作必须使用预处理语句防范 SQL 注入。
- 输出到 HTML 页面的动态数据必须使用
htmlspecialchars()过滤,防范 XSS。 - 代码结构清晰,包含基本的错误处理。
技术方案与文件结构
/user_admin/
├── config.php # 数据库配置(敏感信息)
├── connect.php # 数据库连接与通用函数
├── index.php # 用户列表展示页(主页面)
├── create.php # 新增用户页面
├── edit.php # 编辑用户页面
└── delete.php # 删除用户处理脚本
分步骤实现
步骤 1:创建配置文件与通用连接文件
config.php:
php
<?php
// 数据库配置常量
define('DB_HOST', 'localhost');
define('DB_USER', 'your_username');
define('DB_PASS', 'your_password');
define('DB_NAME', 'php_tutorial');
?>
connect.php:
php
<?php
// 包含配置
require_once 'config.php';
// 启用错误报告(开发环境)
error_reporting(E_ALL);
ini_set('display_errors', 1);
// 建立数据库连接
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($mysqli->connect_error) {
die('连接数据库失败,请检查配置。错误代码:' . $mysqli->connect_errno);
}
// 设置字符集
$mysqli->set_charset("utf8mb4");
/**
* 安全获取GET/POST参数,并过滤XSS
* @param string $key 参数键名
* @param mixed $default 默认值
* @return mixed 过滤后的值
*/
function getInput($key, $default = '') {
$value = $default;
if (isset($_POST[$key])) {
$value = $_POST[$key];
} elseif (isset($_GET[$key])) {
$value = $_GET[$key];
}
// 去除两端空格,防止无意义的空白字符
$value = trim($value);
// 使用htmlspecialchars转换特殊字符,防止XSS(用于输出时)
// 注意:这里返回的是转义后的字符串,适用于直接echo。如果用于数据库查询,预处理语句会处理。
// 更佳实践是在输出时进行htmlspecialchars,这里返回原始值用于绑定。
return $value;
}
?>
步骤 2:实现用户列表页 (index.php)
php
<?php
require_once 'connect.php';
$page_title = "用户管理后台";
include_once 'header.php'; // 假设有一个简单的HTML头部文件
?>
<h1>用户列表</h1>
<p><a href="create.php" class="btn">添加新用户</a></p>
<?php
// 查询所有用户
$sql = "SELECT id, username, email, created_at FROM users ORDER BY id DESC";
$result = $mysqli->query($sql);
if ($result && $result->num_rows > 0) {
?>
<table border="1" cellspacing="0" cellpadding="8" width="100%">
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>操作</th>
</tr>
<?php while ($user = $result->fetch_assoc()): ?>
<tr>
<td><?php echo htmlspecialchars($user['id']); ?></td>
<td><?php echo htmlspecialchars($user['username']); ?></td>
<td><?php echo htmlspecialchars($user['email']); ?></td>
<td><?php echo htmlspecialchars($user['created_at']); ?></td>
<td>
<a href="edit.php?id=<?php echo urlencode($user['id']); ?>">编辑</a> |
<a href="delete.php?id=<?php echo urlencode($user['id']); ?>"
onclick="return confirm('确定要删除用户 "<?php echo addslashes(htmlspecialchars($user['username'])); ?>" 吗?此操作不可恢复!');">删除</a>
</td>
</tr>
<?php endwhile; ?>
</table>
<?php
$result->free();
} else {
echo "<p>暂无用户数据。</p>";
}
?>
<?php
// 关闭连接
$mysqli->close();
include_once 'footer.php'; // 假设有一个简单的HTML底部文件
?>
步骤 3:实现新增用户页 (create.php)
php
<?php
require_once 'connect.php';
$page_title = "添加新用户";
include_once 'header.php';
// 处理表单提交
$message = '';
$message_type = ''; // success, error
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = getInput('username');
$email = getInput('email');
$password = getInput('password');
// 简单的后端验证
$errors = [];
if (empty($username)) $errors[] = "用户名不能为空";
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "邮箱格式无效";
if (empty($password)) $errors[] = "密码不能为空";
if (empty($errors)) {
// 对密码进行哈希处理
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// 使用预处理语句插入
$sql = "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)";
$stmt = $mysqli->prepare($sql);
if ($stmt) {
$stmt->bind_param('sss', $username, $email, $password_hash);
if ($stmt->execute()) {
$message = "用户添加成功!";
$message_type = 'success';
// 清空表单字段(可选)
$username = $email = $password = '';
} else {
// 检查是否是唯一约束冲突(如用户名重复)
if ($mysqli->errno == 1062) {
$message = "添加失败:用户名或邮箱已存在。";
} else {
$message = "添加失败:" . $stmt->error;
}
$message_type = 'error';
}
$stmt->close();
} else {
$message = "SQL语句准备失败:" . $mysqli->error;
$message_type = 'error';
}
} else {
$message = implode('<br>', $errors);
$message_type = 'error';
}
}
?>
<h1>添加新用户</h1>
<p><a href="index.php">← 返回用户列表</a></p>
<?php if ($message): ?>
<div class="message <?php echo $message_type; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="post" action="">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" value="<?php echo htmlspecialchars(getInput('username')); ?>" required>
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars(getInput('email')); ?>" required>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<button type="submit">添加用户</button>
</div>
</form>
<?php
$mysqli->close();
include_once 'footer.php';
?>
(编辑页 edit.php 和删除处理页 delete.php 逻辑类似,均使用预处理语句,篇幅所限不在此全文展开,但思路如下:)
edit.php:GET 请求时,根据id参数用预处理语句查询用户信息并预填表单;POST 请求时,用预处理语句执行 UPDATE。delete.php:接收 GET 参数id,用预处理语句执行 DELETE,然后跳转回index.php。
项目测试与部署指南
- 本地测试:
- 确保 Apache/Nginx、PHP、MySQL 服务已启动。
- 将项目文件夹放入 Web 服务器根目录(如
htdocs)。 - 在浏览器访问
http:// localhost/user_admin/index.php。 - 测试添加、编辑、删除、列表功能,并尝试输入包含特殊字符(如
',<script>)的数据,观察是否被正确处理。
- 安全测试:
- 在用户名输入框尝试输入:
test' OR '1'='1,查看是否会引发 SQL 错误或异常结果。在我们的安全代码下,它应被当作普通字符串处理。
- 部署建议:
- 将
config.php移到 Web 目录之外,并通过绝对路径包含。 - 关闭 PHP 错误显示(
display_errors = Off),开启错误日志(log_errors = On)。 - 为数据库用户分配最小必要权限(非 root 用户)。
项目扩展建议
- 增加用户角色(如管理员、普通用户)字段,并在列表和表单中管理。
- 为列表页添加分页功能(使用 SQL
LIMIT子句)。 - 增加搜索功能(按用户名或邮箱模糊查询)。
- 为删除操作添加 CSRF 令牌防护。
最佳实践
1. 数据库操作安全黄金法则
- 永远使用预处理语句 :这是防御 SQL 注入唯一可靠的方法。不要使用字符串拼接,即使你使用了
mysqli_real_escape_string(),在某些边缘情况下也可能不安全。 - 最小权限原则 :PHP 连接数据库使用的账号,只赋予其应用所需的最小权限(如
SELECT,INSERT,UPDATE,DELETE),不要使用root账号。 - 错误处理:生产环境禁止向用户显示原始数据库错误信息(可能泄露表结构),应记录到日志文件,并向用户展示通用友好提示。
2. 代码组织与性能
- 分离配置 :数据库凭据等敏感信息必须放在独立配置文件(如
config.php)中,并确保该文件不在 Web 可访问目录内,或通过.htaccess文件禁止访问。 - 重用连接:一个脚本生命周期内,避免重复创建数据库连接。通常一个请求一个连接足矣。
- 及时释放资源 :对于大的结果集,使用
$result->free()或$stmt->close()及时释放内存。脚本结束时会自动关闭连接,但显式调用$mysqli->close()是良好习惯。 - 使用持久连接需谨慎 :
mysqli支持持久连接(p:前缀),在特定高并发场景可能有益,但管理不当易导致连接堆积,一般不建议初学者使用。
3. 输入验证与输出转义
- 输入验证:在绑定到 SQL 之前,根据业务逻辑验证数据(如邮箱格式、必填字段、长度限制)。预处理语句防注入,但不负责业务验证。
- 输出转义 :任何从数据库取出并要输出到 HTML 页面的数据,都必须使用
htmlspecialchars($string, ENT_QUOTES, 'UTF-8')进行转义,以防范 XSS 攻击。这是与 SQL 注入并列的常见安全漏洞。
4. 具体安全漏洞案例与防护
案例一:SQL 注入(已详细讲解)
- 攻击代码 :
$username = "admin' -- ";拼接到"SELECT * FROM users WHERE username = '$username' AND password = '$pass'"中,导致密码验证被注释掉。 - 防护代码 :必须使用如本章所示的预处理语句(
bind_param)。
案例二:XSS 跨站脚本攻击
- 攻击场景 :用户将昵称设置为
<script>alert('黑客');</script>。如果不加处理直接输出到页面,其他用户查看时就会执行该脚本。 - 防护代码:
php
// 错误:直接输出
echo $user['nickname']; // 危险!
// 正确:输出前转义
echo htmlspecialchars($user['nickname'], ENT_QUOTES, 'UTF-8');
案例三:CSRF 跨站请求伪造
- 攻击场景 :用户登录了银行网站 A,未退出。访问了恶意网站 B,B 网站中有一个隐藏的图片标签
<img src="http:// bank.com/transfer?to=hacker&amount=1000">,导致用户在不知情下向黑客转账。 - 防护方案:
- 使用 CSRF 令牌(Token)。
- 关键操作使用 POST 请求而非 GET。
- 检查 HTTP Referer 头(不完全可靠)。
php
// 生成并存储Token在Session中
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// 在表单中隐藏该Token
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
// 处理表单时验证Token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('非法请求,CSRF验证失败!');
}
练习题与挑战
基础练习题
- 连接与简单查询【难度:★】
- 题目 :编写一个 PHP 脚本
list_products.php。假设你有一个名为shop的数据库,其中包含products表(字段:id,name,price,stock)。脚本需要连接数据库,查询所有库存大于 0 的商品,并以 HTML 列表形式显示商品名和价格。
- 题目 :编写一个 PHP 脚本
- 提示 :参照示例 2,SQL 语句为
SELECT name, price FROM products WHERE stock > 0。 - 参考答案要点 :包含连接文件,执行查询,循环
fetch_assoc(),使用htmlspecialchars输出。
- 预处理语句插入【难度:★★】
- 题目 :编写一个脚本
add_product.php,包含一个 HTML 表单(商品名、价格、库存)。表单提交后,使用预处理语句将数据安全地插入到products表中。
- 题目 :编写一个脚本
- 提示 :参照示例 4 和实战项目的
create.php。注意价格可能是浮点数,绑定参数时使用'sdi'(字符串、浮点数、整数)或'sdd'。 - 参考答案要点 :表单使用 POST 方法,后端验证输入,使用
prepare和bind_param,处理成功或失败信息。
进阶练习题
- 用户登录验证【难度:★★★】
- 题目 :基于本章的
users表(假设已有password_hash字段),创建一个登录页面login.php。用户输入用户名和密码后,使用预处理语句根据用户名查询出密码哈希值,然后用password_verify()函数验证密码。成功后,将用户 ID 存入$_SESSION并跳转到欢迎页面。
- 题目 :基于本章的
- 提示 :
SELECT password_hash FROM users WHERE username = ?。password_verify($input_password, $hashed_password_from_db)返回布尔值。 - 参考答案要点:启动 Session,预处理查询,验证密码,设置 Session 变量,重定向。
- 带搜索和分页的列表【难度:★★★☆】
- 题目 :改进
index.php的用户列表。1) 增加一个搜索框,可以按用户名模糊搜索(LIKE)。2) 为列表实现简单分页,每页显示 5 条记录。
- 题目 :改进
- 提示:
- 搜索:
WHERE username LIKE ?,绑定参数为"%{$keyword}%"。 - 分页:SQL 中使用
LIMIT ?, ?,第一个参数是偏移量($page-1)*$per_page,第二个参数是$per_page。绑定参数时注意是'ii'(两个整数)。需要计算总记录数以生成分页链接。 - 参考答案要点 :处理 GET 参数
keyword和page,构造动态 WHERE 子句,计算总页数,使用预处理语句执行分页查询。
综合挑战题
- 完整博客文章管理模块【难度:★★★★】
- 题目 :为你下一章将构建的博客系统,提前实现文章管理的后端 PHP 部分。创建
articles表(id, title, content, author_id, category_id, created_at)。编写四个脚本:
- 题目 :为你下一章将构建的博客系统,提前实现文章管理的后端 PHP 部分。创建
article_list.php:显示文章列表(标题、作者、分类、时间)。article_create.php:发布新文章的表单和处理。article_edit.php:编辑已有文章。article_delete.php:删除文章。- 要求 :必须使用预处理语句。
author_id和category_id可以用固定值或从users,categories表关联(关联查询是下一章重点,这里可用简单下拉框选择固定 ID)。思考如何安全地处理富文本内容(content)的存储和 XSS 防护。 - 解题思路 :这是对本章所有知识点的综合运用。关键在于设计好各个脚本的数据流和安全处理。对于富文本内容,直接使用
htmlspecialchars会破坏格式,需要考虑使用白名单过滤的 HTML 净化库(如 HTML Purifier),或将内容以原始格式存储,在显示时确保其来自可信来源。
章节总结
重点知识回顾
- 连接数据库 :使用
new mysqli()建立连接,并务必检查connect_error和处理字符集。 - 执行查询 :
$mysqli->query()用于简单查询,配合fetch_assoc()等函数遍历结果集。 - 执行操作 :
INSERT,UPDATE,DELETE使用query()或预处理语句执行,通过affected_rows和insert_id获取影响。 - 安全核心------预处理语句 :这是本章的重中之重 。通过
prepare(),bind_param(),execute()三步,彻底免疫 SQL 注入。务必养成习惯,对所有涉及变量的 SQL 操作使用此方式。 - 辅助安全------输出转义 :使用
htmlspecialchars()对输出到 HTML 的动态数据进行转义,防范 XSS。
技能掌握要求
完成本章学习与实践后,您应该能够:
- 在 PHP 中编写安全的数据库连接代码。
- 使用预处理语句自信地完成数据的增、删、改、查操作。
- 解释 SQL 注入的原理,并能演示为何预处理语句可以防御它。
- 构建一个包含基本 CRUD 功能、具备安全意识的简单数据管理后台。
进一步学习建议
- 深入 mysqli :探索
multi_query()(多查询)、存储过程、事务处理(begin_transaction,commit,rollback)等高级特性。 - 了解 PDO:PHP 另一个主流数据库抽象层 PDO,支持多种数据库,其预处理语句的用法与 mysqli 略有不同,也是值得学习的技能。
- 为实战做准备 :下一章,我们将把数据库操作与更复杂的业务逻辑结合,设计多张关联表,并使用
JOIN查询,最终构建出一个功能更完整的博客系统。请确保本章的基础已牢固掌握。