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.

Weiter · Datenquellen Woher die Datensätze kommen, welche Feldtypen es gibt und wie Filter an die Query gehen.