Report definition
A report is a PHP class plus a fluent builder. The class provides data and hooks; the builder declares bands, groups, filters and aggregates.
The report class
Every report extends Guggach\Reports\Report. Four methods are required; everything else is an optional hook for layout, page setup, PDF and filters.
| key(): string | Stable report key, e.g. price_list. It addresses routes, views and the translation namespace. |
|---|---|
| name(): string | Display name used for the page title and the PDF. |
| source(): ReportSource | Returns the data source. Sources are report-local and never shared between reports. |
| define(ReportBuilder $r): void | Declares bands, groups, filters and aggregates. This is where the report definition is built. |
| layout(): string | Layout class or Blade view for the outer chrome. Default: default. |
| pageSetup(): ?PageSetup | Optional report override for paper, orientation and margins. It wins over layout and config. |
| pdfHeader(): ?string · pdfFooter(): ?string | HTML templates for the PDF header and footer. Chromium replaces .pageNumber, .totalPages and .title; the HTML preview stays unchanged. |
| filterControls(): array | Describes the filter form as a list of FilterControl objects. Without an override, one control is generated per filterable source field. |
| withFilters(array $filters): static · filters(): array | Sets and reads the runtime filters. The engine calls withFilters() before rendering. |
| definition(): ReportDefinition | Builds the immutable ReportDefinition through the builder. Used by the engine and the preview. |
| viewNamespace(): string · viewsPath(): ?string | Blade namespace and directory of the module views. The namespace defaults to key() with underscores. |
viewNamespace() and viewsPath() derive the module views from key() and the class directory. Override them only when your views deliberately live elsewhere.
<?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');
});
}
}
The complete price list report class — including layout, filter form and group.
The ReportBuilder
The builder is the fluent interface inside define(). Every method returns the builder so bands and groups can be chained.
| mode(ReportMode $mode) | Flow or Strict. Overrides reports.default_mode for this report. |
|---|---|
| pageSetup(PageSetup $setup) | Paper size, orientation, unit, margins and DPI for this report. |
| layout(string $layout) | Layout class or view name. A layout() on the report class takes precedence. |
| detail($view = null, ?Closure $closure = null, ?float $height = null) | The central band: runs once per record. Accepts a view name, a closure or a prepared Detail instance. |
| freeform($view = null, ?Closure $closure = null, ?float $height = null) | Free-form report: one detail band contains the complete form and automatically starts a form scope. No groups and no N-up. |
| pageHeader($view = null, ?Closure $closure = null) | Page header. Repeats on every page in Strict mode; typically combined with hideOnFirstPage(). |
| pageFooter($view = null, ?Closure $closure = null) | Page footer, for example for page numbers. |
| reportStart($view = null, ?Closure $closure = null) | Band at the start of the report, exactly once. Ideal for the title and header data. |
| reportEnd($view = null, ?Closure $closure = null) | Band at the end of the report, exactly once. The report aggregates are available here. |
| gridHeader($view = null, ?Closure $closure = null) | Table header. Repeated on every page in Strict mode. Alternative: detail()->repeatGridHeader(). |
| pageNumbering(string $mode) | continuous or perForm. perForm restarts the page counter for every form instance; typical for invoices. |
| filter(array $filter) | Low-level filter definition. For forms, filterControls() on the report class is the usual route. |
| group(string|array $key, ?Closure $callback = null) | Defines a group and returns a GroupBuilder. An array creates a composite key. |
| aggregate(string $name, string $class, ?string $field = null, string $scope = 'report') | Report aggregate with name, aggregate class and optional field. The scope is accepted; in v1 report aggregates are report-wide. |
| build(): ReportDefinition | Creates the definition. Called by Report::definition() and normally not used directly. |
Bands and their options
Every band method returns a band object. These options apply to all bands; special cases add their own methods.
| view(?string $view) · closure(?Closure $closure) | Render source of the band. A closure receives the RenderContext; a view name resolves relative to the module. |
|---|---|
| height(?float $height) · minHeight(?float $minHeight) | Fixed height (required in Strict) or minimum height (Flow), in the PageSetup unit. |
| keepTogether(bool $keepTogether = true) | Prevents a break inside the band. Enabled by default on the detail band. |
| breakBefore(bool $on = true) · breakAfter(bool $on = true) | Forces a page break before or after the band. |
| repeatOnNewPage(bool $repeat = true) | Repeats the band after a page break, for example a table header. |
| hideOnFirstPage() · hideOnLastPage() · onlyOddPages() · onlyEvenPages() | Band visibility. Several restrictions can be combined. |
| pages(array $pages) | Explicit page list, e.g. [1] for a band only on the first page. |
| formScope(bool $on = true) · restartPageNumber(bool $on = true) | Marks the start of a form instance and restarts the page counter per instance. |
| localeUsing(Closure|string|null $resolver) | Locale resolver for a band. The API exists, but the v1 engine does not evaluate it yet. |
Extra options on the detail band
The detail band is the only band with column logic (N-up) and a built-in table header.
| repeatGridHeader(?string $view = null, ?Closure $closure = null) | Attaches a table header directly to the detail band; it repeats on every new page. |
|---|---|
| columns(int $columns, float $gap = 0.0, float $rowGap = 0.0) | Splits the full-width band into N cells per row. height now means row height; gap is horizontal, rowGap vertical. |
| fill(string $order) | Cell fill order. Only row is implemented in v1; other values throw an InvalidArgumentException. |
| pad(bool $pad = true) | Pads the last row with empty cells so the column grid stays aligned. |
<?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 mode with fixed heights and N-up with four labels per row.
Setting up the page
PageSetup describes the paper and its usable area. The engine derives the Strict-mode capacity from it.
| PageSetup::a4() | A4 portrait with the default margins 20/15/20/15 mm and unit mm. |
|---|---|
| PageSetup::fromConfig(mixed $config) | Builds the setup from reports.paper. Used internally for the config level of the cascade. |
| portrait() · landscape() | Changes the orientation and returns a new instance. |
| marginsMm(int $top, int $right, int $bottom, int $left) | Sets all four margins in millimetres. |
| dimensions(): array | Returns width and height in mm for the selected size and orientation. Supports a3, a4, a5, letter and legal. |
The cascade is config (reports.paper) → layout (Layout::pageSetup()) → builder ($r->pageSetup()) → report (Report::pageSetup()). In v1 the highest set level wins as a whole; a field-by-field merge is not implemented yet.
What the views receive
Every band receives a RenderContext. It is the only hand-off between the engine and Blade — and works the same for views and closures.
Properties
| $ctx->report | The report instance, e.g. for $ctx->report->name(). |
|---|---|
| $ctx->definition | The resolved ReportDefinition with bands, groups and mode. |
| $ctx->record | The current record. Works the same for arrays, models and objects. |
| $ctx->index | Zero-based index of the record within the whole report. |
| $ctx->total | Number of records in the report (not pages). |
| $ctx->filters | The active filter values of the run. |
| $ctx->locale | Active locale of the run, or null. |
| $ctx->groups | The currently open groups as a list of level, keys and key. |
| $ctx->aggregates | Aggregate values by name. See the Groups & aggregates page for details. |
| $ctx->page | Page metadata in strict free-form streaming: number, total, local, formTotal, global. In Flow and Strict use the {page}/{pages} placeholders. |
| $ctx->pageCursor | PageCursor for strict free-form blocks; null elsewhere. |
Methods
| $ctx->field(string $key, mixed $default = null) | Reads a field from the record. The same access works directly as $ctx->fieldname. |
|---|---|
| $ctx->isFirst() | True for the first record of the report. |
| $ctx->isLast() | True for the last record of the report. |
| $ctx->currentGroup() | The innermost open group as an array with level, keys and key — or null. |
Shortcut: in Blade, $ctx->price is enough instead of $ctx->field('price'). The magic only kicks in when no real property or method of that name exists.
<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>
Three views from the price list: detail row, group footer with an aggregate and report end with the total count.