0

0

优化Dompdf批量生成大量PDF:解决Web请求超时与性能瓶颈

DDD

DDD

发布时间:2025-10-06 10:39:35

|

152人浏览过

|

来源于php中文网

原创

优化Dompdf批量生成大量PDF:解决Web请求超时与性能瓶颈

针对Dompdf批量生成大量PDF时遇到的Web请求超时问题,本文提供了一套专业的解决方案。核心思想是将耗时且资源密集型的PDF生成任务从Web服务器请求中剥离,转移到PHP命令行脚本或后台任务中执行,从而规避Web服务器的执行时间限制,提升系统稳定性和用户体验,并详细指导如何实现这一过程。

1. 引言:Dompdf批量生成PDF的挑战

dompdf是一个广受欢迎的php库,用于将html转换为pdf,其在生成单个或少量pdf文件时表现出色。然而,当业务需求涉及到批量生成大量pdf文件(例如数百个甚至更多),且每个文件可能包含海量数据(如数千行记录)时,直接在web请求中同步执行此过程往往会遭遇严重的性能瓶颈和超时问题。用户在尝试生成100+个项目的pdf,其中部分项目数据量高达2000+行时,就明确遇到了请求超时的问题。

2. 当前实现方式及问题分析

用户最初的实现方式是在一个Web请求中,通过循环遍历所有待生成PDF的项目,为每个项目执行数据库查询、数据处理,然后调用Dompdf渲染并保存PDF文件。

核心代码片段(简化版):

// Web控制器或路由处理逻辑
$finalItems = array('item1', 'item2', 'item3', /* ... 更多项目 ... */);

foreach ($finalItems as $item) {
    // 1. 数据查询与准备
    // 假设此处包含多个DB::table查询,获取销售、采购、库存等数据
    $saleData = DB::table('sale_data')->where('item_name', $item)->get();
    $purchaseData = DB::table('purchase_data')->where('item_name', $item)->get();
    $stock_trf = DB::table('stock_transfer')->where('item_name', $item)->get();
    $res = array_merge(json_decode(json_encode($saleData), true), json_decode(json_encode($purchaseData), true), json_decode(json_encode($stock_trf), true));

    // 2. Dompdf渲染与保存
    $pdf = PDF::loadView('myPDF', compact('res')); // 加载Blade视图
    $pdf->setPaper('a3', 'landscape');
    $pdf->save(public_path() . '/pdf/item_' . $item . '.pdf');
    // $pdf->stream('item_' . $item . '.pdf'); // 如果直接下载,但此处是批量保存
}

问题分析:

  1. PHP执行时间限制 (set_time_limit): PHP脚本在Web服务器环境下通常有默认的执行时间限制(如30秒或60秒)。当循环生成大量PDF时,总耗时很容易超出这个限制,导致脚本中断。
  2. Web服务器超时: 除了PHP自身的限制,Web服务器(如Apache、Nginx)也有请求超时设置。即使PHP脚本的执行时间被延长,Web服务器也可能在达到其超时限制后终止连接。
  3. 资源消耗: 每次迭代都需要进行数据库查询、数据合并、HTML渲染和PDF转换,这些都是CPU和内存密集型操作。批量执行会导致服务器资源在短时间内被大量占用,影响其他请求的响应,甚至导致服务器不稳定。
  4. 用户体验: 用户发起请求后需要长时间等待,直到所有PDF生成完毕。这种同步等待不仅体验差,还可能导致用户误以为系统无响应而重复操作。

3. 专业解决方案:离线处理与后台任务

解决此类问题的核心思想是将耗时且资源密集型的PDF生成任务从Web服务器的即时请求中剥离出来,作为后台任务异步执行。这种“离线处理”的模式具有显著优势:

  • 规避超时限制: 后台任务通常不受Web服务器和PHP set_time_limit 的约束。
  • 提升用户体验: Web请求可以立即响应用户,告知任务已提交,无需等待漫长的处理过程。
  • 资源优化: 后台任务可以在服务器负载较低时执行,或者通过任务队列进行调度,避免资源瞬时过载。
  • 系统健壮性: 后台任务更容易实现错误处理、重试机制和任务状态追踪。

4. 实现步骤与示例代码

4.1 步骤一:前端触发任务并记录

用户在Web界面选择要生成PDF的项目后,前端发送一个轻量级的请求到后端。后端控制器不直接生成PDF,而是将任务信息(例如待处理的项目ID列表、用户ID、生成日期范围等)记录下来,并立即返回一个成功响应给用户。

Web控制器示例:

// app/Http/Controllers/PdfGeneratorController.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; // 用于存储临时文件

