Skip to content

Commit 17366ca

Browse files
committed
pdf generator
1 parent 893bca2 commit 17366ca

5 files changed

Lines changed: 204 additions & 2 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"symfony/runtime": "5.4.*",
3636
"league/flysystem-bundle": "^3.3",
3737
"league/flysystem-async-aws-s3": "^3.29",
38-
"league/flysystem-aws-s3-v3": "^3.29"
38+
"league/flysystem-aws-s3-v3": "^3.29",
39+
"knplabs/knp-snappy-bundle": "^1.10"
3940
},
4041
"require-dev": {
4142
"doctrine/doctrine-fixtures-bundle": "^3.4",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
"webpack-cli": "^4.7.2",
1616
"webpack-copy-plugin": "0.0.4"
1717
}
18-
}
18+
}

src/Controller/Api/ValidationsController.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Entity\Validation;
66
use App\Exception\ApiException;
77
use App\Export\CsvReportWriter;
8+
use App\Export\PdfReportWriter;
89
use App\Repository\ValidationRepository;
910
use App\Service\MimeTypeGuesserService;
1011
use App\Service\ValidatorArgumentsService;
@@ -323,6 +324,28 @@ public function downloadSourceData($uid)
323324
return $this->getDownloadResponse($zipFilepath, $validation->getDatasetName() . '-source.zip');
324325
}
325326

327+
/**
328+
* @Route(
329+
* "/{uid}/files/pdf",
330+
* name="validator_api_download_pdf",
331+
* methods={"GET"}
332+
* )
333+
*/
334+
public function generatePdf($uid, PdfReportWriter $writer,
335+
): Response {
336+
$validation = $this->repository->findOneByUid($uid);
337+
if (!$validation) {
338+
throw new ApiException("No record found for uid=$uid", Response::HTTP_NOT_FOUND);
339+
}
340+
341+
$pdf = $writer->generate($validation);
342+
343+
return new Response($pdf, Response::HTTP_OK, [
344+
'Content-Type' => 'application/pdf',
345+
'Content-Disposition' => 'inline; filename="report.pdf"',
346+
]);
347+
}
348+
326349
/**
327350
* Returns binary response of the specified file.
328351
*

src/Export/PdfReportWriter.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Export;
4+
5+
use App\Entity\Validation;
6+
use Knp\Snappy\Pdf;
7+
use Twig\Environment;
8+
9+
class PdfReportWriter
10+
{
11+
public function __construct(
12+
private readonly Pdf $snappy,
13+
private readonly Environment $twig,
14+
) {}
15+
16+
/**
17+
* @param Validation $validation
18+
* @return string Raw PDF binary content
19+
*/
20+
public function generate(Validation $validation): string
21+
{
22+
$entries = json_decode($validation->getResults());
23+
24+
$hasErrors = (bool) array_filter(
25+
$entries,
26+
static fn(array $e): bool => strtolower($e['level']) === 'error'
27+
);
28+
29+
$order = ['error', 'warning', 'info'];
30+
$grouped = [];
31+
32+
foreach ($entries as $entry) {
33+
$grouped[$entry['level']][] = $entry;
34+
}
35+
36+
$html = $this->twig->render('pdfModel.html.twig', [
37+
'groupedEntries' => $grouped,
38+
'hasErrors' => $hasErrors,
39+
]);
40+
41+
return $this->snappy->getOutputFromHtml($html, [
42+
'encoding' => 'UTF-8',
43+
'enable-local-file-access' => true,
44+
'margin-top' => '10mm',
45+
'margin-bottom' => '10mm',
46+
'margin-left' => '12mm',
47+
'margin-right' => '12mm',
48+
]);
49+
}
50+
}

templates/pdfModel.html.twig

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!DOCTYPE html>
2+
<html lang="fr">
3+
<head>
4+
<meta charset="UTF-8">
5+
<style>
6+
body {
7+
font-family: DejaVu Sans, Arial, sans-serif;
8+
font-size: 13px;
9+
color: #1a1a2e;
10+
margin: 40px;
11+
}
12+
13+
.banner {
14+
padding: 16px 24px;
15+
border-radius: 6px;
16+
font-size: 15px;
17+
font-weight: bold;
18+
margin-bottom: 32px;
19+
border-left: 6px solid;
20+
}
21+
22+
.banner.has-errors {
23+
background-color: #fde8e8;
24+
border-color: #e53e3e;
25+
color: #c53030;
26+
}
27+
28+
.banner.no-errors {
29+
background-color: #e6f4ea;
30+
border-color: #38a169;
31+
color: #276749;
32+
}
33+
34+
h2.group-title {
35+
font-size: 13px;
36+
text-transform: uppercase;
37+
letter-spacing: 0.08em;
38+
margin: 28px 0 8px;
39+
padding-bottom: 4px;
40+
border-bottom: 2px solid currentColor;
41+
}
42+
43+
h2.level-error { color: #e53e3e; border-color: #e53e3e; }
44+
h2.level-warning { color: #d97706; border-color: #d97706; }
45+
h2.level-info { color: #3182ce; border-color: #3182ce; }
46+
47+
table {
48+
width: 100%;
49+
border-collapse: collapse;
50+
margin-bottom: 8px;
51+
}
52+
53+
thead th {
54+
background-color: #2d3748;
55+
color: #ffffff;
56+
text-align: left;
57+
padding: 8px 12px;
58+
font-size: 12px;
59+
text-transform: uppercase;
60+
letter-spacing: 0.05em;
61+
}
62+
63+
tbody tr:nth-child(even) { background-color: #f7fafc; }
64+
tbody tr:nth-child(odd) { background-color: #ffffff; }
65+
66+
tbody td {
67+
padding: 7px 12px;
68+
border-bottom: 1px solid #e2e8f0;
69+
vertical-align: top;
70+
}
71+
72+
.badge {
73+
display: inline-block;
74+
padding: 2px 8px;
75+
border-radius: 4px;
76+
font-size: 11px;
77+
font-weight: bold;
78+
text-transform: uppercase;
79+
}
80+
81+
.badge-error { background: #fed7d7; color: #c53030; }
82+
.badge-warning { background: #fefcbf; color: #b7791f; }
83+
.badge-info { background: #bee3f8; color: #2b6cb0; }
84+
85+
.code { font-family: monospace; font-size: 12px; color: #553c9a; }
86+
</style>
87+
</head>
88+
<body>
89+
90+
{# ── Top banner ── #}
91+
{% if hasErrors %}
92+
<div class="banner has-errors">
93+
Document Invalide
94+
</div>
95+
{% else %}
96+
<div class="banner no-errors">
97+
Document Valide
98+
</div>
99+
{% endif %}
100+
101+
{# ── One table per level ── #}
102+
{% for level, rows in groupedEntries %}
103+
<h2 class="group-title level-{{ level|lower }}">{{ level|upper }}</h2>
104+
105+
<table>
106+
<thead>
107+
<tr>
108+
<th style="width: 18%">Code</th>
109+
<th style="width: 14%">Level</th>
110+
<th>Message</th>
111+
</tr>
112+
</thead>
113+
<tbody>
114+
{% for entry in rows %}
115+
<tr>
116+
<td class="code">{{ entry.code }}</td>
117+
<td>
118+
<span class="badge badge-{{ entry.level|lower }}">{{ entry.level }}</span>
119+
</td>
120+
<td>{{ entry.message }}</td>
121+
</tr>
122+
{% endfor %}
123+
</tbody>
124+
</table>
125+
{% endfor %}
126+
127+
</body>
128+
</html>

0 commit comments

Comments
 (0)