PHP 如何连接 MySQL:从配置到查询的完整示例

PHP 如何连接 MySQL:从配置到查询的完整示例

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 参数和错误处理分清楚,往往比单纯让一段连接代码“跑起来”更重要。

© 版权声明

相关文章