Filter form

The package builds a filter form from a source’s fields. Input is validated, passed to the preview as query parameters and applied by the source as a WHERE clause or an in-memory filter.

How the form is built

Without a custom filterControls(), one control is created per filterable ReportField. The reports.form route renders it; “Continue” sends the values to reports.preview.

ReportSource::fields() ReportField[] is the allow-list. Only filterable fields appear in the form.
Report::filterControls() Optional override. Default: FilterBuilder::for($this->source())->build().
Reports::filterSchema($report) Serialised control list for your own UI (Blade, Vue, React).
GET /reports/{name} Renders the form. Without controls, the route redirects to /run.

No filters? No form.

If every field has filterable: false or the source has no fields, filterControls() is empty. The form route then redirects straight to the preview. That is not an error — it is the intended one-click case.

Try it live

This is the real, automatically generated price list form. “Continue” validates the values and opens the preview.

The iframe shows the form route. After submitting, it navigates to the preview inside the iframe; “Open preview” starts it in a new tab.

The filter fields

The field type determines the widget, operators and query format. FilterBuilder can override all three per field.

ReportField(type: 'string') Text field. Operators: like, =, !=. Query: ?name=Kaffee or ?name[operator]=like&name[value]=Kaffee. Use multiple() for several rows.
'number' · 'int' · 'integer' · 'decimal' · 'float' Number field with an operator select. Operators: =, !=, <, <=, >, >=, between, in. Query: ?price[operator]=between&price[value][]=50&price[value][]=100.
'date' · 'datetime' Date field with =, before, after, between, is_null. Query with an ISO date: ?released_at[operator]=after&released_at[value][]=2026-01-01.
'boolean' · 'bool' Yes/no select. Query: ?active=1 (yes) or ?active=0 (no).
'enum' with up to 6 options Simple select. Query: ?category=Küche. Internally the in operator is used.
'enum' with more than 6 options or ->multiselect() Collapsible checkbox list. Query: ?category[]=Küche&category[]=Büro.
'id' · 'fk' · 'relation' Searchable dual-list picker. Query: ?supplier[]=1&supplier[]=2.
'json' Text control without a value for is_null and is_not_null.

Filters as query parameters

The form submits with GET. You can build the same URLs yourself — for deep links, emails or a custom form.

# text (default operator: like)
/reports/price_list/run?name=Kaffee

# text with an explicit operator
/reports/price_list/run?name[operator]=like&name[value]=Kaffee

# number: greater or equal
/reports/price_list/run?price[operator]=>=&price[value][]=50

# number: between
/reports/price_list/run?price[operator]=between&price[value][]=50&price[value][]=100

# enum / multiselect / picker
/reports/price_list/run?category=K%C3%BCche
/reports/price_list/run?category[]=K%C3%BCche&category[]=B%C3%BCro
/reports/price_list/run?supplier[]=1&supplier[]=2

The query formats produced by the automatic form.

How the filters are applied

The FilterValidator sits between the form and the report. It only lets known controls through and normalises the operator and value.

FilterValidator Allow-list from filterControls(): unknown keys are dropped, values are normalised per operator (scalar, between array, in list, like pattern).
FilterBag The normalised result is passed to the source via withFilters().
DatabaseSource Eloquent and the query builder translate filters into WHERE and sorting into ORDER BY.
ArraySource · CollectionSource · RawSqlSource These sources filter in PHP after loading; they use the same operators.
$ctx->filters The active filters inside a band. Through the package routes the values are normalised: ["operator" => "in", "value" => ["Küche"]].

Direct code calls such as Reports::html($report, $filters) bypass the FilterValidator. There only the source’s field allow-list and the default operator apply; validate your own input first.

Building your own form

You can point your own form at the package routes or write your own controller. The form only needs the right field names and the right action.

<form method="GET" action="{{ route('reports.preview', ['name' => 'price_list']) }}" target="_blank">
    <select name="category">
        <option value="">Alle Kategorien</option>
        <option value="Küche">Küche</option>
        <option value="Büro">Büro</option>
    </select>

    <input type="text" name="name" placeholder="Bezeichnung enthält …">

    <select name="price[operator]">
        <option value="=">=</option>
        <option value=">=">≥</option>
        <option value="<=">≤</option>
        <option value="between">zwischen</option>
    </select>
    <input type="number" step="any" name="price[value][]" placeholder="von">
    <input type="number" step="any" name="price[value][]" placeholder="bis">

    <button type="submit">Vorschau öffnen</button>
</form>
use Guggach\Reports\Facades\Reports;
use Guggach\Reports\Filters\FilterValidator;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

final class PriceListController
{
    public function __invoke(Request $request): Response
    {
        $report = Reports::find('price_list');

        // Same allow-list and operator normalisation as the package routes.
        $filters = (new FilterValidator($report->filterControls()))
            ->validate($request->query())
            ->all();

        return response(Reports::html($report, $filters));
    }
}

The same form as Blade markup and as a custom controller.

Customising the automatic form

Override filterControls() in the report class and adjust individual fields with the FilterBuilder. Everything else stays automatic.

->field('price') Selects a field for the following customisations.
->control('number') Overrides the widget: text, number, boolean, select, multiselect, date or picker.
->operators([...]) Restricts or extends the operators; the first one becomes the default.
->options([...]) Values for select, multiselect or picker.
->multiple() Allows multiple values.
->picker(...) Turns an ID/FK field into a searchable picker.
->only([...]) · ->except([...]) Keep or remove specific controls.
->build() Returns the finished list of FilterControl objects.
use Guggach\Reports\Filters\FilterBuilder;

public function filterControls(): array
{
    return FilterBuilder::for($this->source())
        ->field('price')->operators(['=', '>=', '<=', 'between'])
        ->field('category')->multiselect()
        ->field('supplier')->picker(valueField: 'id', labelField: 'label')
        ->build();
}

Customise three fields and let the rest be generated automatically.

The full reference for FilterBuilder, operators and field types is on the Data sources page. For a completely custom form you only need filterControls() if you want to keep using the package routes or the FilterValidator.