Dokumentation durchblättern
Module entwickeln
PDF & Berichte
Wie Module PDFs erzeugen und eigene Berichtsformate beisteuern — über die PdfRendererRegistry und die ReportFormatRegistry, mit lauffähigen Beispielen.
Berichte sind in einem Winterdienst-System kein Nebenschauplatz: Ein PDF-Einsatznachweis oder ein CSV-Export ist oft das, was am Ende beim Kunden oder in der Akte landet. Schneespur trennt dabei zwei Dinge sauber — das Rendern eines PDFs aus einem Template und das Format eines Berichts. Beide sind über Registries austauschbar, sodass ein Modul eine eigene Render-Engine oder ein zusätzliches Dateiformat beisteuern kann, ohne den Core anzufassen.
PDF-Rendering
Das PdfRendererInterface
Ein PDF-Renderer nimmt einen View-Namen samt Daten entgegen und gibt das fertige PDF als rohe Binärzeichenkette zurück. Mehr verlangt das Interface nicht:
interface PdfRendererInterface
{
public function slug(): string;
public function label(): string;
/**
* @param array{paper?: string, orientation?: string, isRemoteEnabled?: bool,
* footer?: array{left: string, right: string}} $options
* @return string Raw PDF binary
*/
public function render(string $view, array $data, array $options = []): string;
/** @return string Raw PDF binary with footer */
public function renderFooter(string $html, string $leftText, string $rightText): string;
}
Der Vorteil dieser schmalen Schnittstelle: Wer ein PDF braucht, muss nicht wissen, welche
Engine es erzeugt. Der Aufrufer arbeitet immer gegen PdfRendererInterface, der konkrete
Renderer ist austauschbar.
Der Core-Renderer: DomPDF
Standardmäßig registriert der Core den DomPdfRenderer, der auf dem Paket dompdf/dompdf
aufsetzt. DomPDF braucht keine externen Binaries und läuft damit auf gewöhnlichem
Webhosting — das ist der Grund, warum es die Voreinstellung ist. Ein Modul kann diesen
Renderer ohne weitere Einrichtung nutzen.
PDF im eigenen Modul verwenden
Den aktiven Renderer lösen Sie über die PdfRendererRegistry auf und übergeben ihm einen
Blade-View plus die Daten:
$renderer = app(PdfRendererRegistry::class)->resolve();
$pdf = $renderer->render('my-module::reports.invoice', [
'customer' => $customer,
'items' => $items,
'total' => $total,
], [
'paper' => 'A4',
'orientation' => 'portrait',
'footer' => [
'left' => brand() . ' — Rechnung',
'right' => 'Seite {PAGE_NUM} von {PAGE_COUNT}',
],
]);
return response($pdf)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="invoice-' . $customer->id . '.pdf"');
Zwei Details lohnen den Blick: brand() liefert den markenneutralen Anzeigenamen der
Installation, sodass derselbe Code unter Schneespur wie unter Wintertrace passt. Und die
Platzhalter {PAGE_NUM} / {PAGE_COUNT} im Footer werden beim Rendern durch die echte
Seitenzahl ersetzt.
Render-Optionen
Das options-Array steuert das Seitenlayout:
| Option | Typ | Standard | Bedeutung |
|---|---|---|---|
paper | string | 'A4' | Papierformat (A4, Letter usw.) |
orientation | string | 'portrait' | 'portrait' oder 'landscape' |
isRemoteEnabled | bool | false | Externe Bilder laden zulassen |
footer | array | — | {left: string, right: string} für den Fußzeilentext |
isRemoteEnabled steht bewusst auf false: Externe Bilder nachzuladen kostet Zeit und
öffnet eine Tür nach außen. Schalten Sie es nur ein, wenn Ihr Template tatsächlich entfernte
Grafiken braucht.
Einen eigenen PDF-Renderer als Modul bauen
Reicht DomPDF nicht, kann ein Modul einen alternativen Renderer beisteuern — etwa einen, der
das Binary wkhtmltopdf nutzt. Sie implementieren dasselbe Interface:
namespace Schneespur\Module\WkPdf\Pdf;
use App\Services\Pdf\PdfRendererInterface;
class WkhtmltopdfRenderer implements PdfRendererInterface
{
public function slug(): string { return 'wkhtmltopdf'; }
public function label(): string { return 'wkhtmltopdf'; }
public function render(string $view, array $data, array $options = []): string
{
$html = view($view, $data)->render();
// Use wkhtmltopdf binary to convert HTML to PDF
// ...
return $pdfBinary;
}
public function renderFooter(string $html, string $leftText, string $rightText): string
{
// ...
}
}
Weil der Renderer hinter dem Interface steht, ändert sich für die aufrufende Stelle nichts —
sie ruft weiterhin render() auf. Die Registrierung läuft wie bei allen Registries über den
ServiceProvider des Moduls; die genaue API steht unter
Registries.
Berichtsformate
Das Rendern erzeugt ein PDF. Ein Berichtsformat geht eine Ebene höher: Es beschreibt, in welcher Datei-Form ein bestimmter Berichtstyp ausgegeben wird — als PDF, CSV, oder etwas Eigenem.
Das ReportFormatInterface
interface ReportFormatInterface
{
public function slug(): string;
public function label(): string;
/** @return string[] Report types this format supports */
public function supportedReportTypes(): array;
/** @return string File content */
public function generate(string $reportType, mixed $subject, array $params = []): string;
public function mimeType(): string;
public function fileExtension(): string;
}
supportedReportTypes() ist der Grund, warum nicht jedes Format jeden Bericht beherrschen
muss: Ein Format meldet selbst, welche Berichtstypen (etwa job, customer, object) es
bedienen kann. Die Oberfläche bietet dann nur die sinnvollen Kombinationen an.
Die Core-Formate
Der Core bringt zwei Formate mit:
| Slug | Format | Unterstützte Berichtstypen |
|---|---|---|
pdf | job, customer, object | |
csv | CSV | job, customer, object |
Ein eigenes Berichtsformat bauen
Ein Modul kann ein weiteres Format ergänzen — zum Beispiel Excel. Sie implementieren
ReportFormatInterface und liefern den Dateiinhalt als Zeichenkette zurück:
namespace Schneespur\Module\Excel\Report;
use App\Services\Report\ReportFormatInterface;
class ExcelReportFormat implements ReportFormatInterface
{
public function slug(): string { return 'xlsx'; }
public function label(): string { return 'Excel (XLSX)'; }
public function supportedReportTypes(): array
{
return ['job', 'customer', 'driver'];
}
public function generate(string $reportType, mixed $subject, array $params = []): string
{
// Use PhpSpreadsheet or similar to generate Excel content
// Return raw file content as string
}
public function mimeType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
public function fileExtension(): string
{
return 'xlsx';
}
}
Registriert wird das Format über die ReportFormatRegistry:
app(ReportFormatRegistry::class)->register('xlsx', ExcelReportFormat::class);
Danach taucht „Excel (XLSX)” überall dort als Auswahl auf, wo ein unterstützter Berichtstyp exportiert wird — ohne dass eine Core-Datei geändert wurde.
Vorhandene Export-Infrastruktur
Ein Teil der Export-Logik steckt bereits im Core. CsvExportController und
CustomerPdfController übernehmen die Auslieferung, und der PortalPdfController lässt
Kunden im Portal ihre eigenen Berichte erzeugen. Darauf aufbauend hat ein Modul drei Wege:
- Neue Formate (Excel, XML usw.) über die
ReportFormatRegistryergänzen. - Neue Berichtstypen (Fahrer-Übersicht, Fahrzeug-Protokoll usw.) über eigene Controller bereitstellen.
- Bestehende Berichte erweitern, indem es per Filter-Hook zusätzliche Daten einspeist — siehe Filter-Hooks.
Blade-Templates für PDFs
Ein praktischer Stolperstein zum Schluss: DomPDF lädt externe CSS-Dateien nicht zuverlässig.
Blade-Views für die PDF-Ausgabe sollten ihre Stile deshalb inline im <style>-Block
mitbringen statt über ein verlinktes Stylesheet:
{{-- resources/views/reports/invoice.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
th { background: #f5f5f5; }
.total { font-weight: bold; text-align: right; }
</style>
</head>
<body>
<h1>Rechnung — {{ $customer->name }}</h1>
<table>
<thead>
<tr><th>Position</th><th>Beschreibung</th><th>Betrag</th></tr>
</thead>
<tbody>
@foreach($items as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->description }}</td>
<td>{{ number_format($item->amount / 100, 2, ',', '.') }} €</td>
</tr>
@endforeach
</tbody>
</table>
<p class="total">Gesamt: {{ number_format($total / 100, 2, ',', '.') }} €</p>
</body>
</html>
Die Schriftart DejaVu Sans ist hier kein Zufall: Sie bringt die nötigen Glyphen für
deutsche Umlaute und das Euro-Zeichen mit, die in vielen Standard-PDF-Schriften fehlen.