Datenquellen

Eine Source liefert die Datensätze und beschreibt ihre Felder. Sie ist reportlokal, kennt Filter und Sortierung und entscheidet, ob diese in SQL oder im Speicher angewendet werden.

Der ReportSource-Vertrag

Von ReportSource erbst du dich ein. Pflicht ist nur records(); alle anderen Methoden haben sinnvolle Defaults oder sind optionale Hooks.

records(): iterable Pflicht. Liefert die Datensätze als Array, Collection, Generator oder Eloquent-Collection.
fields(): array Liste von ReportField-Objekten. Sie ist die Allow-List für Filter, Sortierung und Spalten.
parameters(): array Deklariert Laufzeitparameter als ReportParameter. Metadaten für UI und Validierung; die Engine reicht sie nicht automatisch weiter.
key(): string Identität der Source. Default ist der Klassename.
locale(): ?string · withLocale(?string $locale) Aktive Sprache lesen und setzen. Die Engine setzt sie vor dem Lauf.
filters(): ?FilterBag · withFilters(FilterBag|array $filters) Aktive Filter lesen und setzen. Ein Array wird automatisch in eine FilterBag umgewandelt.
sorts(): ?SortBag · withSorts(SortBag|array $sorts) Aktive Sortierung lesen und setzen.
filterFields(): array Leitet aus fields() alle filterbaren Felder als FilterField ab.
applyFilters(array $records): array Protected Helper für In-Memory-Filterung. Nutzt dieselben Operatoren wie die Query-Variante.
applySorts(array $records): array Protected Helper für In-Memory-Sortierung anhand der Feld-Allow-List.
sortSpecs(): array Protected Helper, der die Sortierung als ORDER-BY-Spezifikationen für eine Datenbank-Query liefert.

Die Engine ruft withLocale(), withFilters() und withSorts() vor records() auf. Eine Source kann diese Werte auswerten, muss aber nicht: Ohne Auswertung bleiben Filter und Sortierung wirkungslos.

Fertige Source-Typen

Das Paket bringt fünf Adapter mit. DatabaseSource ist die gemeinsame Basis für Eloquent und Query Builder und wird nicht direkt verwendet.

new ArraySource($records = [], $fields = [], $parameters = []) Array im Speicher. Filter und Sortierung werden in PHP angewendet; ideal für Tests und berechnete Daten.
new CollectionSource($items = [], $fields = [], $parameters = []) Collection oder Iterable im Speicher. Verhält sich wie ArraySource, akzeptiert aber auch Lazy-Collections.
new EloquentSource(Model|Builder|class-string $source, $fields = [], $parameters = []) Eloquent-Modell, Builder oder Modellklasse. Filter und Sortierung werden in die Query geschoben; die Zeilen bleiben Models.
new QuerySource(Builder $query, $fields = [], $parameters = []) Laravel Query Builder, etwa DB::table(). Filter und Sortierung werden als WHERE und ORDER BY pushed down.
new RawSqlSource(string $sql, array $bindings = [], $fields = [], $parameters = [], ?string $connection = null) Rohes SQL mit Bindings. Filter und Sortierung werden nach dem Fetch im Speicher angewendet, weil sich beliebiges SQL nicht sicher umschreiben lässt.
use App\Models\Customer;
use Guggach\Reports\Sources\EloquentSource;
use Guggach\Reports\Sources\QuerySource;
use Guggach\Reports\Sources\RawSqlSource;
use Guggach\Reports\Sources\ReportField;

// The field DTOs are a normal PHP variable. They are optional for every
// source: without them there is no filter/sort allow-list.
$fields = [
    new ReportField('id', 'ID', type: 'id'),
    new ReportField('name', 'Name'),
    new ReportField('revenue', 'Revenue', type: 'number'),
    new ReportField('active', 'Active', type: 'boolean'),
];

// Eloquent: model, builder or model class name
new EloquentSource(
    Customer::query()->where('active', true),
    fields: $fields,
);

// Query builder: DB::table()
new QuerySource(
    DB::table('customers')->where('active', true),
    fields: $fields,
);

// Raw SQL: SQL and bindings stay developer-owned
new RawSqlSource(
    'select id, name, revenue from customers where active = ?',
    [1],
    fields: $fields,
    connection: 'reporting',
);
<?php

declare(strict_types=1);

namespace App\Reports\PriceList;

use Guggach\Reports\Sources\ReportField;
use Guggach\Reports\Sources\ReportSource;

