PHP 怎么写CRD

wen PHP项目 2

PHP 中如何优雅地实现 CRUD:从零到生产级的最佳实践

目录导读

  1. CRUD 的本质与 PHP 的定位
  2. 环境准备与数据库连接(PDO 篇)
  3. CREATE(创建)——安全插入数据的三种姿势
  4. READ(读取)——查询的艺术与防注入
  5. UPDATE(更新)——动态字段更新的陷阱
  6. DELETE(删除)——软删除与硬删除的抉择
  7. CRUD 封装成类——告别重复代码
  8. 常见问答(FAQ)

CRUD 的本质与 PHP 的定位

CRUD(Create, Read, Update, Delete)是绝大多数 Web 应用的基石,无论是博客、电商还是企业管理后台,底层都是对数据库进行的这四种基本操作,PHP 作为服务端脚本语言,天然适合处理 HTTP 请求并与 MySQL/PostgreSQL 等数据库交互。

PHP 怎么写CRD

关键点:现代 PHP(7.4+ / 8.x)已经拥有非常成熟的 PDO(PHP Data Objects)扩展,它提供了统一的数据库访问接口,并内置预处理语句(Prepared Statements),这是防 SQL 注入的第一道防线。任何不经过预处理拼接 SQL 的 CRUD 都是不负责任的。


环境准备与数据库连接(PDO 篇)

确保你的 php.ini 中启用了 pdo_mysql 扩展,使用 PDO 连接数据库的标准姿势:

<?php
$host = '127.0.0.1';
$db   = 'test_db';
$user = 'root';
$pass = 'secret';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$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, $user, $pass, $options);
} catch (PDOException $e) {
    // 生产环境记录错误日志,不要直接输出 $e->getMessage()
    error_log($e->getMessage());
    exit('数据库连接失败');
}
?>

注意utf8mb4utf8 支持更多字符(如 emoji),强烈推荐。


CREATE(创建)——安全插入数据的三种姿势

1 使用命名占位符(推荐)

$sql = "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
    ':name' => $username,
    ':email' => $email,
    ':age' => $age,
]);
$lastId = $pdo->lastInsertId(); // 获取自增ID

2 使用问号占位符

$sql = "INSERT INTO users (name, email) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$username, $email]);

3 批量插入的高效方式

$pdo->beginTransaction();
try {
    $sql = "INSERT INTO logs (message) VALUES (?)";
    $stmt = $pdo->prepare($sql);
    foreach ($logMessages as $msg) {
        $stmt->execute([$msg]);
    }
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
}

注意execute() 中的参数数组值会被自动转为字符串,如果字段是整数且比较敏感,可以显式 bindValue 并指定类型。


READ(读取)——查询的艺术与防注入

1 单行查询

$sql = "SELECT * FROM users WHERE email = :email LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([':email' => $inputEmail]);
$user = $stmt->fetch(); // 返回 false 表示未找到

2 多行查询与分页

$page = max(1, (int)$_GET['page'] ?? 1);
$perPage = 10;
$offset = ($page - 1) * $perPage;
$sql = "SELECT * FROM articles ORDER BY created_at DESC LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$articles = $stmt->fetchAll();

3 LIKE 查询的陷阱

$search = "%{$keyword}%";
$sql = "SELECT * FROM products WHERE name LIKE :kw";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':kw', $search);
$stmt->execute();

不要将 直接拼在 SQL 字符串里,而是作为参数值传入。


UPDATE(更新)——动态字段更新的陷阱

最常见的问题是:用户可能只修改其中一个字段,如何动态构建 SET 子句?

1 动态字段更新(安全写法)

$fields = [];
$params = [];
if (!empty($data['name'])) {
    $fields[] = "name = :name";
    $params[':name'] = $data['name'];
}
if (isset($data['age'])) {
    $fields[] = "age = :age";
    $params[':age'] = (int)$data['age'];
}
if (empty($fields)) {
    return false; // 没有要更新的字段
}
$params[':id'] = $id;
$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = :id";
$stmt = $pdo->prepare($sql);
return $stmt->execute($params);

