
在开发api时,我们经常遇到需要为核心资源提供附加功能,例如生成并下载与该资源关联的特定格式文件(如pdf发票、csv报告等)。api-platform以其强大的资源管理能力简化了crud操作,但当涉及到非标准输出格式(如application/pdf)时,直接将其集成到apiresource的output_formats中可能会引入额外的复杂性,例如需要自定义编码器和openapi文档装饰器。本文将介绍一种更简洁、更符合职责分离原则的方法来解决这一问题。
假设我们有一个Invoice(发票)实体,它已经通过Api-Platform暴露了标准的RESTful接口(GET、POST、PUT、DELETE)。现在,我们需要为每张发票提供一个下载其PDF文档的路由,例如/invoices/{id}/document,并且该路由的响应内容类型必须是application/pdf。
初学者可能会尝试通过在#[ApiResource]注解中定义一个自定义操作,并指定output_formats为['application/pdf'],同时指向一个自定义控制器来处理逻辑。
// app/src/Entity/Invoice.php
#[ApiResource(itemOperations: [
'get',
'put',
'patch',
'delete',
'get_document' => [
'method' => 'GET',
'path' => '/invoices/{id}/document',
'controller' => DocumentController::class,
'output_formats' => ['application/pdf']
],
])]
class Invoice
{
// ... 实体属性和方法
}这种方法的问题在于,Api-Platform的output_formats主要用于数据序列化(如JSON、XML、JSON-LD等),并期望有相应的编码器来处理。对于二进制文件(如PDF),直接使用这种机制会要求我们为application/pdf注册一个自定义的编码器,这通常是不必要的复杂化,并且可能与Api-Platform的OpenAPI文档生成机制产生冲突。
更优雅的解决方案是将文件生成和下载的逻辑从Api-Platform的核心资源管理中解耦出来,将其视为一个独立的Symfony控制器功能。Api-Platform资源仅负责暴露一个指向该文件的URL。
首先,我们需要在Invoice实体中添加一个方法,用于生成PDF文档的访问URL。这个URL将作为Invoice资源的一个可读属性暴露给API消费者。
// app/src/Entity/Invoice.php
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
operations: [
// ... 其他标准操作
],
normalizationContext: ['groups' => ['read:invoice']]
)]
class Invoice
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
#[Groups(['read:invoice'])]
private ?string $invoiceNumber = null;
// ... 其他属性
public function getId(): ?int
{
return $this->id;
}
public function getInvoiceNumber(): ?string
{
return $this->invoiceNumber;
}
public function setInvoiceNumber(string $invoiceNumber): self
{
$this->invoiceNumber = $invoiceNumber;
return $this;
}
/**
* 获取发票PDF文档的URL。
* 该方法会通过序列化组暴露给API消费者。
*/
#[Groups(["read:invoice"])]
public function getDocumentUrl(): string
{
// 假设路由名为 'app_invoice_document'
// 实际应用中,应使用Symfony的Router服务生成URL,以确保正确性
// 例如:$this->router->generate('app_invoice_document', ['id' => $this->id], UrlGeneratorInterface::ABSOLUTE_URL);
return "/invoices/{$this->id}/document";
}
}通过#[Groups(["read:invoice"])]注解,getDocumentUrl()方法将在Invoice资源被序列化(例如,GET /invoices/{id})时,作为一个普通属性包含在响应中。API消费者会看到类似"documentUrl": "/invoices/123/document"的字段,然后他们可以使用这个URL发起单独的请求来下载PDF。
接下来,我们需要创建一个标准的Symfony控制器来处理/invoices/{id}/document这个URL。这个控制器将负责:
// app/src/Controller/InvoiceDocumentController.php
<?php
namespace App\Controller;
use App\Entity\Invoice;
use App\Service\InvoiceDocumentService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; // 用于安全控制
class InvoiceDocumentController extends AbstractController
{
private InvoiceDocumentService $documentService;
public function __construct(InvoiceDocumentService $invoiceDocumentService)
{
$this->documentService = $invoiceDocumentService;
}
/**
* 处理发票PDF文档的下载请求。
*
* @param Invoice $invoice Symfony的ParamConverter会自动将{id}转换为Invoice对象
*/
#[Route('/invoices/{id}/document', name: 'app_invoice_document', methods: ['GET'])]
// 确保只有授权用户才能访问该文档
#[IsGranted('VIEW', subject: 'invoice')]
public function __invoke(Invoice $invoice): Response
{
// 1. 调用服务生成PDF文档的路径或内容
// 假设服务返回一个文件路径
$pdfFilePath = $this->documentService->createDocumentForInvoice($invoice);
if (!file_exists($pdfFilePath)) {
throw $this->createNotFoundException('The invoice document was not found.');
}
// 2. 创建BinaryFileResponse以发送文件
$response = new BinaryFileResponse($pdfFilePath);
// 3. 设置正确的Content-Type
$response->headers->set('Content-Type', 'application/pdf');
// 4. 设置Content-Disposition以强制浏览器下载文件,并指定文件名
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT, // DISPOSITION_INLINE 会尝试在浏览器中打开
'invoice_' . $invoice->getInvoiceNumber() . '.pdf'
);
// 5. 可选:设置缓存控制头
$response->setPublic();
$response->setMaxAge(3600); // 缓存1小时
return $response;
}
}在这个控制器中:
InvoiceDocumentService的职责是根据传入的Invoice对象生成PDF文件并返回其路径。这部分逻辑可以根据您使用的PDF生成库(如Dompdf、TCPDF、MPDF等)进行实现。
// app/src/Service/InvoiceDocumentService.php
<?php
namespace App\Service;
use App\Entity\Invoice;
// 假设你使用一个PDF生成库,例如wkhtmltopdf或MPDF
// use Knp\Snappy\Pdf; // 示例
class InvoiceDocumentService
{
// private Pdf $snappyPdf; // 注入PDF生成器
// public function __construct(Pdf $snappyPdf)
// {
// $this->snappyPdf = $snappyPdf;
// }
/**
* 为指定发票创建PDF文档。
*
* @param Invoice $invoice
* @return string PDF文件的临时或永久存储路径
*/
public function createDocumentForInvoice(Invoice $invoice): string
{
// 实际的PDF生成逻辑
// 例如:
// $htmlContent = $this->generateHtmlForInvoice($invoice);
// $pdfContent = $this->snappyPdf->getOutputFromHtml($htmlContent);
// 假设将PDF保存到临时文件
$tempFilePath = sys_get_temp_dir() . '/invoice_' . $invoice->getInvoiceNumber() . '.pdf';
// file_put_contents($tempFilePath, $pdfContent); // 写入PDF内容
// 模拟生成一个空PDF文件以供演示
file_put_contents($tempFilePath, "This is a dummy PDF for Invoice " . $invoice->getInvoiceNumber());
return $tempFilePath;
}
// private function generateHtmlForInvoice(Invoice $invoice): string
// {
// // 根据发票数据生成HTML内容,用于PDF转换
// return "<h1>Invoice #{$invoice->getInvoiceNumber()}</h1><p>...</p>";
// }
}通过将Api-Platform资源与文件下载逻辑解耦,我们能够以一种更清晰、更易于维护的方式为API提供非标准输出格式。核心思想是让Api-Platform负责暴露一个指向文件的URL,而实际的文件生成和传输则由一个标准的Symfony控制器处理。这种方法不仅简化了开发过程,也提升了API的整体设计质量和可扩展性。
以上就是Api-Platform:为资源添加自定义PDF下载路由的最佳实践的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号