API reference
Every public class and method in the package — signatures, parameters, return values and
errors. The FilterType, MGOperator and TextMatch enums have pages of their own:
Operators and
Match modes.
MuiGrid
The library’s entry point. A final, stateless class whose single public method is static.
MuiGrid::wrap()
Layers the request’s filters, sorting and pagination onto a finished repository.
The method runs four steps. First the text-match mode is resolved: if the schema is left at
TextMatch::Auto, the mode comes from the database driver the repository talks to. Then the
schema builds a WHERE condition from the filter model and adds it to the repository with
andWhere(), leaving the base query untouched. Next an ORDER BY is built from the sort
model and set with orderBy(). Finally the page and the total are obtained from the
framework’s paginator, which issues two queries: SELECT … LIMIT … OFFSET … and a separate
COUNT.
Syntax
public static function wrap(
RepositoryViewInterface $repo,
MuiGridRequest $request,
GridSchema $schema,
?callable $mapper = null,
): MuiGridResponseParameters
$repo — a repository with SELECT, JOINs and the base WHERE already applied. The
method mutates this object: it adds conditions, sets the order and the page bounds. Clone it
beforehand if you need it afterwards.
$request — the parsed grid request: page, page size, sort model and filter model. Usually
a MuiGridRequest subclass carrying your own extra fields.
$schema — the column whitelist and fallback order. Decides what from the browser is
allowed into the query.
$mapper — an optional row transformer shaped fn (object $row): mixed. Applied to the
rows of the current page after the query runs.
Returns
MuiGridResponse — an object with rowCount (total rows matching the filter) and rows
(the current page).
Errors
MuiGridException (HTTP 400) — the field is absent from the schema, not enabled for
filtering, or the operator is not allowed for the column’s type.
Example
$repo = ArticleRepository::instance('a')
->select('a.id, a.title, a.views')
->where(Qb::eq('a.is_published', true));
$response = MuiGrid::wrap($repo, $request, $schema, fn ($r) => ArticleRow::from($r));
return ResponseEntity::ok($response->toArray());Result
{ "rowCount": 134, "rows": [ { "id": 12, "title": "Winter internals" } ] }GridSchema
A set of columns plus a fallback ORDER BY. It holds no per-request state — build it once
and reuse it.
GridSchema::make()
Creates a schema from a list of columns.
Columns are keyed by field name. If one name is declared twice, the last occurrence wins.
Syntax
public static function make(GridColumn ...$columns): selfParameters
$columns — a variadic list of columns. Declare only the ones the client may actually use:
a column enabled for neither filtering nor sorting does nothing.
Returns
GridSchema — a new schema.
Example
$schema = GridSchema::make(
GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable(),
GridColumn::for('views', 'a.views')->filterable(FilterType::Number)->sortable(),
);GridSchema::defaultOrder()
Sets the ORDER BY used when the request carries no usable sort.
It applies in two cases: the sort model is empty, or only unknown and non-sortable fields
remain in it. Without this call an unsorted request reaches the database with no ORDER BY
at all, leaving row order undefined — and pages therefore unstable.
Syntax
public function defaultOrder(string $order): selfParameters
$order — a ready ORDER BY expression without the keyword. This is your SQL: it is
neither validated nor escaped. Include a unique tiebreaker (usually the primary key), or rows
sharing sort values will shuffle between pages.
Returns
GridSchema — the same schema, for chaining.
Example
GridSchema::make(/* … */)->defaultOrder('a.created_at DESC, a.id DESC');GridSchema::textMatch()
Pins how case-insensitive matching is spelled for the whole schema.
By default the schema sits at TextMatch::Auto and MuiGrid::wrap() resolves the spelling
from the repository’s driver. An explicit mode cancels that detection.
Syntax
public function textMatch(TextMatch $mode): selfParameters
$mode — one of the TextMatch modes: Auto, ILike, Like or Lower.
Returns
GridSchema — the same schema, for chaining.
Example
GridSchema::make(/* … */)->textMatch(TextMatch::Lower);GridSchema::textMatchMode()
Returns the mode the schema was configured with — possibly still Auto.
Useful when you need the mode before it is resolved: to apply the same dialect to a domain filter, for instance.
Syntax
public function textMatchMode(): TextMatchReturns
TextMatch — the schema’s mode, with Auto left unresolved.
GridSchema::buildWhere()
Builds the WHERE condition from a filter model.
Each item is looked up in the schema by field name; an unknown field, or one not declared
filterable, stops processing with an exception. Items that produce an empty condition (an
isAnyOf with an empty set, say) are skipped. The result is joined with AND, or — when
logicOperator is or — with a parenthesized OR.
MuiGrid::wrap() normally calls this for you; call it directly when you need the condition
apart from pagination.
Syntax
public function buildWhere(MGFilterModel $model, ?TextMatch $textMatch = null): QbParameters
$model — the filter model from the request.
$textMatch — the resolved match mode. null means “use the schema’s own”; if that is
still Auto there is no repository here to detect from, so the portable TextMatch::Lower
applies.
Returns
Qb — the condition, or Qb::empty() when nothing effective remains.
Errors
MuiGridException (HTTP 400) — unknown field, field not enabled for filtering, or an
operator outside the FilterType set.
Example
$where = $schema->buildWhere($request->filterModel, TextMatch::ILike);
echo $where->getQuery();Result
(a.title ILIKE :iqb0 AND a.views >= :iqb1)GridSchema::buildOrder()
Builds the ORDER BY expression from a sort model.
Fields are processed in the order the browser sent them. An unknown or non-sortable field is
silently skipped — the table routinely emits transient sort state, and failing on it would be
hostile to the UI. When no usable field remains, defaultOrder() is returned.
Syntax
public function buildOrder(array $sortModel): stringParameters
$sortModel — an array of MGSortItem from the request.
Returns
string — an ORDER BY expression without the keyword, or an empty string when there is
neither a usable sort nor a fallback order.
Example
echo $schema->buildOrder([new MGSortItem('title', 'desc')]);Result
lower(a.title) DESCGridColumn
The declaration of one column: the browser’s field name, an SQL expression and the permitted operations. Built through a fluent interface.
GridColumn::for()
Creates a column, mapping a field name to an SQL expression.
A fresh column can do nothing — neither filter nor sort. Capabilities are enabled explicitly, by separate calls.
Syntax
public static function for(string $field, string $sql): selfParameters
$field — the field name the browser sends. Must match the field prop of the MUI DataGrid
column.
$sql — the SQL expression it maps to. Inserted into the query verbatim, so it must come
from your code: a column (a.title), a joined column (au.name) or any expression
(coalesce(a.nick, a.name)).
Returns
GridColumn — a new column.
GridColumn::filterable()
Allows filtering on the column and sets its value type.
The type acts as a gate: an operator outside this FilterType set is rejected before it
reaches SQL. The sets are listed in Operators.
Syntax
public function filterable(FilterType $type): selfParameters
$type — FilterType::String, Number, Boolean or Date. Choose by how the value
behaves in SQL rather than by its PHP type: a status column compared only for equality is a
String, and a timestamp is a Date.
Returns
GridColumn — the same column, for chaining.
Example
GridColumn::for('views', 'a.views')->filterable(FilterType::Number);
// accepts =, !=, >, >=, <, <=, isAnyOf, isEmpty, isNotEmpty
// rejects contains, startsWith, … → HTTP 400GridColumn::sortable()
Allows sorting on the column, optionally by a different expression.
The sort expression is independent of the filter expression: a column can be searched on
a.title and ordered by lower(a.title).
Syntax
public function sortable(?string $sqlExpr = null): selfParameters
$sqlExpr — an optional ORDER BY expression. null means “order by the column’s own
expression”. Like that one, this is trusted SQL: neither validated nor escaped, so never
assemble it from request data.
Returns
GridColumn — the same column, for chaining.
Example
GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable('lower(a.title)');GridColumn::textMatch()
Pins the text-match mode for this column, overriding the schema’s.
Use it when one column needs different behavior — lower() folding for a non-ASCII name,
say — while the rest of the grid is happy with the connection’s default. TextMatch::Auto
means “inherit from the schema”, the same as not calling this at all.
Syntax
public function textMatch(TextMatch $mode): selfParameters
$mode — the mode for this column.
Returns
GridColumn — the same column, for chaining.
GridColumn::filterUsing()
Overrides the operator-to-SQL mapping for this column.
The resolver runs after the type check and before the standard mapping. Returning a
Qb takes over; returning null hands control back to the standard behavior for that
operator — which is how you write only the exceptions.
Syntax
public function filterUsing(callable $resolver): selfParameters
$resolver — a callable shaped fn (MGFilterItem $item, TextMatch $mode): ?Qb. The second
argument is the already-resolved match mode; declaring it is optional, and one-parameter
resolvers keep working.
Returns
GridColumn — the same column, for chaining.
Example
GridColumn::for('authorName', 'au.name')
->filterable(FilterType::String)
->filterUsing(fn (MGFilterItem $i): ?Qb => match ($i->operator) {
MGOperator::IS_EMPTY => Qb::isNull('a.author_id'),
MGOperator::IS_NOT_EMPTY => Qb::isNotNull('a.author_id'),
default => null,
});Other GridColumn methods
| Method | Returns | What it does |
|---|---|---|
isFilterable() |
bool |
whether filterable() was declared |
isSortable() |
bool |
whether sortable() was declared |
orderExpr() |
string |
the ORDER BY expression — custom or the base one |
resolveFilter(MGFilterItem $item, TextMatch $mode) |
?Qb |
builds the condition for a filter item; called by the schema |
Public properties: $field and $sql, both read-only.
MuiGridRequest
The grid request envelope. Hydrated by Winter’s request layer from the JSON body; there are no arrays to parse by hand.
Syntax
class MuiGridRequest
{
public function __construct(
#[Min(0)] public readonly int $page = 0,
#[Min(1), Max(10000)] public readonly int $pageSize = 20,
#[ListOf(MGSortItem::class)] public readonly array $sortModel = [],
#[Valid] public readonly MGFilterModel $filterModel = new MGFilterModel(),
) {
}
public function offset(): int; // $page * $pageSize
}Properties
$page — zero-based page index. A negative value is rejected by validation.
$pageSize — rows per page. Between 1 and 10,000; the upper bound guards against a
“give me everything” request disguised as pagination.
$sortModel — a list of MGSortItem, hydrated via #[ListOf].
$filterModel — a nested MGFilterModel, validated via #[Valid]. The property is
non-nullable with an empty default: an omitted filterModel becomes an empty model,
while an explicit "filterModel": null is a 400.
Extending it
PHP constructor parameters are not inherited, so a subclass redeclares the envelope and
forwards it to parent::__construct(). The #[ListOf] and #[Valid] attributes on the
redeclared parameters are mandatory — without them the hydrator won’t build the nested types.
Declare filterModel as MGFilterModel $filterModel = new MGFilterModel(), not
?MGFilterModel = null: passing null up to a non-nullable parent is a TypeError.
The base class marks each property readonly individually rather than the whole class — precisely so a subclass can normalize its own fields in the constructor body.
Example
class ArticleGridRequest extends MuiGridRequest
{
public function __construct(
#[Positive] public ?int $authorId = null,
int $page = 0,
int $pageSize = 20,
#[ListOf(MGSortItem::class)] array $sortModel = [],
#[Valid] MGFilterModel $filterModel = new MGFilterModel(),
) {
parent::__construct($page, $pageSize, $sortModel, $filterModel);
}
}MuiGridResponse
What MuiGrid::wrap() returns. Final, readonly, implements JsonSerializable.
Syntax
final readonly class MuiGridResponse implements JsonSerializable
{
public int $rowCount; // total rows matching the filter, ignoring the page
public array $rows; // the current page, already mapped
public function toArray(): array;
}Properties
$rowCount — how many rows match the filter in total. This is what the table’s pager needs;
the page size plays no part in it.
$rows — the rows of the current page. If a mapper was passed to wrap(), they are already
transformed.
Example
return ResponseEntity::ok($response->toArray());
// or, since the class is JsonSerializable:
return ResponseEntity::ok($response);Result
{ "rowCount": 134, "rows": [ /* … */ ] }Request DTOs
Three small readonly classes make up the request. You normally meet them only inside a
filterUsing() resolver.
MGSortItem
One sort instruction — { field, sort }.
readonly class MGSortItem
{
public string $field; // the browser's field name
public string $sort; // 'asc' | 'desc' — validated with #[In]
public function isDesc(): bool;
}The direction is validated during hydration and accepted in any case; a missing direction means ascending.
MGFilterItem
One filter instruction — { field, operator, value }.
readonly class MGFilterItem
{
public string $field; // the browser's field name
public MGOperator $operator; // an enum, cast from the request string
public mixed $value; // the value as the client sent it
}operator is cast to the enum during hydration: an unknown operator string is rejected
before the request ever reaches the schema.
MGFilterModel
The filter model — { logicOperator, items }.
readonly class MGFilterModel
{
/** @var MGFilterItem[] */
public array $items; // #[ListOf(MGFilterItem::class)]
public string $logicOperator; // 'and' | 'or' — validated with #[In], default 'and'
public function isOr(): bool;
}An empty items contributes nothing to the query — your base WHERE is left alone.
MuiGridException
The library’s exception. It extends the kernel’s RequestException, so the router answers
400 and passes the message to the client.
class MuiGridException extends RequestException {}It is thrown in three cases: the field is not in the schema, the field is declared but not
enabled for filtering, or the operator is outside the column’s FilterType set. It is also
the right thing to throw from your own resolver when a filter value won’t do.
Example
{ "message": "Operator 'contains' is not allowed for field 'views'" }Next steps
- Operators —
MGOperator,FilterTypeand the matrix between them. - Match modes — the
TextMatchenum. - Upgrading to 3.0 — what changed in the signatures.