Im ersten Artikel habe ich gezeigt was das Report-Plugin produziert. Dieser Artikel zeigt den kompletten Weg dorthin – von der Umfrage bis zum fertigen Report, Schritt für Schritt.
Als Beispiel dient eine Restaurant-Feedback-Umfrage. Ein alltagsnahes Szenario: Gäste bewerten ihren Besuch, das Restaurant will verstehen was gut läuft und wo Verbesserungsbedarf besteht.
Die Umfrage
Die Umfrage besteht aus neun Fragen – kompakt, aber ausreichend für einen differenzierten Report.
| Code | Typ | Frage |
|---|---|---|
q1 |
L – Radio List | Wie bewerten Sie Ihren Restaurantbesuch insgesamt? (1–6) |
nps |
L – Radio List | Wie wahrscheinlich würden Sie uns weiterempfehlen? (0–10) |
matrix |
F – Array | Wie bewerten Sie die folgenden Aspekte? (9 Subfragen, Skala 1–6) |
anlass |
L – Radio List | Aus welchem Anlass waren Sie bei uns? |
besuchszeit |
L – Radio List | Wann haben Sie uns besucht? |
frequency |
L – Radio List | Wie häufig besuchen Sie unser Restaurant? |
strengths |
M – Multiple Choice | Welche Punkte sind Ihnen besonders positiv aufgefallen? |
potentials |
O – List with Comment | Wo sehen Sie Verbesserungsbedarf? |
freetext |
T – Long Text | Möchten Sie uns noch etwas mitteilen? |
Wichtig dabei: die Fragen-Codes – q1, nps, matrix und so weiter. Diese Codes vergebe ich beim Anlegen der Fragen in LimeSurvey bewusst und sprechend. Sie sind der Ankerpunkt für das Mapping im nächsten Schritt.
Das Ziel
Bevor ich das Mapping anlege oder eine Zeile Template-Code schreibe, definiere ich das Ziel: Wie soll der fertige Report aussehen?
Für die Restaurant-Feedback-Umfrage will ich sechs Seiten:
- Dashboard – die wichtigsten Kennzahlen auf einen Blick
- Aspekte des Besuchs – Heatmap mit allen neun Bewertungsaspekten
- Gesamtbewertung & Weiterempfehlung – Bewertungsverteilung und NPS-Breakdown
- Stärken & Verbesserungspotenziale – was Gäste loben und was sie vermissen
- Besuchskontext – wer kommt wann und warum, Gruppenvergleiche
- Freitext-Rückmeldungen – offene Antworten als Kommentar-Feed
Das ist kein Zufallsprodukt – es ist eine bewusste Entscheidung. Nicht jede Frage braucht eine eigene Seite, und nicht jede Frage muss überall auftauchen.
Konzepte definieren
Das Template arbeitet nicht mit Fragen-Codes wie q1 oder matrix – es arbeitet mit Konzepten. Konzepte sind abstrakte Namen die beschreiben was eine Information bedeutet, nicht wo sie in der Datenbank liegt.
Für diesen Report definiere ich neun Konzepte:
| Konzept | Bedeutung |
|---|---|
overall_satisfaction |
Gesamtbewertung des Besuchs |
recommendation |
Weiterempfehlungsbereitschaft (NPS) |
aspects |
Bewertung einzelner Aspekte (Matrix) |
strengths |
Genannte Stärken (Multiple Choice) |
potentials |
Verbesserungsbedarf (mit Kommentar) |
visit_frequency |
Besuchshäufigkeit |
occasion |
Besuchsanlass |
visit_time |
Besuchszeit |
freetext |
Offene Rückmeldungen |
Das Template kennt nur diese Konzeptnamen. Welche konkrete Frage dahintersteckt, weiß das Template nicht – das ist Aufgabe des Mappings.
Mapping anlegen
Das Mapping verbindet Konzepte und Fragen. Die Admin-Oberfläche zeigt für jedes Konzept ein Dropdown mit den kompatiblen Fragen der gewählten Umfrage:

