Computed values
A column does not have to be a column of the table: state() computes the
value per record, and queryColumn() tells the list which database
columns to use for sorting and searching.
state()
Section titled “state()”TextColumn::make('name') ->state(fn ($person): string => $person->last_name . ', ' . $person->first_name),
TextColumn::make('hint') ->state(fn ($person): ?string => $person->entry_date > date('Y-m-d') ? 'Joins soon' : null),
TextColumn::make('total')->money('EUR') ->state(fn ($order) => $order->quantity * $order->price),The closure receives the record (an array or an entity, as the model
returns it) and returns the value; the format options of the column apply
to it (money(), date(), labels(), …). Return null for an empty
cell.
The closure runs for every shown row: keep it cheap. Do not query the database per row – use a join, a sub-select or a CountColumn.
Query columns
Section titled “Query columns”A computed value cannot be sorted or searched by itself: the database does
not know it. sortable() and searchable() only take effect when the
column names the database columns with queryColumn():
TextColumn::make('name') ->state(fn ($person): string => $person->last_name . ', ' . $person->first_name) ->queryColumn('people.last_name', 'people.first_name') ->sortable() ->searchable(),- Sorting uses the columns in order: by last name, then first name.
- Searching matches each column on its own:
müllerfinds “Müller, Hans”, “Hans Müller” finds both words separately (OR), but not as one value. To search a composed value use a TextFilter with expression(). - Names are
columnoralias.column([a-z_][a-z0-9_]*); anything else throws anInvalidArgumentException. Plain columns of the main table are prefixed with its name automatically when noqueryColumn()is set.
queryColumn() also works without state(), e.g. when the attribute name
of the record differs from the column to sort by.
Columns of a join
Section titled “Columns of a join”Values of other tables come into the list with a join in scopeQuery() of
the resource. The tenant scope of the main model stays in place.
public function scopeQuery(Model $model): void{ $model->select('invoices.*, c.name AS customer_name') ->join('customers c', 'c.id = invoices.customer_id');}
public function collection(Collection $collection): Collection{ return $collection->columns([ TextColumn::make('number')->sortable()->searchable(), TextColumn::make('customer_name')->label('Customer')->queryColumn('c.name')->sortable()->searchable(), ]);}A person’s full name from a join, sorted by last name, then first name:
public function scopeQuery(Model $model): void{ $model->select("contacts.*, CONCAT(p.last_name, ', ', p.first_name) AS person", false) ->join('people p', 'p.id = contacts.person_id');}
// in collection():TextColumn::make('person')->queryColumn('p.last_name', 'p.first_name')->sortable()->searchable(),Joined tables are not scoped by Anvil: join only through keys of records the main query already restricts (as above), or add the tenant condition to the join yourself.