All examples
Data sources · SQLite-backed source adapters
One report, five source adapters: QuerySource, EloquentSource, RawSqlSource, CollectionSource and ArraySource all read the same SQLite table. Pick the adapter in the filter form.
Flow
Filters
Aggregates
/reports/database_sources/run
What is typical here
- A real SQLite table with demo orders, created by a migration
- QuerySource and EloquentSource push filters and sorting into SQL
- RawSqlSource, CollectionSource and ArraySource filter and sort in memory
- The source selector is a normal typed filter control
- Report start/end bands show the active adapter and the aggregated totals
Module structure
app/Reports/DatabaseSources/
├── Report.php # switch between five source adapters
└── views/
├── report-start.blade.php
├── grid-head.blade.php
├── row.blade.php
└── report-end.blade.php
app/Models/DemoOrder.php
database/migrations/…_create_demo_orders_table.php
Files
<?php
declare(strict_types=1);
namespace App\Reports\DatabaseSources;
use App\Models\DemoOrder;
use Guggach\Reports\Aggregates\Count;
use Guggach\Reports\Aggregates\Sum;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Filters\FilterBuilder;
use Guggach\Reports\Filters\FilterControl;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ArraySource;
use Guggach\Reports\Sources\CollectionSource;
use Guggach\Reports\Sources\EloquentSource;
use Guggach\Reports\Sources\QuerySource;
use Guggach\Reports\Sources\RawSqlSource;
use Guggach\Reports\Sources\ReportField;
use Guggach\Reports\Sources\ReportSource;
use Illuminate\Support\Facades\DB;
/**
* SQLite-backed demo: the same report can be rendered through every source
* adapter. Pick the source in the generated filter form; the report start band
* shows which adapter is active.
*/
final class Report extends BaseReport
{
/** @var array<string, string> */
private const SOURCE_LABELS = [
'query' => 'QuerySource · DB::table()',
'eloquent' => 'EloquentSource · Model',
'raw_sql' => 'RawSqlSource · SQL + Bindings',
'collection' => 'CollectionSource · Collection',
'array' => 'ArraySource · Array',
];
public function key(): string
{
return 'database_sources';
}
public function name(): string
{
return 'Datenquellen (SQLite)';
}
public function layout(): string
{
return 'default';
}
public function source(): ReportSource
{
$fields = $this->dataFields();
$source = match ($this->selectedSource()) {
'eloquent' => new EloquentSource(DemoOrder::class, $fields),
'raw_sql' => new RawSqlSource('select * from demo_orders', [], $fields),
'collection' => new CollectionSource(DemoOrder::all(), $fields),
'array' => new ArraySource(DemoOrder::all()->toArray(), $fields),
default => new QuerySource(DB::table('demo_orders'), $fields),
};
return $source
->withFilters($this->filters())
->withSorts(['amount' => 'desc']);
}
/**
* @return list<FilterControl>
*/
public function filterControls(): array
{
$sourceControl = new FilterControl(
key: 'source',
label: 'Quelle',
control: 'select',
type: 'enum',
operators: ['='],
defaultOperator: '=',
options: self::SOURCE_LABELS,
required: true,
);
return [
$sourceControl,
...FilterBuilder::for($this->dataFields())->build(),
];
}
public function define(ReportBuilder $r): void
{
$r->reportStart(view: 'report-start');
$r->gridHeader(view: 'grid-head');
$r->detail(view: 'row');
$r->reportEnd(view: 'report-end');
$r->aggregate('total', Sum::class, field: 'amount', scope: 'report');
$r->aggregate('orders', Count::class, field: 'order_number', scope: 'report');
}
/**
* @return array<string, string>
*/
public static function sourceLabels(): array
{
return self::SOURCE_LABELS;
}
/**
* @return list<ReportField>
*/
private function dataFields(): array
{
return [
new ReportField('order_number', 'Auftrag'),
new ReportField('customer', 'Kunde'),
new ReportField('category', 'Kategorie', type: 'enum', options: [
'Miete' => 'Miete',
'Service' => 'Service',
'Verkauf' => 'Verkauf',
]),
new ReportField('amount', 'Betrag', type: 'number', aggregatable: true),
new ReportField('ordered_at', 'Datum', type: 'date'),
new ReportField('status', 'Status', type: 'enum', options: [
'offen' => 'offen',
'bezahlt' => 'bezahlt',
]),
];
}
private function selectedSource(): string
{
$raw = $this->filters()['source'] ?? 'query';
if (is_array($raw)) {
$raw = $raw['value'] ?? $raw[0] ?? 'query';
}
return is_string($raw) && array_key_exists($raw, self::SOURCE_LABELS) ? $raw : 'query';
}
}
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class DemoOrder extends Model
{
protected $table = 'demo_orders';
public $timestamps = false;
protected $guarded = [];
}
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('demo_orders', function (Blueprint $table): void {
$table->id();
$table->string('order_number')->unique();
$table->string('customer');
$table->string('category');
$table->decimal('amount', 10, 2);
$table->date('ordered_at');
$table->string('status');
});
DB::table('demo_orders')->insert([
['order_number' => 'AU-2026-1001', 'customer' => 'Alpine Studio GmbH', 'category' => 'Miete', 'amount' => 2400.00, 'ordered_at' => '2026-09-18', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1002', 'customer' => 'BA Contemporary Dance', 'category' => 'Miete', 'amount' => 2133.35, 'ordered_at' => '2026-09-18', 'status' => 'offen'],
['order_number' => 'AU-2026-1003', 'customer' => 'Müller Informatik AG', 'category' => 'Service', 'amount' => 860.00, 'ordered_at' => '2026-09-22', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1004', 'customer' => 'Seeblick Immobilien', 'category' => 'Miete', 'amount' => 3200.00, 'ordered_at' => '2026-09-28', 'status' => 'offen'],
['order_number' => 'AU-2026-1005', 'customer' => 'Nordlicht GmbH', 'category' => 'Verkauf', 'amount' => 12500.00, 'ordered_at' => '2026-10-02', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1006', 'customer' => 'Kunsthaus West', 'category' => 'Service', 'amount' => 1450.00, 'ordered_at' => '2026-10-05', 'status' => 'offen'],
['order_number' => 'AU-2026-1007', 'customer' => 'Rheinblick AG', 'category' => 'Miete', 'amount' => 1800.00, 'ordered_at' => '2026-10-08', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1008', 'customer' => 'Studio Nord', 'category' => 'Verkauf', 'amount' => 7600.00, 'ordered_at' => '2026-10-12', 'status' => 'offen'],
['order_number' => 'AU-2026-1009', 'customer' => 'Atelier Blau', 'category' => 'Service', 'amount' => 980.00, 'ordered_at' => '2026-10-17', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1010', 'customer' => 'Hofmann Consulting', 'category' => 'Miete', 'amount' => 2700.00, 'ordered_at' => '2026-10-21', 'status' => 'offen'],
['order_number' => 'AU-2026-1011', 'customer' => 'Kreativwerkstatt AG', 'category' => 'Service', 'amount' => 640.00, 'ordered_at' => '2026-10-24', 'status' => 'bezahlt'],
['order_number' => 'AU-2026-1012', 'customer' => 'Zürich Design Lab', 'category' => 'Verkauf', 'amount' => 9400.00, 'ordered_at' => '2026-10-29', 'status' => 'offen'],
]);
}
public function down(): void
{
Schema::dropIfExists('demo_orders');
}
};
@php
$selected = $ctx->filters['source'] ?? 'query';
if (is_array($selected)) {
$selected = $selected['value'] ?? $selected[0] ?? 'query';
}
$labels = \App\Reports\DatabaseSources\Report::sourceLabels();
@endphp
<style>
.db-demo-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8mm;
border-bottom: 3px solid #0f766e;
padding-bottom: 4mm;
margin-bottom: 5mm;
}
.db-demo-head h1 { margin: 0 0 1mm; font-size: 22px; color: #0f172a; }
.db-demo-head p { margin: 0; font-size: 10px; color: #64748b; }
.db-source-badge {
padding: 2mm 3mm;
border-radius: 2mm;
background: #ccfbf1;
color: #0f766e;
font-size: 9px;
font-weight: 700;
white-space: nowrap;
}
.db-source-hint {
margin-bottom: 5mm;
padding: 2.5mm 3mm;
background: #f8fafc;
border-left: 3px solid #0f766e;
font-size: 9px;
color: #475569;
}
.db-grid-head,
.db-row {
display: flex;
align-items: center;
gap: 3mm;
box-sizing: border-box;
}
.db-grid-head {
height: 9mm;
border-bottom: 2px solid #0f766e;
font-size: 8px;
font-weight: 700;
color: #0f172a;
}
.db-row {
min-height: 9mm;
border-bottom: 1px solid #e2e8f0;
font-size: 9px;
color: #334155;
}
.db-col-order { flex: 0 0 30mm; font-weight: 600; color: #0f172a; }
.db-col-customer { flex: 1 1 auto; }
.db-col-category { flex: 0 0 22mm; }
.db-col-date { flex: 0 0 22mm; color: #64748b; }
.db-col-status { flex: 0 0 20mm; }
.db-col-amount { flex: 0 0 24mm; text-align: right; font-variant-numeric: tabular-nums; }
.db-status {
display: inline-block;
padding: 0.8mm 1.6mm;
border-radius: 1mm;
font-size: 8px;
font-weight: 600;
}
.db-status--paid { background: #dcfce7; color: #166534; }
.db-status--open { background: #fef3c7; color: #92400e; }
.db-summary {
display: flex;
justify-content: flex-end;
gap: 10mm;
margin-top: 4mm;
padding-top: 3mm;
border-top: 2px solid #0f172a;
font-size: 11px;
font-weight: 700;
}
</style>
<div class="db-demo-head">
<div>
<h1>Datenquellen</h1>
<p>Dieselben SQLite-Daten, gerendert durch den gewählten Source-Adapter.</p>
</div>
<div class="db-source-badge">{{ $labels[$selected] ?? $selected }}</div>
</div>
<div class="db-source-hint">
Wähle im Filterformular eine andere Quelle. Alle fünf Adapter lesen dieselbe Tabelle
<code>demo_orders</code>; Query- und Eloquent-Source filtern und sortieren per SQL,
die übrigen Adapter in-memory.
</div>
<div class="db-grid-head">
<span class="db-col-order">Auftrag</span>
<span class="db-col-customer">Kunde</span>
<span class="db-col-category">Kategorie</span>
<span class="db-col-date">Datum</span>
<span class="db-col-status">Status</span>
<span class="db-col-amount">Betrag</span>
</div>
<div class="db-row">
<span class="db-col-order">{{ $ctx->order_number }}</span>
<span class="db-col-customer">{{ $ctx->customer }}</span>
<span class="db-col-category">{{ $ctx->category }}</span>
<span class="db-col-date">{{ \Illuminate\Support\Carbon::parse($ctx->ordered_at)->format('d.m.Y') }}</span>
<span class="db-col-status">
<span class="db-status {{ $ctx->status === 'bezahlt' ? 'db-status--paid' : 'db-status--open' }}">
{{ $ctx->status }}
</span>
</span>
<span class="db-col-amount">CHF {{ number_format((float) $ctx->amount, 2, '.', "'") }}</span>
</div>
<div class="db-summary">
<span>{{ $ctx->aggregates['orders'] }} Aufträge</span>
<span>Total CHF {{ number_format((float) $ctx->aggregates['total'], 2, '.', "'") }}</span>
</div>