class PdfGeneratorController extends Controller
{
    public function generateBulkPdfs(Request $request)
    {
        $itemIds = $request->input('item_ids', []); // 从前端获取项目ID数组
        $fromDate = $request->input('from_date');
        $toDate = $request->input('to_date');
        $siteId = $request->input('site_id');

        if (empty($itemIds)) {
            return response()->json(['message' => '请选择至少一个项目进行PDF生成。'], 400);
        }

        // 将任务信息保存到临时文件或数据库任务队列
        $taskData = [
            'item_ids' => $itemIds,
            'from_date' => $fromDate,
            'to_date' => $toDate,
            'site_id' => $siteId,
            'user_id' => auth()->id(), // 如果需要关联用户
            'status' => 'pending', // 任务状态
            'created_at' => now(),
        ];

        $taskId = uniqid('pdf_task_');
        Storage::put("pdf_tasks/{$taskId}.json", json_encode($taskData));

        // 启动后台脚本(此处以exec为例,更推荐使用Laravel Queue)
        // 注意:这里的路径需要根据实际项目结构调整
        $command = 'php ' . base_path('artisan') . ' pdf:generate ' . $taskId . ' > /dev/null 2>&1 &';
        exec($command);

        return response()->json(['message' => 'PDF生成任务已提交,请稍后查看或等待通知。', 'task_id' => $taskId]);
    }
}

4.2 步骤二:创建命令行脚本(Artisan Command)

在Laravel框架中,最优雅的方式是创建一个Artisan命令。这个命令将负责读取任务信息,并在命令行环境下执行PDF生成逻辑。

Magic AI Avatars
Magic AI Avatars

神奇的AI头像,获得200多个由AI制作的自定义头像。

下载

创建Artisan命令:

php artisan make:command GenerateBulkPdfs

Artisan命令示例 (app/Console/Commands/GenerateBulkPdfs.php):

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Barryvdh\DomPDF\Facade as PDF; // 假设你已经安装并配置了barryvdh/laravel-dompdf

class GenerateBulkPdfs extends Command
{
    protected $signature = 'pdf:generate {taskId}';
    protected $description = 'Generates multiple PDFs in the background.';

    public function handle()
    {
        // 设置PHP执行无时间限制和足够的内存
        set_time_limit(0);
        ini_set('memory_limit', '-1'); // 或一个足够大的值,如 '1024M'

        $taskId = $this->argument('taskId');
        $this->info("Starting PDF generation for task: {$taskId}");

        // 从存储中读取任务数据
        if (!Storage::exists("pdf_tasks/{$taskId}.json")) {
            $this->error("Task data not found for ID: {$taskId}");
            return Command::FAILURE;
        }

        $taskData = json_decode(Storage::get("pdf_tasks/{$taskId}.json"), true);
        $itemIds = $taskData['item_ids'];
        $fromDate = $taskData['from_date'];
        $toDate = $taskData['to_date'];
        $siteId = $taskData['site_id'];

        $generatedPdfs = [];
        $pdfOutputDirectory = public_path('pdf'); // PDF保存目录

        // 确保PDF输出目录存在
        if (!file_exists($pdfOutputDirectory)) {
            mkdir($pdfOutputDirectory, 0777, true);
        }

        foreach ($itemIds as $item) {
            try {
                $this->info("Processing item: {$item}");

                // 原始代码中的数据库查询和数据准备逻辑
                $getGrp = DB::table('item_master')->select('group')->where('item_name', $item)->get();
                $rs = json_decode(json_encode($getGrp), true);
                $getGP = call_user_func_array('array_merge', $rs);

                $saleData = DB::table('sale_data')->where('item_name', $item)->where('site_id', $siteId)->whereBetween('bill_date', [$fromDate, $toDate])->get();
                $purchaseData = DB::table('purchase_data')->where('item_name', $item)->where('site_id', $siteId)->whereBetween('bill_date', [$fromDate, $toDate])->get();
                $stock_trf = DB::table('stock_transfer')->where('item_name', $item)->where('site_id', $siteId)->whereBetween('bill_date', [$fromDate, $toDate])->get();

                $sales = json_decode(json_encode($saleData), true);
                $purchase = json_decode(json_encode($purchaseData), true);
                $stock = json_decode(json_encode($stock_trf), true);
                $res = array_merge($sales, $purchase, $stock);

                $groupName = $getGP['group']; // 假设需要这个变量

                // 加载视图并生成PDF
                $pdf = PDF::loadView('myPDF', compact('res', 'groupName')); // 确保myPDF视图能访问这些变量
                $pdf->setPaper('a3', 'landscape');
                $pdfFileName = 'item_' . str_replace('/', '_', $item) . '.pdf'; // 替换非法文件名字符
                $pdfPath = $pdfOutputDirectory . '/' . $pdfFileName;
                $pdf->save($pdfPath);

                $generatedPdfs[] = $pdfFileName;
                $this->info("Generated PDF for item {$item}: {$pdfFileName}");

            } catch (\Exception $e) {
                $this->error("Error generating PDF for item {$item}: " . $e->getMessage());
                // 记录错误或进行其他处理
            }
        }

        // 更新任务状态(例如,保存生成的PDF列表到任务数据,或发送通知)
        $taskData['status'] = 'completed';
        $taskData['generated_pdfs'] = $generatedPdfs;
        Storage::put("pdf_tasks/{$taskId}.json", json_encode($taskData));

        $this->info("All PDFs generated for task: {$taskId}. Total: " . count($generatedPdfs));
        return Command::SUCCESS;
    }
}

