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.