/**
 * Data source of the price list. Report-local on purpose (spec 7.1): a source
 * is never shared between reports. Filters are applied in memory here; a
 * database-backed variant would push them down instead.
 */
final class Source extends ReportSource
{
    public function records(): iterable
    {
        return $this->applyFilters([
            ['sku' => 'A-100', 'name' => 'Kaffeemaschine', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 129.00],
            ['sku' => 'B-200', 'name' => 'Wasserkocher', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 39.90],
            ['sku' => 'C-300', 'name' => 'Toaster', 'category' => 'Büro', 'supplier' => 'Berger GmbH', 'price' => 59.50],
            ['sku' => 'D-400', 'name' => 'Mixer', 'category' => 'Büro', 'supplier' => 'Conti AG', 'price' => 89.00],
        ]);
    }

    public function fields(): array
    {
        return [
            new ReportField('sku', 'SKU', type: 'string'),
            new ReportField('name', 'Bezeichnung', type: 'string'),
            new ReportField('category', 'Kategorie', type: 'enum', options: ['Küche', 'Büro']),
            new ReportField('supplier', 'Lieferant', type: 'relation', options: [
                'Nordwind AG', 'Berger GmbH', 'Conti AG', 'Delta Handels',
                'Egger & Co', 'Fuchs AG', 'Graf GmbH', 'Huber AG',
            ]),
            new ReportField('price', 'Preis', type: 'number'),
        ];
    }
}

Eine eigene In-Memory-Source und die drei Datenbank-Adapter im Vergleich.

fields ist optional — ohne DTOs gibt es keine Filter und keine Sortierung

Der Parameter fields hat den Default []. Ohne Feldliste ist fields() leer: filterFields() liefert nichts, das Filterformular bleibt leer, übergebene Filter werden verworfen und applySorts()/sortSpecs() erzeugen keine Sortierung. Der Report rendert trotzdem alle Datensätze, weil die Blade-Views direkt auf die Record-Felder zugreifen. Die Feldliste ist eine Allow-List für Filter, Sortierung und UI-Metadaten — sie wählt keine Datenbankspalten aus. $fields im Beispiel ist eine gewöhnliche PHP-Variable mit ReportField[]; du kannst die DTOs auch direkt in den Konstruktor schreiben.

Felder beschreiben

ReportField ist ein unveränderliches DTO. Es beschreibt, wie ein Datensatzfeld heisst, angezeigt, gefiltert und sortiert wird.

string $key Feldname im Datensatz, z. B. price.
string $label Anzeigename in Filterformular und Spaltenköpfen.
string $type = 'string' Feldtyp. Er steuert das Default-Control und die verfügbaren Operatoren.
bool $sortable = true Erlaubt dieses Feld in der Sortierung.
bool $filterable = true Nimmt das Feld in das Filterformular und in filterFields() auf.
bool $aggregatable = false Metadatum für UI und Exporte. Die Engine leitet daraus keine Aggregate ab.
bool $hidden = false Metadatum, um ein Feld in der UI auszublenden.
?string $format = null Optionales Formatierungs-Metadatum, etwa currency oder date.
array $options = [] Werte für enum- und relation-Felder. Als Liste oder als Wert/Label-Map.

Feldtypen und ihre Defaults

Der Typ bestimmt das Widget und die Operatoren. Über FilterBuilder lässt sich beides pro Feld überschreiben.

string Text-Control mit like, = und !=.
number · int · integer · decimal · float Number-Control mit =, !=, <, <=, >, >=, between und in.
boolean · bool Boolean-Control mit dem Operator = .
date · datetime Date-Control mit =, before, after und between.
enum Select bei höchstens sechs Optionen, sonst Multiselect. Operator: in.
id · fk · relation Searchable Picker (Dual-List) mit dem Operator in.
json Nur is_null und is_not_null.

Parameter

ReportParameter deklariert Eingaben, die nicht über die Filter-Allow-List laufen — etwa einen festen Stichtag oder einen Mandanten.

string $name Name des Parameters.
string $type = 'string' Typ für UI und Validierung.
bool $required = false Markiert den Parameter als Pflicht.
mixed $default = null Vorbelegung, wenn der Aufrufer keinen Wert liefert.

Filter, Sortierung und Push-down

Die Source entscheidet, wo Filter und Sortierung angewendet werden. Das ist der wichtigste Performance-Unterschied zwischen den Adaptern.

