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.

Next · Data sources Where the records come from, which field types exist and how filters reach the query.