Server API reference (PHP)
Namespace: Elyra\DataGrid\Server. Install via composer require elyra/datagrid-server.
Grid
Factory entry point.
Grid::table(string $table): GridDefinition
GridDefinition
Fluent builder + the security allow-list.
| Method | Returns | Purpose |
|---|---|---|
table(string) (static) |
self | Create a definition. |
key(string $column) |
self | Primary key (tiebreaker, editing, selection). |
connection(?string $name) |
self | config/database.php connection. |
driver(string|GridDriver) |
self | 'elyrasql', 'mysql', 'pgsql', 'clickhouse', 'sqlite' / 'sqlanywhere', or an instance. Optional: without it the dialect follows the connection (pgsql, sqlite, clickhouse map to themselves; mysql/mariadb/other stay elyrasql). |
column(string $key, ...) |
self | Declare one column (see below). |
columns(array $keys) |
self | Quick-add columns with defaults. |
searchable(array $keys) |
self | Mark columns for global search. |
respond(GridRequest) |
array | Execute a query → GridResponse array. |
mutate(GridMutation) |
array | create/update/delete → GridMutationResult array. |
export(GridRequest, string $format = 'csv', string $filename = 'export') |
StreamedResponse | Streamed CSV/XLSX. |
authorize(callable $cb) |
self | Gate create/update/delete/export (fn($ability, $ctx): bool). Required before any write/export unless withoutAuthorization() is called. |
withoutAuthorization() |
self | Explicitly opt out of the grid-level write/export gate (route/policy guards it instead). |
limits(GridLimits) |
self | Override request guard rails (take/skip/filter-depth/in-values/export-rows/paste-rows). |
ask(string $query, ?callable $engine = null) |
FilterGroup | Natural-language → validated filter (see below). |
interpret(string $query, ?callable $engine = null) |
AskResult | Same translation, plus what the allow-list dropped. |
explain(?FilterGroup, ?string $search, array $headers, array $phrases) |
string | Describe a filter as a sentence (no LLM). |
editableColumns() |
GridColumn[] | Columns writable via mutations. |
all() |
array | All declared columns. |
column() signature
public function column(
string $key,
?string $column = null, // real column (default: key)
?string $expression = null, // raw SQL (read-only, not quoted)
bool $sortable = true,
bool $filterable = true,
bool $searchable = false,
?bool $aggregatable = null, // numeric → true
string $type = 'string', // string|number|date|bool
bool $editable = false, // write allow-list
string $editor = 'text', // text|number|select|date|checkbox
string|array|null $rules = null, // Laravel validation
array $options = [], // [['value'=>..,'label'=>..]] for select
): self
GridColumn
Value object describing one column (key, column/expression, sortable,
filterable, searchable, aggregatable, type, editable, editor,
rules, options). isExpression() reports whether it is a raw expression
(read-only).
Drivers — Contracts\GridDriver
Implement to support another dialect. Built-in:
| Class | Notes |
|---|---|
Drivers\ElyraSqlDriver (default) |
FACET() single-pass facets, PERCENTILE/MEDIAN, HYBRID search. |
Drivers\MySqlDriver |
Portability fallback; facets via N GROUP BY; MATCH full-text; no percentile/hybrid. |
Drivers\PostgresDriver |
Double-quoted identifiers; percentile_cont/median; tsvector full-text; no hybrid. |
Drivers\ClickHouseDriver |
OLAP column store; quantile/median; no ESCAPE clause; no hybrid. |
Drivers\SqliteDriver |
SQLite / SQL Anywhere; additive aggregates only; no percentile/hybrid. |
Interface surface: name(), quote(), ref(), aggregateExpr(),
groupBy(), facetQueries(), searchPredicate().
Engine internals
GridEngine— orchestratesrespond(): WHERE (filter + search) → viewport rows / groups / total / aggregates / facets / timing. Grouping runs oneGROUP BYper level so non-additive aggregates (avg/median/percentile/ countDistinct) are correct at every level.exportKeyset()returns the single-pass export plan (selects / table / where / bindings / key).GridMutator— validates and writes create/update/delete (only editable columns, bound values).GridExporter— streams CSV (BOM +;) or a rich XLSX (openspout: bold header band,#,##0.00numeric columns, column widths). Reads in bounded key-range windows (key >= a AND key < b) for constant memory on ElyraSQL.
Natural-language filtering — ask()
ask() translates a plain-language query into a validated FilterGroup using an
LLM whose JSON-schema output is restricted to this grid's declared, filterable
columns and the known operator set. The result is re-validated against the
allow-list (unknown fields/operators dropped) and compiled through the ordinary
FilterCompiler, so it can only ever produce filters the grid already permits;
no row data is sent to the model. Requires the optional laravel/ai package and
a provider key (otherwise a GridException is thrown). The LLM call sits behind
an injectable $engine so it can be stubbed in tests.
Each column is offered only the operators its type accepts (read through
FilterCompiler::operatorAllowedForType(), so there is no second table to drift),
and returned values are checked against the column type. Off-type output is
dropped during validation rather than reaching the compiler, which would refuse
the whole request.
interpret() returns an AskResult — the surviving FilterGroup plus a
dropped list of ['field', 'op', 'reason'], with matched() and partial()
helpers. Silently discarding a condition leaves the user with a filter answering a
narrower question than they asked; reporting it is what lets a client say so.
explain() runs the other direction — a filter tree rendered as a sentence — and
is not an LLM call. A filter tree is structured data, so describing it is
formatting rather than inference: faithful by construction, free, and available
whether or not the assistant is configured.
Full guide: Natural-language filtering.
Change auditing — GridRowChanged
Applied mutations fire Elyra\DataGrid\Server\Events\GridRowChanged, carrying
the row before and after over the declared columns only. changes() gives
field => ['old', 'new'] for what actually differs; inverse() gives the
mutation that would undo it (null for a delete — the write allow-list cannot
restore non-editable columns, and a partial restore would be worse than none).
Capturing the previous row costs one SELECT, so it happens only when something
is listening — datagrid.audit.enabled (null = auto, true/false to force).
Full guide: Editing.
Live updates — versionColumn()
->versionColumn('updated_at') declares what stamps the grid's data version.
meta: ['version'] then answers with dataVersion, built from COUNT(*) and
MAX(column) over the request's own filter and search.
The count is what makes a DELETE visible — MAX alone is unchanged by one. It is
still a heuristic: a delete and an insert between two polls landing on the same
maximum give the same stamp, and a write that does not touch the column is
invisible. Full guide: Live updates.
Request limits — GridLimits
GridDefinition::limits() (or config('datagrid.limits')) bounds a single
request: maxTake (1000), maxSkip (100k, caps deep OFFSET), maxFilterDepth
(15), maxInValues (1000), maxPasteRows (500, enforced in mutateBatch() so
every caller is covered — a Livewire paste, your own HTTP endpoint, or
application code), maxFacetValues (1000, also the default when a facet request
omits top), and maxExportRows (null = unlimited). Exceeding a limit throws a
GridException; maxTake and facet top are clamped instead.
maxSkip is neither clamped nor thrown. A window that starts past it returns
no rows and a pagingLimit field carrying the limit, so a client can say the
limit was reached. Up to 0.9.0 it was clamped, which answered a request for row
150,000 with the rows at 100,000 while the response still said skip: 150000 —
the wrong rows, shown in the wrong place. The Livewire pager also stops at the
last page the limit allows. Raise it with limits() if your result sets are
larger; each deeper page costs a larger OFFSET.
Request breadth is bounded too, because depth limits alone leave a
shallow-but-enormous request as a DoS lever on any grid with anonymous read
access: maxFilterConditions (200, across the whole tree), maxGroupLevels (5 —
the engine runs one query per level), maxAggregates (25 per aggregate list),
maxFacets (25, each its own GROUP BY) and maxSearchLength (200 characters).
maxGroupRows (10 000) is different in kind: it bounds a data-dependent
result rather than the request. Grouping a column with a million distinct values
is not abusive, just expensive, so it clamps each grouping level instead of
throwing — and the response carries groupsTruncated: true so a partial grouping
is never presented as the whole picture. Render that as a notice; do not ignore
it.
maxExportRows is only optional for grids that declare a key. A keyless grid
cannot be paginated (no stable order to page by), so its export buffers the whole
result set in memory — with no cap that is an OOM waiting to happen, and
export() refuses rather than risking it. Declare a key, or set an explicit cap.
Column capabilities
The declared flags are the read allow-list, and each is enforced where it
applies: sortable in the sort compiler, filterable in the filter compiler,
aggregatable for aggregate expressions, and editable for writes.
filterable: false also blocks facets and grouping on that column. Both
emit the column's distinct values, which is exactly what the flag withholds — a
facet on a non-filterable column would otherwise dump every value in it.
Exceptions
Exceptions\GridException is thrown for unknown fields, disallowed operations
(non-sortable/filterable/aggregatable), unsupported driver features, bad
operators, exceeded request limits, an unauthorized mutation/export, or an
unavailable assistant — the security guardrail that keeps clients within the
declared schema.
Service provider
GridServiceProvider is auto-discovered. The Livewire package adds
GridLivewireServiceProvider (views, translations, inlined theme).