如何在 Laravel Job 中本地调整并优化多图后再保存
可行方案
可以完全不依赖第三方在线服务,在图片存入正式可访问存储前通过 Laravel Job 完成尺寸调整与优化。禁止直接向 Job 传递 Request 对象或 UploadedFile 实例,需先在当前HTTP请求周期内将上传文件暂存到服务端可控临时目录,再把可正常序列化的文件元数据传入 Job。
核心原因
Request对象绑定当前HTTP请求的完整上下文与资源句柄,无法完整序列化还原,不符合队列任务的传参规则。- PHP 默认会在HTTP请求结束后自动清理上传生成的系统临时文件,异步 Job 拉取任务执行时,原临时文件通常已被删除,直接传文件实例会触发文件不存在错误。
- 先把文件转存到自定义临时目录,既可以保证 Job 执行时文件可读取,传入的路径、文件名等普通数组数据也支持正常序列化。
代码实现
1. 控制器层:校验上传并暂存临时文件
在控制器中完成文件校验,将所有上传文件存入本地非公开临时目录,收集纯数组格式的文件信息后再派发任务。
namespace App\Http\Controllers; use App\Jobs\ProcessUploadedImage; use Illuminate\Http\Request; class ImageUploadController extends Controller { public function upload(Request $request) { $request->validate([ 'images' => 'required|array', 'images.*' => 'required|image|max:10240', // 单图最大10MB,按需调整 ]); $uploadFiles = []; // 每个上传批次生成唯一临时目录,避免文件重名冲突 $tempBatchDir = 'temp/image_uploads/' . uniqid('batch_'); foreach ($request->file('images') as $file) { $extension = $file->getClientOriginalExtension(); $tempFileName = uniqid('img_') . '.' . $extension; // 将文件存入local磁盘(对应storage/app目录,无法直接通过web访问) $tempPath = $file->storeAs($tempBatchDir, $tempFileName, 'local'); $uploadFiles[] = [ 'temp_path' => $tempPath, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), ]; } // 仅传入普通数组,不传递请求对象 ProcessUploadedImage::dispatch($uploadFiles); return response()->json(['message' => '上传任务已提交']); } }
2. Job层:处理图片并转存正式目录
图片调整通过本地PHP扩展(GD/Imagick)完成,不需要调用外部服务。示例使用 intervention/image 包简化图片操作,如需完全不用第三方依赖,也可直接通过GD/Imagick原生函数实现相同逻辑。
可选依赖安装命令:
composer require intervention/image
任务类代码:
namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Storage; use Intervention\Image\Facades\Image; use Throwable; class ProcessUploadedImage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; private array $uploadFiles; // 按需调整配置 private string $targetDisk = 'public'; private string $targetDirectory = 'uploads/images'; private int $maxWidth = 1920; private int $optimizeQuality = 80; public function __construct(array $uploadFiles) { $this->uploadFiles = $uploadFiles; } public function handle(): void { $tempDisk = Storage::disk('local'); foreach ($this->uploadFiles as $file) { try { if (!$tempDisk->exists($file['temp_path'])) { continue; } // 初始化图片实例 $image = Image::make($tempDisk->path($file['temp_path'])); // 等比缩放,仅缩小超大图,不强制放大小图 if ($image->width() > $this->maxWidth) { $image->resize($this->maxWidth, null, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); } // 生成正式存储文件名 $extension = pathinfo($file['original_name'], PATHINFO_EXTENSION); $targetFileName = uniqid('img_') . '.' . $extension; $targetPath = $this->targetDirectory . '/' . $targetFileName; // 编码压缩后存入目标磁盘 $encodedImage = (string) $image->encode(null, $this->optimizeQuality); Storage::disk($this->targetDisk)->put($targetPath, $encodedImage); // 如需持久化图片信息到数据库,在此处编写对应逻辑 } finally { // 单文件处理完成后立即删除临时文件 if ($tempDisk->exists($file['temp_path'])) { $tempDisk->delete($file['temp_path']); } } } // 清理当前批次的空临时目录 $batchDir = dirname($this->uploadFiles[0]['temp_path']); if (empty($tempDisk->allFiles($batchDir))) { $tempDisk->deleteDirectory($batchDir); } } /** * 任务执行失败时的兜底清理逻辑 */ public function failed(Throwable $exception): void { $tempDisk = Storage::disk('local'); foreach ($this->uploadFiles as $file) { if ($tempDisk->exists($file['temp_path'])) { $tempDisk->delete($file['temp_path']); } } if (!empty($this->uploadFiles)) { $batchDir = dirname($this->uploadFiles[0]['temp_path']); if ($tempDisk->directoryExists($batchDir)) { $tempDisk->deleteDirectory($batchDir); } } } }
注意事项
- 临时文件默认存在
storage/app下的非公开目录,处理完成前无法通过外网访问,避免原图泄露。 - 如果上传图片数量多、单图处理耗时长,可以遍历文件时为每个文件单独派发Job,利用队列并发提升处理效率,传参逻辑一致。
- 即使当前队列使用同步驱动,也建议按该方案实现,避免后续切换异步队列时出现兼容性错误。
- 目标磁盘支持本地公开目录、对象存储等所有Laravel文件系统兼容的存储驱动。
内容的提问来源于stack exchange,提问作者John D
相关产品推荐
相关产品推荐

