
引言
在现代 web 应用中,将动态数据生成 pdf 文档是常见的需求,dompdf 作为一款流行的 php 库,因其能够将 html 转换为 pdf 而广受欢迎。然而,当需要批量生成数百个甚至更多包含大量数据和多页内容的 pdf 文件时,web 服务器环境下的 php 脚本往往会因为执行时间过长而遭遇超时,导致任务失败。本文将深入分析这一问题,并提供一套健壮的解决方案,特别是推荐使用 php 命令行接口(cli)进行后台处理。
问题分析:Web 环境下的限制
用户提供的代码片段展示了一个典型的场景:通过循环遍历大量数据项,为每个数据项生成一个独立的 PDF 文件。在每次循环中,脚本会查询数据库、合并数据、加载视图并渲染为 PDF,最后保存文件。当数据量庞大(例如,一个数据项有 2000+ 行,每页 25 行),或者需要处理的 finalItems 数量达到数百个时,整个过程将变得非常耗时。
在 Web 环境中,PHP 脚本的执行受到多重限制:
- PHP max_execution_time 限制: PHP 配置文件 php.ini 中的 max_execution_time 参数定义了脚本允许执行的最大时间(默认为 30 秒或 60 秒)。长时间运行的脚本会触发此限制。
- Web 服务器超时: 即使 PHP 脚本自身的执行时间被放宽,前端的 Web 服务器(如 Apache、Nginx)也有自己的请求超时设置。如果客户端在一定时间内没有收到服务器的响应,Web 服务器也会中断连接,导致请求失败。
- 资源消耗: Dompdf 在渲染复杂 HTML 时会消耗大量的 CPU 和内存资源。批量生成 PDF 会迅速耗尽服务器资源,进一步加剧超时风险。
这些限制使得在 Web 请求中直接处理大量 PDF 生成任务变得不切实际。
短期解决方案:调整 PHP 执行时间
最直接的缓解方法是增加 PHP 脚本的执行时间限制。可以通过在脚本开头调用 set_time_limit(0) 来取消 PHP 脚本的执行时间限制。
setPaper('a3', 'landscape');
$fileName = 'item_' . $item . '.pdf';
$outputPath = public_path() . '/pdf/' . $fileName;
// 确保输出目录存在
if (!is_dir(public_path() . '/pdf/')) {
mkdir(public_path() . '/pdf/', 0777, true);
}
$pdf->save($outputPath); // 只保存,不直接流式输出
$pdfNames[] = $fileName;
}
// 在所有PDF生成完成后,可以提供一个下载链接列表,或打包下载
// return view('download_pdfs', compact('pdfNames'));
?>注意事项:
- set_time_limit(0) 仅解决了 PHP 脚本层面的超时,无法规避 Web 服务器本身的超时限制。
- 长时间占用 Web 请求会阻塞服务器资源,影响其他用户的访问体验。
- 这种方法适用于生成数量有限、内容不特别复杂的 PDF,不适用于大规模批量生成。
推荐方案:利用 PHP CLI 进行后台处理
对于大规模或耗时长的 PDF 生成任务,最佳实践是将任务从 Web 请求中分离出来,作为独立的命令行脚本(CLI)在后台执行。这种方法具有以下显著优势:
- 无 Web 服务器超时限制: CLI 脚本不受 Web 服务器的请求超时限制,可以长时间运行直至任务完成。
- 提升用户体验: Web 请求可以立即响应用户,告知任务已提交并在后台处理,而不是让用户长时间等待。
- 资源隔离: CLI 任务可以独立管理其资源,避免与 Web 请求争抢资源。
- 易于调度和管理: 可以通过 cron job 或消息队列系统来调度和管理 CLI 任务。
实现步骤
创建独立的 CLI 脚本: 编写一个专门的 PHP 脚本,包含 PDF 生成的核心逻辑。这个脚本将在命令行环境中运行。
-
数据传递机制: Web 应用需要将生成 PDF 所需的数据(例如 itemIds、日期范围等)传递给 CLI 脚本。常用的方法包括:
- 命令行参数: 最直接的方式,通过 argv 数组在 CLI 脚本中获取。
- 文件传递: Web 应用将数据写入临时文件(如 JSON 或 CSV),CLI 脚本读取该文件。
- 数据库/消息队列: Web 应用将任务详情写入数据库的任务表或推送到消息队列,CLI 脚本作为消费者拉取任务。
触发 CLI 脚本: Web 应用可以使用 PHP 的 exec()、shell_exec() 或 proc_open() 等函数在后台异步执行 CLI 脚本。为了不阻塞 Web 请求,通常需要将命令放入后台执行。
-
进度与结果反馈: 由于 CLI 脚本在后台运行,Web 应用需要一种机制来获取任务的进度和最终结果。
- 数据库状态更新: CLI 脚本在执行过程中更新数据库中的任务状态。
- 文件通知: CLI 脚本生成完成后,可以写入一个结果文件,Web 应用轮询该文件或通过其他方式获取。
- 邮件通知: 任务完成后发送邮件给用户。
示例代码:CLI 模式下的 PDF 生成
我们将原始的 PDF 生成逻辑迁移到一个独立的 CLI 脚本中,并通过命令行参数接收输入。
1. CLI 脚本 (generate_pdfs.php)
make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
// 手动引入 Dompdf (如果不是在框架环境下)
require_once 'vendor/autoload.php'; // 确保 Dompdf 已通过 Composer 安装
use Dompdf\Dompdf;
use Dompdf\Options;
// 设置不限制执行时间
set_time_limit(0);
// 假设我们通过命令行参数接收 item IDs, fromDate, toDate, site_id
// 示例运行命令:php generate_pdfs.php "itemA,itemB,itemC" "2023-01-01" "2023-01-31" 1
$itemIdsStr = $argv[1] ?? ''; // 第一个参数是逗号分隔的 item ID 字符串
$fromDate = $argv[2] ?? '';
$toDate = $argv[3] ?? '';
$siteId = $argv[4] ?? null;
$itemIds = array_filter(explode(',', $itemIdsStr));
if (empty($itemIds)) {
echo "Error: Item IDs are required.\n";
exit(1);
}
// 模拟数据库连接和数据获取
// 在实际项目中,这里应替换为你的数据库查询逻辑,例如使用 Laravel 的 DB Facade
function getDbData($tableName, $itemName, $siteId, $fromDate, $toDate) {
// 这是一个模拟函数,实际应替换为数据库查询
// 例如:
// return DB::table($tableName)
// ->where('item_name', $itemName)
// ->where('site_id', $siteId)
// ->whereBetween('bill_date', [$fromDate, $toDate])
// ->get()->toArray();
echo " - Fetching data from $tableName for item $itemName...\n";
// 返回一些模拟数据
return [
['batch_no' => 'B001', 'mfg_date' => '2023-01-01', 'exp_date' => '2024-01-01', 'quantity_in_kgltr' => 10, 'bill_no' => 'BN001', 'bill_date' => '2023-01-05', 'sales_to_customer_name' => 'Customer A'],
['batch_no' => 'B002', 'mfg_date' => '2023-02-01', 'exp_date' => '2024-02-01', 'quantity_in_kgltr' => 15, 'bill_no' => 'BN002', 'bill_date' => '2023-01-10', 'sales_to_customer_name' => 'Customer B'],
// ... 更多模拟数据以填充多页 PDF
['batch_no' => 'B003', 'mfg_date' => '2023-03-01', 'exp_date' => '2024-03-01', 'quantity_in_kgltr' => 20, 'bill_no' => 'BN003', 'bill_date' => '2023-01-15', 'sales_to_customer_name' => 'Customer C'],
// 确保有足够的数据来模拟多页
...array_fill(0, 50, ['batch_no' => 'BXXX', 'mfg_date' => '2023-04-01', 'exp_date' => '2024-04-01', 'quantity_in_