Das Plugin filtert die Frageauswahl automatisch nach kompatiblen Fragetypen. aspects akzeptiert nur Typ F – es erscheinen also nur Matrix-Fragen im Dropdown. recommendation akzeptiert nur L und ! – weil der NPS-Algorithmus numerische Codes 0–10 voraussetzt.
Nach dem Speichern ist das Mapping aktiv. Ab jetzt weiß das Plugin: wenn das Template overall_satisfaction anfragt, soll es die Daten aus q1 liefern.
Den Report definieren
Der vollständige Report umfasst sechs Seiten und knapp 200 Zeilen PHP. Dabei wird nicht beschrieben wie Daten geladen oder Diagramme gerendert werden. Das Template definiert ausschließlich Struktur und Semantik des Reports. Ich zeige die drei aussagekräftigsten Ausschnitte.
Das Dashboard – acht KPI-Karten in zwei Reihen:
$report->page('dashboard', function (PageBuilder $page): void {
$page->title('Dashboard');
$page->section(function (SectionBuilder $section): void {
$section->kpi('response-count')
->title('Befragte gesamt')->icon('👥')->color('primary')->col(3);
// Skala 1–6 (1 = Sehr gut) → invertProgress: niedriger Wert = besserer Balken
$section->kpi('sc-mean', 'overall_satisfaction')
->title('Ø Gesamtbewertung')->icon('⭐')->color('success')
->invertProgress()->scaleMax(6)->col(3);
// Farbe wird zur Laufzeit automatisch gesetzt (grün/gelb/rot je nach Score)
$section->kpi('nps', 'recommendation')
->title('Net Promoter Score')->icon('📊')->color('info')->col(3);
$section->kpi('completion-rate')
->title('Abschlussquote')->icon('✅')->color('secondary')
->withProgressBar()->col(3);
});
$page->section(function (SectionBuilder $section): void {
$section->kpi('best-rated-aspect', 'aspects')
->title('Bester Aspekt')->icon('🏆')->color('success')->col(3);
$section->kpi('worst-rated-aspect', 'aspects')
->title('Schwächster Aspekt')->icon('📉')->color('danger')->col(3);
$section->kpi('mc-top-answer', 'strengths')
->title('Meist genannte Stärke')->icon('💪')->color('success')->col(3);
$section->kpi('top-answer', 'potentials')
->title('Häufigster Kritikpunkt')->icon('🔧')->color('warning')->col(3);
});
});
invertProgress() und scaleMax(6) tauchen mehrfach auf – das ist die Schulnoten-Logik: auf einer Skala von 1–6 ist ein niedriger Wert besser, der Fortschrittsbalken muss also invertiert werden damit er visuell korrekt ist.
Die Heatmap – Bewertungsverteilung je Aspekt:
$report->page('aspects', function (PageBuilder $page): void {
$page->title('Aspekte des Besuchs');
$page->section(function (SectionBuilder $section): void {
$section->kpi('best-rated-aspect', 'aspects')
->title('Beste Kategorie')->icon('🏆')->color('success')->col(3);
$section->kpi('worst-rated-aspect', 'aspects')
->title('Schwächste Kategorie')->icon('📉')->color('danger')->col(3);
$section->kpi('mean-score', 'aspects')
->title('Ø Aspect Score')->icon('📊')->color('info')
->invertProgress()->scaleMax(6)->col(3);
// Anteil kritischer Bewertungen (Noten 5+6) — letzte 2 Antwortoptionen
$section->kpi('matrix-bottom-box', 'aspects')
->title('Kritische Bewertungen')->icon('⚠️')->color('warning')
->boxes(2)->col(3);
});
$page->section(function (SectionBuilder $section): void {
$section->matrixHeatmap('aspects')
->title('Bewertungsverteilung je Aspekt')->fullWidth();
});
});
Stärken & Verbesserungspotenziale – zwei Charts mit semantischer Farbwelt:
$report->page('feedback', function (PageBuilder $page): void {
$page->title('Stärken & Verbesserungspotenziale');
// …KPI-Zeile…
// Stärken: grüne Farbwelt
$page->section(function (SectionBuilder $section): void {
$section->multipleChoiceFrequency('strengths')
->title('Was ist Ihnen besonders positiv aufgefallen?')
->asChart()->percentage()->barColor('#59a14f')->fullWidth();
});
// Verbesserungspotenziale: orange/warning Farbwelt
$page->section(function (SectionBuilder $section): void {
$section->singleChoiceFrequency('potentials')
->title('Wo sehen Sie Verbesserungsbedarf?')
->asChart('bar')->percentage()->barColor('#FF7043')->fullWidth();
});
});
Die Farbwahl ist bewusst – grün für Stärken, orange für Verbesserungspotenziale. Ein einziger Parameter pro Chart.
Das Ergebnis
Hier ist was der Report produziert. Alle Screenshots basieren auf demselben Template und demselben Mapping – nicht auf fünf verschiedenen Reports.
Dashboard – acht KPI-Karten, sofort lesbar:

Aspekte des Besuchs – Heatmap mit neun Aspekten:

Gesamtbewertung & Weiterempfehlung – Bewertungsverteilung und NPS-Breakdown:

Stärken & Verbesserungspotenziale – was Gäste loben und was sie vermissen:

Besuchskontext – Gruppenvergleiche nach Häufigkeit und Anlass:

Was wurde nicht programmiert?
Das ist der Teil der mich selbst immer wieder überrascht wenn ich einen neuen Report fertigstelle.
Für diesen Report – sechs Seiten, acht KPI-Karten allein auf dem Dashboard, Heatmap, NPS-Breakdown, Gruppenvergleiche, Freitext-Feed – wurde:
- keine einzige SQL-Abfrage geschrieben – Datenbankzugriffe, Aggregation nach Gruppen, Filterung nach
submitdate, Joins mit der Token-Tabelle – alles intern - kein Chart initialisiert – kein Chart.js, kein D3, keine Konfigurationsobjekte
- kein HTML-Template gebaut – keine Twig-Dateien, keine Bootstrap-Layouts, kein CSS
- keine Filterlogik implementiert – Filter-Button, Seitenleiste, URL-Parameter, SQL-WHERE-Klauseln – out of the box
- keine Zugangskontrolle gebaut – Run-Mechanismus, Token-Validierung, Gültigkeitszeiträume – konfigurierbar, nicht programmierbar
- keine Farblogik für den NPS geschrieben – die KPI-Karte wechselt automatisch die Farbe je nach Score: grün bei positivem NPS, gelb bei neutralem, rot bei negativem. Ein Kommentar im Code sagt alles:
// overridden at runtime by KpiNpsHandler (auto-color)
Was tatsächlich geschrieben wurde: knapp 200 Zeilen PHP.
Ein sechsseitiger Report mit Dashboard, Heatmap, NPS-Auswertung, Gruppenvergleichen und Freitext-Feed entstand aus knapp 200 Zeilen PHP.
Das ist die Struktur des Reports in der DSL. Seiten, Sektionen, Darstellungstypen, Konzeptnamen, Farbparameter. Der Rest ist Framework.
Das ist der eigentliche Wert eines Reporting-Frameworks gegenüber einer Eigenentwicklung: nicht dass man weniger kann – sondern dass man sich auf das konzentriert was den Report einzigartig macht, statt jedes Mal dieselbe Infrastruktur neu zu bauen.
Das Report-Plugin, die Dokumentation und Demo-Anfragen: Kontakt