跳转到内容

CodeIgniter 4 中的生产环境使用

生产环境中的控制器依赖具体的 NextPDF 服务。它们会明确处理已记录的异常层级,并发出可观测性信号。耗时的 PDF 工作会通过 CodeIgniter 4 Queue 移出请求流程之外执行。

CodeIgniter 4 会通过自身的定位器(locator)来 resolve(解析)该软件包的服务。服务定位器(service locator)模式会把容器(container)交给某个对象,让该对象自行取出所需依赖。此模式并不建议使用(PSR-11 §1.3,modal SHOULD NOT)。若要遵循该指引,请在控制器边界逐一解析每个 NextPDF 服务,再把具体对象向内传递。请勿把 Services 类或容器传入你的领域代码。

每个 PHP 示例都会单独一行声明 declare(strict_types=1);(PSR-12 §x1.x3.p34)。

生产环境考量已验证接口
解析服务Services::pdf(false)Services::pdfDocument(false)Services::documentFactory()
构建响应PdfResponse::download() / inline()DownloadResponse
捕获失败NextPDF\Exception\NextPdfException(生态系统基类型)
异步生成GeneratePdfJob:已注册于 Config\Queue::$jobHandlers
路径 / 可调用项防护GeneratePdfJob 会抛出 InvalidArgumentException

生产环境控制器 —— 错误处理与可观测性

标题为“生产环境控制器 —— 错误处理与可观测性”的章节

核心引擎抛出的异常全部继承自 NextPDF\Exception\NextPdfException。只捕获这一个类型,就能涵盖核心和扩展功能的失败。这里的 catch 块会带上下文记录日志,并返回一个明确定义的错误响应,绝不留下空的 catch。

<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use NextPDF\CodeIgniter\Config\Services;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final class InvoiceController extends BaseController
{
public function download(int $id): DownloadResponse|ResponseInterface
{
/** @var LoggerInterface $logger */
$logger = \service('logger');
$start = \hrtime(true);
try {
$pdf = Services::pdf(false);
$pdf->document()->addPage();
$pdf->document()->cell(0, 10, "Invoice #{$id}");
$response = $pdf->download("invoice-{$id}.pdf");
$logger->info('pdf.invoice.generated', [
'invoice_id' => $id,
'elapsed_ms' => (\hrtime(true) - $start) / 1_000_000,
]);
return $response;
} catch (NextPdfException $e) {
$logger->error('pdf.invoice.failed', [
'invoice_id' => $id,
'exception' => $e::class,
'message' => $e->getMessage(),
]);
return $this->response
->setStatusCode(ResponseInterface::HTTP_INTERNAL_SERVER_ERROR)
->setJSON(['error' => 'pdf_generation_failed', 'invoice_id' => $id]);
}
}
}

Services::pdf(false) 每次调用都会返回全新的库实例和全新的底层文档。因此,并发请求之间绝不会共用文档状态。该软件包的功能测试会验证这一行为。

字体与图像注册表(registry)按进程生命周期单例(singleton)设计。字体注册表只会预热并锁定一次。图像注册表是一个有界的最近最少使用(LRU)缓存。在长驻 worker(CodeIgniter spark 服务器、RoadRunner 风格执行器,或队列 worker)中,这正是预期行为:成本较高的注册表会持续保留,而每份文档都是全新的。请勿在请求或工作代码中索取共用文档(Services::pdfDocument(true));它仅供测试重置使用,否则会在多个请求之间共用内容。

GeneratePdfJob 会通过 codeigniter4/queue 把 PDF 生成工作移出请求流程之外执行。队列运行时会强制以下两项事实成立,而这两项你都必须正确配置。

队列按**名称键(name key)**解析工作,而不是按类字符串解析。队列处理器会将推入的工作名称与 Config\Queue::$jobHandlers 的键进行比对并验证。若遇到未知名称,它会以 CodeIgniter\Queue\Exceptions\QueueException 拒绝。请在 app/Config/Queue.php 中注册工作:

<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;
use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue
{
/** @var array<string, class-string> */
public array $jobHandlers = [
'generate-pdf' => GeneratePdfJob::class,
];
}

将注册名称作为第二个参数推入工作。第一个参数是队列名称,第三个参数是工作数据数组。

<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController
{
public function queueInvoice(int $id): ResponseInterface
{
\service('queue')->push('pdf-queue', 'generate-pdf', [
'builder' => 'App\\PdfBuilders\\InvoiceBuilder::build',
'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf',
'context' => ['invoice_id' => $id],
]);
return $this->response
->setStatusCode(ResponseInterface::HTTP_ACCEPTED)
->setJSON(['status' => 'queued', 'invoice_id' => $id]);
}
}

该工作会将构建器(builder)可调用项限制在 App\PdfBuilders 命名空间(namespace)下,并将输出路径限定在 WRITEPATH/pdfs/ 内。构建器是一个静态方法,会接收一份全新的 Document 和上下文数组,并返回该文档。

<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder
{
/** @param array<string, mixed> $context */
public static function build(Document $document, array $context): Document
{
$invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage();
$document->cell(0, 10, "Invoice #{$invoiceId}");
return $document;
}
}
Terminal window
php spark queue:work pdf-queue

每次工作运行都会通过 Services::pdfDocument() 从全新文档开始。它会应用构建器,然后保存到已验证的路径。该软件包的测试会验证连续两次工作运行不会共用文档状态。

  • 队列在推入时会拒绝以 GeneratePdfJob::class 作为工作名称,因为它不是已注册的键 'generate-pdf'。请务必推入 jobHandlers 的键。
  • 构建器字符串必须完全匹配 App\PdfBuilders\<Class>::<method>。函数、其他命名空间,或带有 prefixed/suffixed 形式的载荷,都会在任何代码运行之前就引发 InvalidArgumentException
  • 输出路径必须解析到 WRITEPATH/pdfs/ 内部,并以 .pdf 结尾(不区分大小写)。路径穿越以及同层前缀路径都会被拒绝。
  • codeigniter4/queue 是该软件包仅供开发使用的依赖。请在实际运行 worker 的应用程序中安装它。

注册表在每个 worker 进程中只会创建一次。文档构建成本随内容变化,与适配器(adapter)无关。对于大型批处理工作,请优先采用队列路径,让请求 worker 保持响应能力。任何有可衡量目标的 recipe(示例)都请设置 performance_budget

队列工作是风险最高的接口。当 broker(消息中介)可被触及时,队列载荷会受到攻击者影响。其可调用项允许列表与路径限定,以及已验证的拒绝案例,记录在 /integrations/codeigniter/security-and-operations/ 一节。

  • 控制器收到的是具体服务,而非容器,这与 PSR-11 §1.3 的服务定位器指引一致。

NextPDF 核心采用 Apache-2.0 许可。在队列工作中生成签名和 PDF/A 输出,需要在 worker 环境安装 NextPDF Pro 或 Enterprise。CodeIgniter 软件包会公开对应的服务方法。安装对应的 Premium 软件包之前,这些方法都会返回 null。请参阅 </get-license/?intent=codeigniter-async-signing>。

  • /integrations/codeigniter/quickstart/ —— 这些控制器的精简版本。
  • /integrations/codeigniter/configuration/ —— 签名、TSA 和路径配置。
  • /integrations/codeigniter/security-and-operations/ —— 队列威胁模型和加固。
  • /integrations/codeigniter/troubleshooting/ —— 队列与发现的失败模式。
  • /integrations/codeigniter/integration/ —— 接线参考和冒烟测试。