2 影响行数的判断

$rowCount = $stmt->rowCount();
// 注意:如果数据未发生变化,rowCount() 可能返回 0,这不一定是错误

DELETE(删除)——软删除与硬删除的抉择

1 硬删除(物理删除)

$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);

2 软删除(推荐生产环境)

在表中增加 deleted_at TIMESTAMP NULL DEFAULT NULL 字段。

$sql = "UPDATE users SET deleted_at = NOW() WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);
// 查询时默认过滤软删除数据
$sql = "SELECT * FROM users WHERE deleted_at IS NULL";

好处:可恢复、保留审计轨迹、避免外键约束断裂。代价:每次查询需额外条件。


CRUD 封装成类——告别重复代码

面向对象封装是生产级代码的核心,一个简单的 BaseModel 示例:

abstract class BaseModel {
    protected PDO $pdo;
    protected string $table;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    public function create(array $data): int {
        $keys = array_keys($data);
        $fields = implode(', ', $keys);
        $placeholders = ':' . implode(', :', $keys);
        $sql = "INSERT INTO {$this->table} ($fields) VALUES ($placeholders)";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($data);
        return (int)$this->pdo->lastInsertId();
    }
    public function find(int $id): ?array {
        $sql = "SELECT * FROM {$this->table} WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':id' => $id]);
        $result = $stmt->fetch();
        return $result ?: null;
    }
    public function update(int $id, array $data): bool {
        $fields = [];
        foreach ($data as $key => $value) {
            $fields[] = "$key = :$key";
        }
        $sql = "UPDATE {$this->table} SET ".implode(', ', $fields)." WHERE id = :id";
        $data['id'] = $id;
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute($data);
    }
    public function delete(int $id): bool {
        $sql = "DELETE FROM {$this->table} WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute([':id' => $id]);
    }
}

进阶:可以引入 Repository 模式或 Query Builder(如 Laravel 的 Eloquent),但在小型项目中,上述基类已经足够清晰复用。


常见问答(FAQ)

Q1: 为什么不能用 mysqlimysql 扩展? A: mysql 扩展已在 PHP 7.0 移除;mysqli 虽能用,但预处理语法繁琐,且面向过程风格易出错,PDO 支持多种数据库(MySQL、PostgreSQL、SQLite),且 API 统一,是主流框架标配。

Q2: 预处理语句真的能完全防注入吗? A: 是的,只要都通过参数绑定,且表名、字段名不能通过参数绑定,必须走白名单验证。ORDER BY 字段,建议使用数组映射。

Q3: 为什么 rowCount() 在 UPDATE 时返回 0? A: 因为如果更新后的值与原始值相同,MySQL 默认返回受影响行数为 0,这是正常的,不能据此判断是否成功,可以改用 execute() 返回布尔值,或使用 SELECT ROW_COUNT() 更精确。

Q4: 数据库连接每次请求都新建,性能太差怎么办? A: 使用长连接PDO::ATTR_PERSISTENT => true),但需注意并发场景下的连接数控制,更推荐使用连接池(如 Swoole 或 PHP-FPM + ProxySQL)。

Q5: 如何处理 CRUD 中的事务性操作(如转账)? A: 使用 beginTransaction()commit()rollBack(),务必在 try-catch 中确保异常时回滚,并设置 PDO::ATTR_ERRMODE 为异常模式。


PHP 写 CRUD 的核心不是语法,而是安全意识(预处理)、结构设计(封装复用)和性能考量(批量操作与索引),掌握 PDO 一劳永逸,配合现代 PHP 框架(Laravel/Symfony)的 ORM 可以更快,但底层原理必然相通,希望本文能帮你从"会写"进阶到"写好"。

抱歉,评论功能暂时关闭!