注意: 视图文件 myPDF.blade.php 的内容应与原始问题中的HTML视图类似,确保数据循环和显示逻辑正确。

{{-- resources/views/myPDF.blade.php --}}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>PDF Report</title>
    <style>
        /* 样式可以根据需要添加 */
        table { width: 100%; border-collapse: collapse; }
        th, td { border: 1px solid black; padding: 8px; text-align: left; }
    </style>
</head>
<body>
    <h1>Report for Group: {{ $groupName ?? 'N/A' }}</h1>
    <table>
        <thead>
            <tr>
                <th>Batch No.</th>
                <th>MFG Date</th>
                <th>EXP Date</th>
                <th>Quantity</th>
                <th>Balance</th>
                <th>Bill No.</th>
                <th>Bill Date</th>
                <th>Customer Name</th>
            </tr>
        </thead>
        <tbody>
            @if(isset($res) && count($res) > 0)
                @php
                    $dlr = array_chunk($res, 25); // 每页25行
                    $last_balance = 0; // 初始余额,可能需要从外部传入或计算
                @endphp
                @foreach ($dlr as $pageData)
                    @foreach ($pageData as $sldata)
                        <tr>
                            <td>{{ $sldata['batch_no'] ?? '' }}</td>
                            <td>{{ $sldata['mfg_date'] ?? '' }}</td>
                            <td>{{ $sldata['exp_date'] ?? '' }}</td>
                            <td>{{ $sldata['quantity_in_kgltr'] ?? '' }}</td>
                            <td>
                                @php
                                    $tocl = (int)($sldata['quantity_in_kgltr'] ?? 0);
                                    $last_balance -= $tocl;
                                    echo $last_balance;
                                @endphp
                            </td>
                            <td>{{ $sldata['bill_no'] ?? '' }}</td>
                            <td>{{ isset($sldata['bill_date']) ? date('d-m-Y', strtotime($sldata['bill_date'])) : '' }}</td>
                            <td>{{ $sldata['sales_to_customer_name'] ?? '' }}</td>
                        </tr>
                    @endforeach
                @endforeach
            @else
                <tr><td colspan="8">No data available for this item.</td></tr>
            @endif
        </tbody>
    </table>
</body>
</html>

4.3 步骤三:调用命令行脚本

在Web控制器中,使用PHP的 exec() 函数来启动Artisan命令,并使用 & 符号将其置于后台运行,确保Web请求不会等待命令执行完毕。

exec() 函数调用:

// 在Web控制器中 (如上面 PdfGeneratorController 的 generateBulkPdfs 方法中)
$command = 'php ' . base_path('artisan') . ' pdf:generate ' . $taskId . ' > /dev/null 2>&1 &';
exec($command);
  • php artisan pdf:generate {taskId}: 这是要执行的Artisan命令。
  • > /dev/null: 将命令的标准输出重定向到空设备,避免在Web服务器日志

相关文章

数码产品性能查询
数码产品性能查询

该软件包括了市面上所有手机CPU,手机跑分情况,电脑CPU,电脑产品信息等等,方便需要大家查阅数码产品最新情况,了解产品特性,能够进行对比选择最具性价比的商品。

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
laravel组件介绍
laravel组件介绍

laravel 提供了丰富的组件,包括身份验证、模板引擎、缓存、命令行工具、数据库交互、对象关系映射器、事件处理、文件操作、电子邮件发送、队列管理和数据验证。想了解更多laravel的相关内容,可以阅读本专题下面的文章。

340

2024.04.09

laravel中间件介绍
laravel中间件介绍

laravel 中间件分为五种类型:全局、路由、组、终止和自定。想了解更多laravel中间件的相关内容,可以阅读本专题下面的文章。

293

2024.04.09

laravel使用的设计模式有哪些
laravel使用的设计模式有哪些

laravel使用的设计模式有:1、单例模式;2、工厂方法模式;3、建造者模式;4、适配器模式;5、装饰器模式;6、策略模式;7、观察者模式。想了解更多laravel的相关内容,可以阅读本专题下面的文章。

773

2024.04.09

thinkphp和laravel哪个简单
thinkphp和laravel哪个简单

对于初学者来说,laravel 的入门门槛较低,更易上手,原因包括:1. 更简单的安装和配置;2. 丰富的文档和社区支持;3. 简洁易懂的语法和 api;4. 平缓的学习曲线。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

385

2024.04.10

laravel入门教程
laravel入门教程

本专题整合了laravel入门教程,想了解更多详细内容,请阅读专题下面的文章。

141

2025.08.05

laravel实战教程
laravel实战教程

本专题整合了laravel实战教程,阅读专题下面的文章了解更多详细内容。

85

2025.08.05

laravel面试题
laravel面试题

本专题整合了laravel面试题相关内容,阅读专题下面的文章了解更多详细内容。

80

2025.08.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

497

2026.03.04

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 13.4万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.3万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 1.0万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号