TenantAtlas/apps/platform/app/Services/Pdf/PdfRenderingGateway.php
ahmido d43ebcb4ee feat(report): implement management report pdf v1 (#449)
Added PDF generation service for management reports as per Spec 378, including Gotenberg integration in docker-compose and configuration updates.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #449
2026-06-14 18:36:07 +00:00

115 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Pdf;
final class PdfRenderingGateway
{
public function __construct(
private readonly PdfRendererClient $client,
) {}
public function healthCheck(?string $correlationId = null): PdfRenderResult
{
if (! $this->isEnabled()) {
return PdfRenderResult::failure(
PdfRenderResult::RENDERER_DISABLED,
'PDF renderer is disabled.',
correlationId: $correlationId,
);
}
return $this->client->healthCheck($correlationId);
}
public function renderHtml(PdfRenderRequest $request): PdfRenderResult
{
if (! $this->isEnabled()) {
return PdfRenderResult::failure(
PdfRenderResult::RENDERER_DISABLED,
'PDF renderer is disabled.',
correlationId: $request->correlationId,
);
}
if (trim($request->html) === '') {
return PdfRenderResult::failure(
PdfRenderResult::INVALID_REQUEST,
'PDF renderer requires server-generated HTML.',
correlationId: $request->correlationId,
);
}
if (strlen($request->html) > $this->maxHtmlBytes()) {
return PdfRenderResult::failure(
PdfRenderResult::PAYLOAD_TOO_LARGE,
'PDF HTML payload exceeds the configured size limit.',
correlationId: $request->correlationId,
);
}
foreach ($request->assets as $filename => $contents) {
if (! $this->isFlatAssetFilename((string) $filename)) {
return PdfRenderResult::failure(
PdfRenderResult::INVALID_ASSET,
'PDF assets must use flat relative filenames.',
correlationId: $request->correlationId,
);
}
if (strlen($contents) > $this->maxAssetBytes()) {
return PdfRenderResult::failure(
PdfRenderResult::PAYLOAD_TOO_LARGE,
'PDF asset payload exceeds the configured size limit.',
correlationId: $request->correlationId,
);
}
}
$result = $this->client->render($request);
if ($result->successful() && $result->pdfBytes !== null && strlen($result->pdfBytes) > $this->maxOutputBytes()) {
return PdfRenderResult::failure(
PdfRenderResult::OUTPUT_TOO_LARGE,
'PDF renderer output exceeds the configured size limit.',
statusCode: $result->statusCode,
correlationId: $result->correlationId,
);
}
return $result;
}
private function isEnabled(): bool
{
return (bool) config('tenantpilot.pdf_renderer.enabled', false)
&& config('tenantpilot.pdf_renderer.driver', 'gotenberg') === 'gotenberg';
}
private function maxHtmlBytes(): int
{
return max(1, (int) config('tenantpilot.pdf_renderer.max_html_bytes', 1024 * 1024));
}
private function maxAssetBytes(): int
{
return max(1, (int) config('tenantpilot.pdf_renderer.max_asset_bytes', 2 * 1024 * 1024));
}
private function maxOutputBytes(): int
{
return max(1, (int) config('tenantpilot.pdf_renderer.max_output_bytes', 10 * 1024 * 1024));
}
private function isFlatAssetFilename(string $filename): bool
{
$filename = trim($filename);
return $filename !== ''
&& $filename === basename($filename)
&& ! str_contains($filename, "\0")
&& $filename !== 'index.html';
}
}