Eloquent tables
Backing a table with an Eloquent model is common enough that Lattice ships a source for it. Extend
EloquentTableDefinition and implement builder() instead of source() — Lattice wires up an
EloquentTableSource from your builder, columns, and pagination mode. It is the same
TableSource contract underneath, with the query-building done for you.
use Illuminate\Database\Eloquent\Builder;use Lattice\Lattice\Attributes\AsTable;use Lattice\Lattice\Tables\Columns\NumberColumn;use Lattice\Lattice\Tables\Columns\TextColumn;use Lattice\Lattice\Tables\Sources\Eloquent\EloquentTableDefinition;use Lattice\Lattice\Tables\TableQuery;
/** * @extends EloquentTableDefinition<Product> */#[AsTable('app.products')]class ProductsTable extends EloquentTableDefinition{ public function columns(): array { return [ TextColumn::make('name')->sortable()->filterable(), NumberColumn::make('price')->sortable()->filterable(), ]; }
public function builder(TableQuery $query): Builder { return Product::query()->select(['id', 'name', 'price']); }}The builder
Section titled “The builder”builder() returns the base query — the rows the table draws from before the request’s filters
and sorts are applied. Lattice applies those for you (see below), so the builder only needs your own
scoping: a select(), eager loads, tenant constraints, a default order.
public function builder(TableQuery $query): Builder{ $builder = Product::query() ->where('team_id', $this->team->id) ->select(['id', 'name', 'price', 'status']);
if ($query->sorts === []) { $builder->latest('id'); }
return $builder;}The TableQuery is passed in so the builder can react to the current request — the example applies a
default order only when the user hasn’t chosen a sort.
How filters and sorts are applied
Section titled “How filters and sorts are applied”EloquentTableSource reads the request’s filters and sorts from the TableQuery and applies them to
your builder automatically, driven by the columns’ capabilities:
- A filter is applied only for a column that is
filterable(), using the column’sFilterTypeto build the rightwhere. - A sort becomes an
orderByon the column key.
Because both map to the column key, a filterable or sortable column key must be a real database column (or an aliased select) — or a relation field, below. For anything computed, keep the column display-only or back the table with a custom source.
Relation columns
Section titled “Relation columns”A column keyed with dot notation reads through a to-one relation. TextColumn::make('author.name')
resolves the author relation and shows its name; Lattice eager-loads the relation, so there is no
N+1. Because it maps to a real relation and field, it stays fully sortable and filterable — the sort
becomes a correlated subquery and the filter a whereHas:
public function columns(): array{ return [ TextColumn::make('title')->sortable()->filterable(), TextColumn::make('author.name')->label('Author')->sortable()->filterable(), ];}
public function builder(TableQuery $query): Builder{ return Post::query()->select(['id', 'title', 'author_id']);}For a to-many relation, key the column by the relation name and name the field with ->multiple().
It renders the related values as a list, filters through whereHas, and is never sortable. Combine it
with ->badge() to render each related value as a pill:
TextColumn::make('tags')->multiple('name')->label('Tags')->filterable();Selecting bulk-action rows
Section titled “Selecting bulk-action rows”When a table has bulk actions, the source also resolves the selected
rows. EloquentTableSource handles this from your builder: resolveMatching() re-runs the filtered
query for “select all matching”, and resolveSelection() loads an explicit set of keys with
whereKey(). You don’t write anything extra — defining the bulk actions is enough.