Engine → withFilters() → withSorts() → records() Die Engine setzt Sprache, Filter und Sortierung, bevor sie records() aufruft.
DatabaseSource (Eloquent, Query) Filter werden zu WHERE, Sortierung zu ORDER BY. Die Datenbank liefert bereits die fertige Menge.
ArraySource, CollectionSource, RawSqlSource Die Records werden zuerst geholt und danach in PHP gefiltert und sortiert. Bei grossen Datenmengen ist Push-down vorzuziehen.

Gruppierung braucht sortierte Daten

Die Gruppierung arbeitet run-basiert: Die Engine vergleicht aufeinanderfolgende Datensätze. Sortiere die Quelle deshalb nach denselben Schlüsseln wie die Gruppen — sonst entstehen mehrere Gruppen mit demselben Schlüssel.

Das Filterformular

filterControls() liefert eine Liste von FilterControl-Deskriptoren. Jede UI — Blade, Vue oder React — rendert damit dasselbe Formular.

FilterBuilder::for(ReportSource|iterable $fields) Startet den Builder aus einer Source oder direkt aus ReportField-Objekten.
field(string $key) Wählt ein Feld für die folgenden Anpassungen.
control(string $control) Setzt das Widget: text, number, boolean, select, multiselect, date oder picker.
label(string $label) Überschreibt das Anzeigelabel des Felds.
operators(array $operators) Schränkt die Operatoren ein oder erweitert sie. Der erste Operator wird zum Default.
options(array $options) Setzt die Auswahlwerte für Select, Multiselect oder Picker.
multiple(bool $multiple = true) Erlaubt mehrere Werte.
wildcards(bool $wildcards = true) Aktiviert Platzhalter in Textfiltern.
required(bool $required = true) Markiert das Feld als Pflichtfeld.
picker(?string $valueField = 'id', ?string $labelField = 'label', ?string $search = null) Macht aus dem Feld einen suchbaren Dual-List-Picker für IDs oder Fremdschlüssel.
text() · number() · boolean() · select() · multiselect() · date() Kurzformen für control().
only(array $keys) · except(array $keys) Behält nur bestimmte Controls oder entfernt sie aus dem Formular.
build(): array Liefert die fertige Liste von FilterControl-Objekten.

Operatoren

Operatoren werden validiert und an den passenden Applier übergeben. Nicht jeder Operator ist für jeden Typ sinnvoll.

= Gleichheit.
!= Ungleichheit.
< · <= · > · >= Grösser-/Kleiner-Vergleiche für Zahlen und Daten.
between Bereich mit zwei Werten.
in Werteliste, etwa bei Multiselect und Picker.
like Teilstring-Suche in Textfeldern.
before · after Datumsvergleiche.
is_null · is_not_null Prüft auf leere Werte.
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();
}
<?php

declare(strict_types=1);

namespace App\Reports\PriceList;

use Guggach\Reports\Sources\ReportField;
use Guggach\Reports\Sources\ReportSource;

/**
 * Data source of the price list. Report-local on purpose (spec 7.1): a source
 * is never shared between reports. Filters are applied in memory here; a
 * database-backed variant would push them down instead.
 */
final class Source extends ReportSource
{
    public function records(): iterable
    {
        return $this->applyFilters([
            ['sku' => 'A-100', 'name' => 'Kaffeemaschine', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 129.00],
            ['sku' => 'B-200', 'name' => 'Wasserkocher', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 39.90],
            ['sku' => 'C-300', 'name' => 'Toaster', 'category' => 'Büro', 'supplier' => 'Berger GmbH', 'price' => 59.50],
            ['sku' => 'D-400', 'name' => 'Mixer', 'category' => 'Büro', 'supplier' => 'Conti AG', 'price' => 89.00],
        ]);
    }

    public function fields(): array
    {
        return [
            new ReportField('sku', 'SKU', type: 'string'),
            new ReportField('name', 'Bezeichnung', type: 'string'),
            new ReportField('category', 'Kategorie', type: 'enum', options: ['Küche', 'Büro']),
            new ReportField('supplier', 'Lieferant', type: 'relation', options: [
                'Nordwind AG', 'Berger GmbH', 'Conti AG', 'Delta Handels',
                'Egger & Co', 'Fuchs AG', 'Graf GmbH', 'Huber AG',
            ]),
            new ReportField('price', 'Preis', type: 'number'),
        ];
    }
}

Das Filterformular der Preisliste und die zugehörige Source mit Feldtypen und Optionen.

Weiter · Gruppen & Aggregate Gruppen verschachteln, Kopf und Fuss definieren und Summen, Durchschnitte und Zähler berechnen.