PHP PDO 实现单封面与多图上传教程
在实际的Web项目开发中,图片上传是非常常见的功能需求,比如文章发布时需要上传单张封面图,同时上传多张配图。本文将结合PHP的PDO数据库扩展,完整讲解如何实现单封面与多图上传的功能,包含前端表单设计、后端逻辑处理、数据库存储以及文件安全校验等完整流程。
一、功能需求与准备
我们要实现的功能如下:
- 支持上传1张封面图片,格式限制为jpg、png、gif,大小不超过2MB
- 支持上传最多5张配图,格式和大小限制与封面一致
- 上传成功后,将图片路径存储到MySQL数据库中,方便后续查询调用
- 对上传文件进行基础安全校验,避免恶意文件上传
开发前需要准备的环境:
- PHP 7.0及以上版本,开启fileinfo扩展用于文件类型校验
- MySQL 5.7及以上版本数据库
- 项目目录下创建uploads文件夹,用于存放上传的图片,注意设置文件夹写入权限
二、数据库表设计
首先我们需要创建一张图片信息表,用来存储上传后的图片路径和关联的业务ID,这里以文章图片为例,表结构如下:
-- 创建文章图片表 CREATE TABLE `article_images` ( `id` int(11) NOT NULL AUTO_INCREMENT, `article_id` int(11) NOT NULL COMMENT '关联的文章ID', `cover_path` varchar(255) DEFAULT NULL COMMENT '封面图路径', `image_path` varchar(255) NOT NULL COMMENT '配图路径', `is_cover` tinyint(1) DEFAULT 0 COMMENT '是否为封面:1是 0否', `upload_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表中is_cover字段用来区分当前图片是封面还是普通配图,方便后续查询时快速筛选封面图。
三、前端上传表单设计
前端需要使用<form>标签实现文件上传,注意表单必须设置enctype属性为multipart/form-data,否则无法正确传递文件数据。同时添加文件选择控件,区分封面和配图的上传入口。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>单封面与多图上传</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<!-- 隐藏字段,传递关联的文章ID,实际项目中可从业务逻辑获取 -->
<input type="hidden" name="article_id" value="1">
<p>上传封面图(仅支持1张):</p>
<input type="file" name="cover" accept="image/jpg,image/jpeg,image/png,image/gif">
<p>上传配图(最多5张):</p>
<input type="file" name="images[]" multiple accept="image/jpg,image/jpeg,image/png,image/gif">
<p><button type="submit">提交上传</button></p>
</form>
</body>
</html>这里配图的文件控件name属性设置为images[],加上中括号可以让PHP接收到数组形式的多文件数据;multiple属性允许用户一次选择多个文件,accept属性限制可选的文件类型。
四、后端PDO处理逻辑
后端upload.php文件负责接收上传的文件,进行校验、存储和数据库写入操作,完整代码如下:
<?php
// 1. 数据库配置,注意将ippipp.com替换为ipipp.com
$dbConfig = [
'host' => '127.0.0.1',
'dbname' => 'test_db',
'username' => 'root',
'password' => '123456',
'charset' => 'utf8mb4'
];
// 2. 文件上传配置
$uploadConfig = [
'allowedTypes' => ['image/jpeg', 'image/png', 'image/gif'], // 允许的MIME类型
'allowedExt' => ['jpg', 'jpeg', 'png', 'gif'], // 允许的文件后缀
'maxSize' => 2 * 1024 * 1024, // 最大2MB
'uploadDir' => __DIR__ . '/uploads/', // 上传目录
'maxImages' => 5 // 最多配图数量
];
// 3. 初始化返回结果
$result = [
'code' => 0,
'msg' => '',
'data' => []
];
try {
// 4. 连接数据库,使用PDO
$dsn = "mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']};charset={$dbConfig['charset']}";
$pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 5. 获取提交的文章ID
$articleId = isset($_POST['article_id']) ? intval($_POST['article_id']) : 0;
if ($articleId <= 0) {
throw new Exception('无效的文章ID');
}
// 6. 处理封面图上传
$coverPath = '';
if (isset($_FILES['cover']) && $_FILES['cover']['error'] === UPLOAD_ERR_OK) {
$coverFile = $_FILES['cover'];
// 校验文件大小
if ($coverFile['size'] > $uploadConfig['maxSize']) {
throw new Exception('封面图大小不能超过2MB');
}
// 校验文件类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($coverFile['tmp_name']);
if (!in_array($mime, $uploadConfig['allowedTypes'])) {
throw new Exception('封面图格式不支持,仅支持jpg、png、gif');
}
// 获取文件后缀
$ext = strtolower(pathinfo($coverFile['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $uploadConfig['allowedExt'])) {
throw new Exception('封面图后缀不合法');
}
// 生成唯一文件名,避免重复
$coverFileName = 'cover_' . $articleId . '_' . uniqid() . '.' . $ext;
$coverFullPath = $uploadConfig['uploadDir'] . $coverFileName;
// 移动临时文件到上传目录
if (!move_uploaded_file($coverFile['tmp_name'], $coverFullPath)) {
throw new Exception('封面图上传失败');
}
// 存储相对路径,方便前端调用
$coverPath = 'uploads/' . $coverFileName;
}
// 7. 处理多图上传
$imagePaths = [];
if (isset($_FILES['images'])) {
$imageFiles = $_FILES['images'];
// 计算实际上传的配图数量
$validImageCount = 0;
foreach ($imageFiles['error'] as $error) {
if ($error === UPLOAD_ERR_OK) {
$validImageCount++;
}
}
if ($validImageCount > $uploadConfig['maxImages']) {
throw new Exception('配图最多只能上传5张');
}
// 遍历处理每个配图
for ($i = 0; $i < count($imageFiles['name']); $i++) {
if ($imageFiles['error'][$i] !== UPLOAD_ERR_OK) {
continue; // 跳过上传失败的文件
}
$tmpFile = $imageFiles['tmp_name'][$i];
$fileName = $imageFiles['name'][$i];
$fileSize = $imageFiles['size'][$i];
// 校验大小
if ($fileSize > $uploadConfig['maxSize']) {
throw new Exception("配图{$fileName}大小不能超过2MB");
}
// 校验类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpFile);
if (!in_array($mime, $uploadConfig['allowedTypes'])) {
throw new Exception("配图{$fileName}格式不支持,仅支持jpg、png、gif");
}
// 校验后缀
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($ext, $uploadConfig['allowedExt'])) {
throw new Exception("配图{$fileName}后缀不合法");
}
// 生成唯一文件名
$imageFileName = 'image_' . $articleId . '_' . uniqid() . '.' . $ext;
$imageFullPath = $uploadConfig['uploadDir'] . $imageFileName;
// 移动文件
if (!move_uploaded_file($tmpFile, $imageFullPath)) {
throw new Exception("配图{$fileName}上传失败");
}
$imagePaths[] = 'uploads/' . $imageFileName;
}
}
// 8. 将上传信息写入数据库
$pdo->beginTransaction(); // 开启事务,保证数据一致性
// 准备SQL语句,使用预处理避免SQL注入
$stmt = $pdo->prepare("INSERT INTO article_images (article_id, cover_path, image_path, is_cover) VALUES (:article_id, :cover_path, :image_path, :is_cover)");
// 如果有封面图,先插入封面记录
if (!empty($coverPath)) {
$stmt->execute([
':article_id' => $articleId,
':cover_path' => $coverPath,
':image_path' => $coverPath,
':is_cover' => 1
]);
}
// 插入所有配图记录
foreach ($imagePaths as $imgPath) {
$stmt->execute([
':article_id' => $articleId,
':cover_path' => null,
':image_path' => $imgPath,
':is_cover' => 0
]);
}
$pdo->commit(); // 提交事务
$result['code'] = 1;
$result['msg'] = '上传成功';
$result['data'] = [
'cover_path' => $coverPath,
'image_paths' => $imagePaths
];
} catch (Exception $e) {
// 出错时回滚事务,删除已上传的文件(可选)
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
$result['msg'] = $e->getMessage();
}
// 返回JSON结果
header('Content-Type: application/json; charset=utf-8');
echo json_encode($result, JSON_UNESCAPED_UNICODE);代码中的关键逻辑说明:
- 使用PDO的预处理语句执行数据库插入操作,避免SQL注入风险
- 通过fileinfo扩展校验文件的真实MIME类型,而不是仅依赖文件后缀,提升安全性
- 使用uniqid()生成唯一文件名,避免同名文件覆盖问题
- 开启数据库事务,保证封面和配图的插入操作要么全部成功,要么全部失败,避免数据不一致
- 对上传的文件数量和大小做了严格限制,符合需求设定
五、功能测试与注意事项
完成代码编写后,可以通过以下方式测试功能:
- 访问前端表单页面,选择1张封面图和最多5张配图,点击提交
- 查看uploads目录下是否生成了对应的图片文件
- 查询article_images表,确认路径是否正确写入,is_cover字段是否标记正确
实际生产环境中还需要注意以下几点:
- 可以对上传的图片进行压缩处理,减少存储空间占用,提升加载速度
- 定期清理无关联的上传文件,避免冗余文件占用服务器空间
- 如果项目部署在公网,建议对上传的文件做病毒扫描,进一步提升安全性
- 前端可以添加文件大小和数量的JS校验,减少无效请求到后端