
PHP 如何连接 MySQL:从配置到查询的完整示例
PHP 连接 MySQL,常见用途包括读取网站内容、保存表单数据、管理用户账号,以及为后台系统提供数据接口。连接本身并不复杂,真正需要认真处理的是数据库驱动是否启用、字符集是否一致、SQL 参数如何绑定、异常如何记录,以及哪些内容不能通过占位符传入。
本文以 PHP 的 PDO 扩展为主,给出可逐步运行的示例,再介绍 mysqli 的对应做法。示例假设使用 MySQL 8.x;较早版本的 MySQL 和 MariaDB 通常也能使用,但认证方式、SQL 功能和服务器配置可能不同。代码里的数据库名、用户名、密码需要替换成当前环境中的实际值。
一、先确认运行环境
PHP 需要安装并启用与 MySQL 通信的扩展。使用 PDO 时,通常需要启用 PDO 和 pdo_mysql;使用 mysqli 时,则需要启用 mysqli。可在命令行运行以下命令检查:
php -v
php -m
在模块列表中查找 PDO、pdo_mysql 或 mysqli。不同操作系统的安装方式不同;某些环境需要单独安装 PHP 的 MySQL 驱动包,另一些环境则通过 PHP 安装包或扩展配置启用。检查命令行 PHP 并不能保证网站所用的 PHP-FPM 或 Web 服务器也启用了相同扩展:命令行和网站可能使用不同版本、不同配置文件,应分别确认。
数据库也必须允许 PHP 所在机器连接。开发环境中,PHP 与 MySQL 常运行在同一主机;Docker 或云环境中,数据库主机名通常是服务名或内网地址,而不是 localhost。还需确认数据库已创建、MySQL 正在监听正确端口,并且账户拥有目标数据库所需的权限。
二、准备数据库和测试表
以下 SQL 可用于创建一个简单的文章表。请通过 MySQL 客户端或数据库管理工具执行;账户需要有相应的建库和建表权限。
CREATE DATABASE app_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
USE app_demo;
CREATE TABLE articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB;
utf8mb4 可以存储完整的 Unicode 字符,包括常见的表情符号。数据库、表、连接以及 PHP 文件的字符编码应保持兼容;只设置 PHP 文件为 UTF-8,并不能代替正确设置数据库连接字符集。utf8mb4_0900_ai_ci 是 MySQL 8.0 提供的排序规则;若使用不支持它的 MySQL 或 MariaDB 版本,可改用该服务器支持的 utf8mb4 排序规则。
三、使用 PDO 建立连接
PDO 提供统一的数据库访问接口,但连接 MySQL 仍需要安装 pdo_mysql 驱动。连接信息包括主机、端口、数据库名、用户名和密码。建议通过环境变量或受保护的配置文件提供凭据,不要把生产环境密码直接写入公开仓库。
<?php
declare(strict_types=1);
$dbHost = getenv('DB_HOST') ?: '127.0.0.1';
$dbPort = getenv('DB_PORT') ?: '3306';
$dbName = getenv('DB_NAME') ?: 'app_demo';
$dbUser = getenv('DB_USER') ?: 'app_user';
$dbPass = getenv('DB_PASS') ?: '';
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
$dbHost,
$dbPort,
$dbName
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $dbUser, $dbPass, $options);
echo '连接成功';
} catch (PDOException $e) {
// 开发环境可以查看详细信息;生产环境不要将异常原文返回给访客。
error_log($e->getMessage());
http_response_code(500);
exit('数据库连接失败');
}
这里的 host、port、dbname 和 charset 属于 PDO MySQL DSN 的连接参数。使用 127.0.0.1 时通常通过 TCP 连接;localhost 在不同系统和驱动配置下可能采用不同的本地连接方式。若连接方式影响到连接结果,先核实运行环境、监听地址和数据库用户的授权主机。
PDO::ATTR_ERRMODE 设置为 PDO::ERRMODE_EXCEPTION 后,数据库错误会以异常形式抛出,便于统一处理。PDO::ATTR_DEFAULT_FETCH_MODE 让默认查询结果以关联数组返回。关闭模拟预处理并使用 MySQL 原生预处理通常是合适的默认选择;但这不意味着所有 SQL、服务器版本或驱动组合都完全相同,部署时仍应在目标环境中测试。
示例使用环境变量读取配置。比如在 Linux 或 macOS 的当前 shell 中,可先设置变量再启动 PHP 程序:
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_NAME=app_demo
export DB_USER=app_user
export DB_PASS='replace-with-a-real-password'
php your_script.php
实际部署时,应由 PHP-FPM、容器平台或服务管理器配置环境变量。不要把密码提交到 Git,也不要通过公开的网页输出环境变量。数据库账户应遵循最小权限原则:应用只获得所需数据库和操作的权限,避免使用数据库管理员账户处理日常请求。
四、执行查询:预处理与参数绑定
用户输入不能直接拼接到 SQL 字符串中。下面用命名占位符查询文章。占位符代表的是值,不是 SQL 的任意片段;参数绑定可以避免值被当作 SQL 语法执行,但不能替代业务校验、权限检查或 HTML 输出转义。
<?php
$title = '数据库入门';
$sql = 'SELECT id, title, body, created_at
FROM articles
WHERE title = :title';
$stmt = $pdo->prepare($sql);
$stmt->execute(['title' => $title]);
$article = $stmt->fetch();
if ($article === false) {
echo '没有找到文章';
} else {
echo '文章编号:' . $article['id'];
}
也可以使用问号占位符:
$stmt = $pdo->prepare(
'SELECT id, title FROM articles WHERE id = ?'
);
$stmt->execute([$id]);
$row = $stmt->fetch();
PDO 的 execute() 参数数组适合传递常见字符串和数字值。若需要明确指定参数类型,或需要按引用绑定,可使用 bindValue() 或 bindParam()。二者行为不同:bindValue() 绑定调用时的值;bindParam() 绑定变量,在语句执行时读取变量内容。
$stmt = $pdo->prepare(
'SELECT id, title FROM articles WHERE id = :id'
);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch();
一个常被忽略的边界:占位符只能代表值
占位符不能用于表名、列名、排序方向等 SQL 结构。例如,下面的写法不能可靠地通过绑定参数改变排序列:
// 不要这样做::column 不是可绑定的列名
$stmt = $pdo->prepare(
'SELECT id, title FROM articles ORDER BY :column'
);
需要动态排序时,应把外部输入映射到代码中预先允许的固定选项,再拼接这些可信的 SQL 片段;排序方向同样应通过明确的允许列表选择。
$allowedSorts = [
'newest' => 'created_at DESC',
'title' => 'title ASC',
];
$sortKey = $_GET['sort'] ?? 'newest';
$orderBy = $allowedSorts[$sortKey] ?? $allowedSorts['newest'];
$sql = 'SELECT id, title, created_at
FROM articles
ORDER BY ' . $orderBy;
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll();
这里拼接的只有程序预先写好的片段,而不是未经验证的输入。不要仅靠“过滤特殊字符”来处理动态标识符,也不要把用户提交的表名、列名或排序表达式原样拼进 SQL。
另一个边界:一个占位符不是一组值
预处理参数通常不能把一个数组展开成多个 IN 条件值。需要查询多个 ID 时,应为每个值生成一个占位符,并分别传参;同时要处理空数组的情况,因为 IN () 不是有效的通用写法。
$ids = [2, 5, 8];
if ($ids === []) {
$rows = [];
} else {
$placeholders = implode(
', ',
array_fill(0, count($ids), '?')
);
$stmt = $pdo->prepare(
"SELECT id, title FROM articles WHERE id IN ($placeholders)"
);
$stmt->execute($ids);
$rows = $stmt->fetchAll();
}
此例中的占位符数量由整数数组的长度决定,实际 ID 仍通过参数绑定,不能改为把 ID 字符串直接拼入 SQL。对于数量特别大的数组,还要考虑数据库对语句大小、参数数量和执行时间的限制;这些限制取决于驱动、服务器版本和环境配置,没有一个适用于所有部署的固定上限。
五、完整的新增、查询、更新和删除示例
下面的代码可放在一个 PHP 文件中运行,前提是前面已创建数据库和 articles 表,并已通过环境变量提供有效连接信息。它依次插入一篇文章、取回新记录、修改标题并删除该记录。为方便演示,删除操作会清理本次新增的数据;真实业务中应根据产品需求决定是否允许删除。
<?php
declare(strict_types=1);
$dbHost = getenv('DB_HOST') ?: '127.0.0.1';
$dbPort = getenv('DB_PORT') ?: '3306';
$dbName = getenv('DB_NAME') ?: 'app_demo';
$dbUser = getenv('DB_USER') ?: 'app_user';
$dbPass = getenv('DB_PASS') ?: '';
$dsn = "mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4";
try {
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// 新增
$insert = $pdo->prepare(
'INSERT INTO articles (title, body) VALUES (:title, :body)'
);
$insert->execute([
'title' => 'PDO 示例文章',
'body' => '这是一条由 PHP 写入的测试内容。',
]);
$newId = (int) $pdo->lastInsertId();
// 查询
$select = $pdo->prepare(
'SELECT id, title, body, created_at
FROM articles
WHERE id = :id'
);
$select->execute(['id' => $newId]);
$article = $select->fetch();
if ($article === false) {
throw new RuntimeException('新记录未能读取');
}
echo '新增编号:' . $article['id'] . PHP_EOL;
echo '标题:' . $article['title'] . PHP_EOL;
// 更新
$update = $pdo->prepare(
'UPDATE articles SET title = :title WHERE id = :id'
);
$update->execute([
'title' => 'PDO 示例文章(已更新)',
'id' => $newId,
]);
echo '更新影响行数:' . $update->rowCount() . PHP_EOL;
// 删除演示记录
$delete = $pdo->prepare(
'DELETE FROM articles WHERE id = :id'
);
$delete->execute(['id' => $newId]);
echo '删除影响行数:' . $delete->rowCount() . PHP_EOL;
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('数据库操作失败');
} catch (RuntimeException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('程序处理失败');
}
lastInsertId() 在常见的自增主键场景中可用于取得最近插入记录的 ID;实际行为与数据库连接及表结构有关,应在具体驱动和用法中验证。rowCount() 对增删改语句常用于读取受影响行数,但对于 SELECT 语句,各驱动的行为可能不同,不能把它当作通用的查询结果数量。查询结果数量可通过 fetchAll() 后计算,或使用单独的 COUNT(*) 查询。
示例只演示了如何操作数据库,并未对 HTML 页面输出进行编码。若把数据库内容输出到网页,应根据输出上下文进行转义,例如普通 HTML 文本可使用:
echo htmlspecialchars(
$article['title'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
SQL 参数绑定防护的是 SQL 值的位置,不是浏览器中的 HTML 或 JavaScript。数据库里的文本即使不是用户直接输入,也不应被默认视为可信网页内容。
六、什么时候使用事务
当多个写入操作必须一起成功或一起回滚时,应考虑事务。例如,创建订单和扣减库存通常不能只成功其中一项。事务是否有效,还依赖使用支持事务的存储引擎(例如 InnoDB)以及具体 SQL 操作;不能假设所有 MySQL 表和所有操作都能回滚。
try {
$pdo->beginTransaction();
$orderStmt = $pdo->prepare(
'INSERT INTO orders (customer_id, total_amount)
VALUES (:customer_id, :total_amount)'
);
$orderStmt->execute([
'customer_id' => $customerId,
'total_amount' => $totalAmount,
]);
$stockStmt = $pdo->prepare(
'UPDATE products
SET stock = stock - :quantity
WHERE id = :product_id AND stock >= :quantity'
);
$stockStmt->execute([
'quantity' => $quantity,
'product_id' => $productId,
]);
if ($stockStmt->rowCount() !== 1) {
throw new RuntimeException('商品不存在或库存不足');
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
上例展示了失败时回滚的基本形式。库存更新带有库存条件,避免在库存不足时仍创建成功订单。生产系统还需要结合业务设计处理并发、重复请求和错误日志;有些语句会影响事务边界,应查阅所用 MySQL 版本对该语句的事务行为说明。
七、mysqli 的基本连接与查询
项目如果已使用 mysqli,也可以继续使用,不必只为连接数据库而强行迁移。mysqli 专用于 MySQL;PDO 则提供多个数据库驱动的统一接口。无论选择哪一种,都应使用预处理语句,避免把外部输入拼接进 SQL。
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli(
getenv('DB_HOST') ?: '127.0.0.1',
getenv('DB_USER') ?: 'app_user',
getenv('DB_PASS') ?: '',
getenv('DB_NAME') ?: 'app_demo',
(int) (getenv('DB_PORT') ?: 3306)
);
$mysqli->set_charset('utf8mb4');
$stmt = $mysqli->prepare(
'SELECT id, title FROM articles WHERE id = ?'
);
$id = 1;
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row === null) {
echo '没有找到文章';
} else {
echo '标题:' . $row['title'];
}
$stmt->close();
$mysqli->close();
bind_param() 的类型字符串需要为每个参数指定类型:i 表示整数、d 表示浮点数、s 表示字符串、b 表示二进制数据。它要求参数以变量形式传入;参数数量和类型应与 SQL 占位符对应。get_result() 依赖 mysqlnd,在没有该驱动的环境中不可用;这类环境可改用 bind_result() 和 fetch() 读取结果。
八、常见错误与排查顺序
- 提示找不到驱动或类。 检查网站实际使用的 PHP 是否启用了
pdo_mysql或mysqli,并确认扩展与 PHP 版本匹配。启用或安装扩展后,按运行环境要求重启 PHP-FPM 或 Web 服务。 - 连接被拒绝或连接超时。 核对主机名、端口、容器网络、防火墙以及 MySQL 监听地址。容器中的
127.0.0.1指向当前容器自身,不一定是数据库容器。 - 认证失败。 核对用户名、密码、授权主机和账户权限。MySQL 用户通常与允许连接的主机范围相关;只在客户端配置文件里改密码,不能替代数据库账户的授权配置。
- 找不到数据库或数据表。 检查数据库名、表名、当前连接的默认数据库,以及部署环境是否执行了建表或迁移脚本。大小写行为可能受操作系统和 MySQL 配置影响。
- 中文乱码或写入失败。 检查连接 DSN 中的
charset=utf8mb4(或 mysqli 的set_charset())、表和列的字符集,以及数据列长度和类型。字符集错误与字段过短是不同问题,应分别排查。 - 参数数量不匹配。 确认 SQL 中占位符数量与传入参数数量相同;不要混用命名占位符和位置占位符,也不要试图把一个数组绑定到单个占位符。
- 生产日志暴露敏感信息。 数据库错误信息可能包含 SQL、表名、主机或其他内部细节。详细信息应写入受控日志,对外只返回通用错误提示,并避免记录密码和敏感业务数据。
九、实践建议
对于新 PHP 项目,若没有既有技术栈约束,PDO 是一个实用的默认选择:接口相对统一,预处理和异常处理也便于组织代码。无论使用 PDO 还是 mysqli,都应做到以下几点:
- 连接时明确设置
utf8mb4,并验证数据库与表的字符集设置。 - 对所有外部数据使用预处理参数;动态表名、列名和排序规则只从固定允许列表中选择。
- 数据库账户遵循最小权限原则,开发、测试和生产环境使用合适的独立凭据。
- 对异常进行记录和分类,但不把连接细节、SQL 错误或凭据返回给普通用户。
- 理解事务、参数类型和结果获取方式的边界,并在目标 PHP、驱动和 MySQL 版本上运行测试。
- 在网页输出数据库内容时按 HTML 上下文转义;SQL 安全和网页输出安全是两类问题。
连接 MySQL 的基础流程可以概括为:确认 PHP 驱动已启用,准备数据库与权限,通过 PDO 或 mysqli 建立连接,使用预处理语句执行数据操作,并在需要时用事务维护数据一致性。把配置、SQL 参数和错误处理分清楚,往往比单纯让一段连接代码“跑起来”更重要。




