flytachi / winter-mui-data-grid
Server-side MUI DataGrid adapter for Winter Framework (K2) — declarative schema: pagination, filtering, sorting and column whitelisting out of the box.
Requires
- php: >=8.4
- flytachi/winter-kernel: ^3.0
Requires (Dev)
- squizlabs/php_codesniffer: @stable
README
Server-side adapter that connects MUI X DataGrid to the Winter Framework (K2).
You declare a column schema — which fields may be filtered and sorted, and how
they map to SQL. The library takes the grid's request (page, sort model, filter
model), enforces that schema as a whitelist, builds a parameterized query on top
of your repository, and returns a { rowCount, rows } payload the DataGrid
understands. SQL-injection safe by construction.
$schema = GridSchema::make( GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable(), GridColumn::for('views', 'a.views')->filterable(FilterType::Number)->sortable(), GridColumn::for('authorName', 'au.name')->filterable(FilterType::String)->sortable(), )->defaultOrder('a.created_at DESC'); return MuiGrid::wrap($repo, $request, $schema)->toArray();
Requirements
- PHP >= 8.4
- flytachi/winter-kernel ^3.0
Installation
composer require flytachi/winter-mui-data-grid
How it works (60 seconds)
A grid request has three moving parts: pagination, sorting and filtering. This library handles all three around a query you build yourself:
┌─────────────────────── your code ───────────────────────┐
│ build repo: SELECT + JOINs + base WHERE │
│ declare GridSchema: field → SQL, filter type, sortable │
└──────────────────────────┬──────────────────────────────┘
│ MuiGrid::wrap(repo, request, schema, mapper?)
┌──────────────────────────▼──────────────────────────────┐
│ library: │
│ • validate filters against the schema (whitelist) │
│ • append WHERE (filters) + ORDER BY (sort) to your repo │
│ • run COUNT + paginated SELECT │
│ • map each row (optional) → MuiGridResponse{rowCount,rows}│
└─────────────────────────────────────────────────────────┘
The schema is the contract: a field the frontend references that you did not
declare is rejected (for filtering) or ignored (for sorting). Operators are gated
by each column's FilterType, so a text operator can't be sent against a numeric
column.
Quick start
A minimal "list articles" endpoint. Two tables: articles a joined to authors au.
1. Request
Extend MuiGridRequest. The grid envelope (page, pageSize, sortModel,
filterModel) is hydrated for you by the K2 request layer — add only your own
domain filters:
use Flytachi\Winter\K2\Http\Request\Validation\ListOf; use Flytachi\Winter\K2\Http\Request\Validation\Valid; use Flytachi\Winter\MuiDataGrid\Entity\MGFilterModel; use Flytachi\Winter\MuiDataGrid\Entity\MGSortItem; use Flytachi\Winter\MuiDataGrid\MuiGridRequest; class ArticleGridRequest extends MuiGridRequest { public function __construct( public ?int $authorId = null, // a domain filter of your own int $page = 0, int $pageSize = 20, #[ListOf(MGSortItem::class)] array $sortModel = [], #[Valid] MGFilterModel $filterModel = new MGFilterModel(), ) { parent::__construct($page, $pageSize, $sortModel, $filterModel); } }
2. Service
You own the query; the library overlays the grid concerns:
use Flytachi\Winter\Cdo\Qb; use Flytachi\Winter\MuiDataGrid\MuiGrid; use Flytachi\Winter\MuiDataGrid\MuiGridResponse; use Flytachi\Winter\MuiDataGrid\Schema\FilterType; use Flytachi\Winter\MuiDataGrid\Schema\GridColumn; use Flytachi\Winter\MuiDataGrid\Schema\GridSchema; class ArticleService { public function grid(ArticleGridRequest $request): MuiGridResponse { $repo = ArticleRepository::instance('a') ->select('a.id, a.title, a.views, a.created_at, au.name author_name') ->joinLeft(AuthorRepository::instance('au'), 'au.id = a.author_id') ->where(Qb::eq('a.is_published', true)); if ($request->authorId) { $repo->andWhere(Qb::eq('a.author_id', $request->authorId)); } $schema = GridSchema::make( GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable(), GridColumn::for('views', 'a.views')->filterable(FilterType::Number)->sortable(), GridColumn::for('authorName', 'au.name')->filterable(FilterType::String)->sortable(), GridColumn::for('createdAt', 'a.created_at')->filterable(FilterType::Date)->sortable(), )->defaultOrder('a.created_at DESC'); return MuiGrid::wrap($repo, $request, $schema); } }
3. Controller
#[PostMapping] public function grid( #[RequestJson, Valid] ArticleGridRequest $request ): ResponseEntity { return ResponseEntity::ok( $this->service->grid($request)->toArray() ); }
4. The request the frontend sends
{
"page": 0,
"pageSize": 25,
"sortModel": [{ "field": "views", "sort": "desc" }],
"filterModel": {
"logicOperator": "and",
"items": [
{ "field": "title", "operator": "contains", "value": "winter" },
{ "field": "authorName", "operator": "equals", "value": "Ada" }
]
}
}
5. The response
{
"rowCount": 134,
"rows": [
{ "id": 12, "title": "Winter internals", "views": 9001, "author_name": "Ada" }
]
}
rowCount is the total matching the filter (for the grid's pager); rows is the
current page.
Documentation
| Guide | What's inside |
|---|---|
| Getting started | End-to-end walkthrough, request/response shape, frontend wiring. |
| Schema & columns | GridSchema, GridColumn, mapping fields to SQL, whitelisting. |
| Filtering | FilterType, operator gating, per-column overrides, AND/OR logic. |
| Sorting | Sort model, custom ORDER BY expressions, default order. |
| Requests & responses | Extending MuiGridRequest, the entity DTOs, MuiGridResponse. |
| Recipes | Joins, subquery search, mappers, category-tree / EXISTS patterns. |
At a glance
// Entry point MuiGrid::wrap( RepositoryViewInterface $repo, // your pre-built query (SELECT/JOIN/base WHERE) MuiGridRequest $request, // the grid request DTO GridSchema $schema, // the filter/sort whitelist (required) ?callable $mapper = null, // optional fn(object $row): mixed ): MuiGridResponse; // { rowCount, rows } (JsonSerializable)
// Schema building blocks GridColumn::for('muiField', 'sql.column') ->filterable(FilterType::String) // allow filtering, gate operators by type ->sortable() // allow sorting by sql.column ->sortable('lower(sql.column)') // ...or by a custom expression ->filterUsing(fn(MGFilterItem $i): ?Qb => …); // per-column operator override GridSchema::make(GridColumn …)->defaultOrder('sql ASC');
FilterType |
Allowed operators |
|---|---|
String |
contains, notContains, startsWith, endsWith, equals, is, not, =, !=, isAnyOf, isEmpty, isNotEmpty |
Number |
equals, is, not, =, !=, >, >=, <, <=, isAnyOf, isEmpty, isNotEmpty |
Boolean |
is, equals, =, isEmpty, isNotEmpty |
Date |
is, not, equals, =, !=, after, onOrAfter, before, onOrBefore, isEmpty, isNotEmpty |
License
MIT — Flytachi