Files
JobcardSystem/app/Domain/Reporting/PrintReportRenderer.php
T

35 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
/** Framework-free HTML renderer intended for browser print/save-to-PDF. */
final class PrintReportRenderer
{
/** @param list<string> $headers @param list<list<mixed>> $rows */
public function render(string $title, array $headers, array $rows): string
{
$headerCount = count($headers);
foreach ($rows as $number => $row) {
if (!is_array($row) || count($row) !== $headerCount) {
throw new InvalidArgumentException(sprintf('Print report row %d must contain %d fields.', $number + 1, $headerCount));
}
}
$head = implode('', array_map(fn(mixed $value): string => '<th>' . $this->escape($value) . '</th>', $headers));
$body = '';
foreach ($rows as $row) {
$body .= '<tr>' . implode('', array_map(fn(mixed $value): string => '<td>' . $this->escape($value) . '</td>', $row)) . '</tr>';
}
$logoFile = function_exists('app_setting') ? app_setting('logo_filename') : null;
$brandName = function_exists('app_setting') ? (app_setting('company_name', 'JOBcard') ?: 'JOBcard') : 'JOBcard';
$brand = $logoFile ? '<div class="report-brand"><img src="/assets/branding/' . $this->escape(basename($logoFile)) . '" alt="' . $this->escape($brandName) . '"></div>' : '';
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="format" content="application/pdf"><title>' . $this->escape($title) . '</title><style>@page{size:auto;margin:1.5cm}body{font-family:Arial,sans-serif;color:#222;margin:2rem}.report-brand{text-align:center;margin:0 0 1.25rem}.report-brand img{max-width:240px;max-height:100px;object-fit:contain}h1{font-size:1.5rem}table{border-collapse:collapse;width:100%}th,td{border:1px solid #bbb;padding:.5rem;text-align:left;vertical-align:top}th{background:#eee}@media print{body{margin:0}.report-brand{margin-bottom:1rem}.report-brand img{max-width:200px;max-height:80px}h1{font-size:1.2rem}table{font-size:10pt}thead{display:table-header-group}tr{page-break-inside:avoid}}</style></head><body><main>' . $brand . '<h1>' . $this->escape($title) . '</h1><table><thead><tr>' . $head . '</tr></thead><tbody>' . $body . '</tbody></table></main></body></html>';
}
/** @param list<array<string,mixed>> $rows */
public function renderRecords(string $title, array $rows): string
{
$headers = $rows === [] ? [] : array_keys($rows[0]);
return $this->render($title, $headers, array_map(fn(array $row): array => array_values($row), $rows));
}
private function escape(mixed $value): string { return htmlspecialchars((string)($value ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
}