Report-Definition
Ein Report ist eine PHP-Klasse plus ein fluenter Builder. Die Klasse liefert Daten und Einstiegspunkte, der Builder deklariert Bänder, Gruppen, Filter und Aggregate.
Die Report-Klasse
Jeder Report erbt von Guggach\Reports\Report. Pflicht sind vier Methoden; alles andere sind optionale Hooks für Layout, Seitenformat, PDF und Filter.
| key(): string | Stabiler Schlüssel des Reports, z. B. price_list. Er adressiert Routen, Views und den Übersetzungs-Namespace. |
|---|---|
| name(): string | Anzeigename für Seitentitel und PDF. |
| source(): ReportSource | Liefert die Datenquelle des Reports. Quellen sind reportlokal und werden nie zwischen Reports geteilt. |
| define(ReportBuilder $r): void | Deklariert Bänder, Gruppen, Filter und Aggregate. Hier entsteht die eigentliche Report-Definition. |
| layout(): string | Layout-Klasse oder Blade-View für das äussere Gerüst. Default: default. |
| pageSetup(): ?PageSetup | Optionaler Report-Override für Papier, Ausrichtung und Ränder. Er gewinnt über Layout und Config. |
| pdfHeader(): ?string · pdfFooter(): ?string | HTML-Templates für PDF-Kopf und -Fuss. Chromium ersetzt .pageNumber, .totalPages und .title; die HTML-Vorschau bleibt unverändert. |
| filterControls(): array | Beschreibt das Filterformular als FilterControl-Liste. Ohne Override wird pro filterbarem Source-Feld ein Control erzeugt. |
| withFilters(array $filters): static · filters(): array | Setzt und liest die Laufzeitfilter. Die Engine ruft withFilters() vor dem Rendern auf. |
| definition(): ReportDefinition | Baut die unveränderliche ReportDefinition über den Builder. Wird von Engine und Vorschau verwendet. |
| viewNamespace(): string · viewsPath(): ?string | Blade-Namespace und Verzeichnis der Modul-Views. Der Namespace ist standardmässig der key() mit Unterstrichen. |
viewNamespace() und viewsPath() leiten die Modul-Views aus key() und dem Ordner der Klasse ab. Überschreibe sie nur, wenn deine Views bewusst woanders liegen.
<?php
declare(strict_types=1);
namespace App\Reports\PriceList;
use App\Reports\Layout\CompanyLayout;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Filters\FilterBuilder;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;
final class Report extends BaseReport
{
public function key(): string
{
return 'price_list';
}
public function name(): string
{
return 'Preisliste';
}
public function layout(): string
{
return CompanyLayout::class;
}
public function source(): ReportSource
{
return new Source;
}
/**
* Demonstrate the fluent control description: operator listbox for the
* number, multiselect for the enum and a searchable dual-list for the
* relation (id/FK) field.
*/
public function filterControls(): array
{
return FilterBuilder::for($this->source())
->field('price')->operators(['=', '>=', '<=', 'between', 'in'])
->field('category')->multiselect()
->field('supplier')->picker(valueField: 'id', labelField: 'label')
->build();
}
public function define(ReportBuilder $r): void
{
// Module-relative view names resolve to app/Reports/PriceList/views/*.
// Report start/end run once per report; the grid header introduces the
// table and the detail band renders one row per record.
$r->reportStart(view: 'report-start');
$r->gridHeader(view: 'grid-head');
$r->detail(view: 'row');
$r->reportEnd(view: 'report-end');
// Grouped by category (data is sorted by category) with a subtotal.
$r->group('category', function ($g): void {
$g->header(view: 'category-head');
$g->footer(view: 'category-foot');
$g->aggSum('price', as: 'category_total');
});
}
}
Die vollständige Report-Klasse der Preisliste — inklusive Layout, Filterformular und Gruppen.
Der ReportBuilder
Der Builder ist das fluent Interface in define(). Alle Methoden geben den Builder zurück, damit sich Bänder und Gruppen verketten lassen.
| mode(ReportMode $mode) | Flow oder Strict. Überschreibt reports.default_mode für diesen Report. |
|---|---|
| pageSetup(PageSetup $setup) | Papierformat, Ausrichtung, Einheit, Ränder und DPI für diesen Report. |
| layout(string $layout) | Layout-Klasse oder View-Name. Ein layout() in der Report-Klasse hat Vorrang. |
| detail($view = null, ?Closure $closure = null, ?float $height = null) | Das zentrale Band: läuft einmal pro Datensatz. Akzeptiert einen View-Namen, eine Closure oder eine fertige Detail-Instanz. |
| freeform($view = null, ?Closure $closure = null, ?float $height = null) | Free-form-Report: ein Detailband enthält das komplette Formular und markiert automatisch eine Formularinstanz. Keine Gruppen und kein N-up. |
| pageHeader($view = null, ?Closure $closure = null) | Seitenkopf. Wird in Strict pro Seite wiederholt; typischerweise mit hideOnFirstPage() kombiniert. |
| pageFooter($view = null, ?Closure $closure = null) | Seitenfuss, etwa für Seitenzahlen. |
| reportStart($view = null, ?Closure $closure = null) | Band am Reportanfang, genau einmal. Ideal für Titel und Kopfdaten. |
| reportEnd($view = null, ?Closure $closure = null) | Band am Reportende, genau einmal. Hier stehen die Report-Aggregate zur Verfügung. |
| gridHeader($view = null, ?Closure $closure = null) | Tabellenkopf. In Strict auf jeder Seite wiederholt. Alternative: detail()->repeatGridHeader(). |
| pageNumbering(string $mode) | continuous oder perForm. perForm startet den Seitenzähler bei jeder Formularinstanz neu; typisch für Rechnungen. |
| filter(array $filter) | Low-Level-Definition eines Filters. Für Formulare ist filterControls() in der Report-Klasse der übliche Weg. |
| group(string|array $key, ?Closure $callback = null) | Definiert eine Gruppe und gibt einen GroupBuilder zurück. Ein Array erzeugt einen zusammengesetzten Schlüssel. |
| aggregate(string $name, string $class, ?string $field = null, string $scope = 'report') | Report-Aggregat mit Name, Aggregat-Klasse und optionalem Feld. Der Scope wird entgegengenommen; in v1 sind Report-Aggregate reportweit. |
| build(): ReportDefinition | Erzeugt die Definition. Wird von Report::definition() aufgerufen und normalerweise nicht direkt verwendet. |
Bänder und ihre Optionen
Jede Band-Methode gibt ein Band-Objekt zurück. Diese Optionen gelten für alle Bänder; Spezialfälle ergänzen eigene Methoden.
| view(?string $view) · closure(?Closure $closure) | Renderquelle des Bands. Eine Closure erhält den RenderContext; ein View-Name wird relativ zum Modul aufgelöst. |
|---|---|
| height(?float $height) · minHeight(?float $minHeight) | Fixe Höhe (Pflicht in Strict) oder Mindesthöhe (Flow), in der Einheit der PageSetup. |
| keepTogether(bool $keepTogether = true) | Verhindert einen Umbruch mitten im Band. Beim Detailband ist das standardmässig aktiv. |
| breakBefore(bool $on = true) · breakAfter(bool $on = true) | Erzwingt einen Seitenumbruch vor oder nach dem Band. |
| repeatOnNewPage(bool $repeat = true) | Wiederholt das Band nach einem Seitenumbruch, etwa einen Tabellenkopf. |
| hideOnFirstPage() · hideOnLastPage() · onlyOddPages() · onlyEvenPages() | Sichtbarkeit des Bands. Mehrere Einschränkungen lassen sich kombinieren. |
| pages(array $pages) | Explizite Seitenliste, z. B. [1] für ein Band nur auf der ersten Seite. |
| formScope(bool $on = true) · restartPageNumber(bool $on = true) | Markiert den Beginn einer Formularinstanz und setzt den Seitenzähler je Instanz zurück. |
| localeUsing(Closure|string|null $resolver) | Sprachresolver für ein Band. Die API ist vorhanden; in v1 wertet die Engine ihn noch nicht aus. |
Zusätzliche Optionen am Detailband
Das Detailband ist das einzige Band mit Spaltenlogik (N-up) und einem eingebauten Tabellenkopf.
| repeatGridHeader(?string $view = null, ?Closure $closure = null) | Hängt einen Tabellenkopf direkt an das Detailband; er wird auf jeder neuen Seite wiederholt. |
|---|---|
| columns(int $columns, float $gap = 0.0, float $rowGap = 0.0) | Teilt das volle Band in N Zellen pro Zeile. height meint dann die Zeilenhöhe; gap ist horizontal, rowGap vertikal. |
| fill(string $order) | Füllreihenfolge der Zellen. In v1 ist nur row implementiert; andere Werte lösen eine InvalidArgumentException aus. |
| pad(bool $pad = true) | Füllt die letzte Zeile mit leeren Zellen auf, damit das Spaltenraster bündig bleibt. |
<?php
declare(strict_types=1);
namespace App\Reports\PackingList;
use Guggach\Reports\Definition\PageSetup;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Definition\ReportMode;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;
final class Report extends BaseReport
{
public function key(): string
{
return 'packing_list';
}
public function name(): string
{
return 'Lieferschein';
}
public function layout(): string
{
return 'default';
}
public function source(): ReportSource
{
return new Source;
}
public function define(ReportBuilder $r): void
{
$r->mode(ReportMode::Strict);
$r->pageSetup(PageSetup::a4()->portrait()->marginsMm(20, 15, 20, 15));
// Module-relative view names resolve to app/Reports/PackingList/views/*.
$r->pageHeader(view: 'header')->height(20);
$r->gridHeader(view: 'grid-head')->height(8);
$r->detail(view: 'row', height: 8);
$r->pageFooter(view: 'footer')->height(10);
}
}
<?php
declare(strict_types=1);
namespace App\Reports\LabelSheet;
use Guggach\Reports\Definition\PageSetup;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Definition\ReportMode;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;
/**
* N-up demo: one full-width detail band split into four label cells per row
* (docs/spec.md 6.7). Strict mode makes the row the pagination unit.
*/
final class Report extends BaseReport
{
public function key(): string
{
return 'label_sheet';
}
public function name(): string
{
return 'Versandetiketten';
}
public function layout(): string
{
return 'default';
}
public function source(): ReportSource
{
return new Source;
}
public function define(ReportBuilder $r): void
{
$r->mode(ReportMode::Strict);
$r->pageSetup(PageSetup::a4()->portrait()->marginsMm(16, 12, 16, 12));
$r->pageHeader(view: 'header')->height(14);
$r->pageFooter(view: 'footer')->height(8);
// One row holds four labels; height is the height of the whole row.
// A row occupies 32 mm + 2 mm row gap = 34 mm; 7 rows (238 mm) fit
// into the 243 mm body of an A4 page => 28 labels.
$r->detail(view: 'label', height: 32)
->columns(4, gap: 3, rowGap: 2);
}
}
Strict-Modus mit fixen Höhen und N-up mit vier Etiketten pro Zeile.
Seite einrichten
PageSetup beschreibt Papier und Nutzfläche. Die Engine rechnet daraus die Kapazität für den Strict-Modus.
| PageSetup::a4() | A4 hochkant mit den Standardrändern 20/15/20/15 mm und Einheit mm. |
|---|---|
| PageSetup::fromConfig(mixed $config) | Baut die Einstellungen aus reports.paper. Intern für die Config-Ebene der Kaskade. |
| portrait() · landscape() | Ändert die Ausrichtung und liefert eine neue Instanz. |
| marginsMm(int $top, int $right, int $bottom, int $left) | Setzt alle vier Ränder in Millimeter. |
| dimensions(): array | Liefert Breite und Höhe in mm für die gewählte Grösse und Ausrichtung. Unterstützt a3, a4, a5, letter und legal. |
Die Kaskade lautet Config (reports.paper) → Layout (Layout::pageSetup()) → Builder ($r->pageSetup()) → Report (Report::pageSetup()). In v1 gewinnt die höchste gesetzte Ebene als Ganzes; ein feldweiser Merge ist noch nicht implementiert.
Was die Views bekommen
Jedes Band erhält einen RenderContext. Er ist der einzige Übergabepunkt zwischen Engine und Blade — und funktioniert für Views genauso wie für Closures.
Eigenschaften
| $ctx->report | Die Report-Instanz, z. B. für $ctx->report->name(). |
|---|---|
| $ctx->definition | Die aufgelöste ReportDefinition mit Bändern, Gruppen und Modus. |
| $ctx->record | Der aktuelle Datensatz. Bei Arrays, Models und Objekten identisch nutzbar. |
| $ctx->index | Nullbasierter Index des Datensatzes im gesamten Report. |
| $ctx->total | Anzahl Datensätze im Report (nicht Seiten). |
| $ctx->filters | Die aktiven Filterwerte des Laufs. |
| $ctx->locale | Aktive Sprache des Laufs oder null. |
| $ctx->groups | Die aktuell offenen Gruppen als Liste aus level, keys und key. |
| $ctx->aggregates | Aggregatwerte nach Name. Details siehe Seite Gruppen & Aggregate. |
| $ctx->page | Seitenmetadaten im Strict-Free-form-Streaming: number, total, local, formTotal, global. In Flow und Strict stehen {page}/{pages} als Platzhalter bereit. |
| $ctx->pageCursor | PageCursor für Strict-Free-form-Blöcke; ausserhalb davon null. |
Methoden
| $ctx->field(string $key, mixed $default = null) | Liest ein Feld aus dem Datensatz. Der gleiche Zugriff funktioniert direkt als $ctx->feldname. |
|---|---|
| $ctx->isFirst() | True beim ersten Datensatz des Reports. |
| $ctx->isLast() | True beim letzten Datensatz des Reports. |
| $ctx->currentGroup() | Die innerste offene Gruppe als Array mit level, keys und key — oder null. |
Kurzform: Statt $ctx->field('price') genügt in Blade $ctx->price. Die Magie greift nur, wenn keine echte Eigenschaft oder Methode dieses Namens existiert.
<tr style="border-bottom:1px solid #ddd">
<td>{{ $ctx->sku }}</td>
<td>{{ $ctx->name }}</td>
<td style="text-align:right">{{ number_format((float) $ctx->price, 2, '.', "'") }}</td>
</tr>
<tr style="font-weight:600; background:#f3f4f6">
<td colspan="2">Summe {{ $ctx->currentGroup()['key'] }}</td>
<td style="text-align:right">{{ number_format((float) $ctx->aggregates['category_total'], 2, '.', "'") }}</td>
</tr>
</tbody>
</table>
<p style="margin-top:12px; color:#555">{{ $ctx->total }} Positionen</p>
Drei Views aus der Preisliste: Detailzeile, Gruppenfuss mit Aggregat und Report-Ende mit Gesamtzahl.