Package · mui-data-grid

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.

Namespace Flytachi\Winter\MuiDataGridPackage flytachi/winter-mui-data-grid

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

php
public static function wrap(
  RepositoryViewInterface $repo,
  MuiGridRequest $request,
  GridSchema $schema,
  ?callable $mapper = null,
): MuiGridResponse

Parameters

$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

php
$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

json
{ "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

php
public static function make(GridColumn ...$columns): self

Parameters

$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

php
$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

php
public function defaultOrder(string $order): self

Parameters

$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

php
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

php
public function textMatch(TextMatch $mode): self

Parameters

$mode — one of the TextMatch modes: Auto, ILike, Like or Lower.

Returns

GridSchema — the same schema, for chaining.

Example

php
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

php
public function textMatchMode(): TextMatch

Returns

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

php
public function buildWhere(MGFilterModel $model, ?TextMatch $textMatch = null): Qb

Parameters

$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

php
$where = $schema->buildWhere($request->filterModel, TextMatch::ILike);

echo $where->getQuery();

Result

sql
(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

php
public function buildOrder(array $sortModel): string

Parameters

$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

php
echo $schema->buildOrder([new MGSortItem('title', 'desc')]);

Result

sql
lower(a.title) DESC

GridColumn

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

php
public static function for(string $field, string $sql): self

Parameters

$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

php
public function filterable(FilterType $type): self

Parameters

$typeFilterType::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

php
GridColumn::for('views', 'a.views')->filterable(FilterType::Number);
// accepts =, !=, >, >=, <, <=, isAnyOf, isEmpty, isNotEmpty
// rejects contains, startsWith, … → HTTP 400

GridColumn::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

php
public function sortable(?string $sqlExpr = null): self

Parameters

$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

php
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

php
public function textMatch(TextMatch $mode): self

Parameters

$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

php
public function filterUsing(callable $resolver): self

Parameters

$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

php
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

php
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

php
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

php
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

php
return ResponseEntity::ok($response->toArray());
// or, since the class is JsonSerializable:
return ResponseEntity::ok($response);

Result

json
{ "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 }.

php
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 }.

php
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 }.

php
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.

php
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

json
{ "message": "Operator 'contains' is not allowed for field 'views'" }

Next steps