Tables and columns
Columns define what is shown in a Resource's listing. They are declared inside columns():
public function columns(): array
{
return [
Column::make('title')->sortable()->searchable(),
Column::make('is_published')->as('boolean')->sortable(),
Column::make('published_at')->as('date')->displayFormat('d/m/Y')->sortable(),
Column::make('status')
->as('badge')
->value(fn (Post $post): string => $post->is_published ? 'Published' : 'Draft')
->color(fn (Post $post): string => $post->is_published ? 'success' : 'gray'),
];
}The Column API
| Method | Description |
|---|---|
make(string $name) | The attribute name on the Eloquent model (or the logical name, if you use value()). |
label(string $label) | Header label. By default, Str::headline($name). |
as(string $type) | How the cell is rendered on the frontend: text (default), boolean, date, badge… |
sortable(bool $sortable = true) | Allows sorting the listing by this column. |
hiddenByDefault(bool $hidden = true) | Hidden by default; the user can show it from the column picker and choose the order from the listing itself. |
displayFormat(string $format) | Display format when as('date') (PHP date() syntax). |
value(Closure $callback) | Replaces the cell value with a computed value. It receives the whole model, not just the attribute. |
color(Closure $callback) | Color of the cell (mostly relevant for as('badge')). It receives the whole model. |
circular(bool $circular = true) | Renders the value of an as('image') column as a circular avatar. It does nothing for the other types. |
url(Closure $callback) | Resolves a link per row. It receives the model and returns the URL, or null to leave that row without a link. |
searchable(bool $searchable = true, ?Closure $using = null, ?Closure $refine = null) | Includes the column in the listing's free-text search. With using, it replaces the default LIKE, and with refine, it refines the result in PHP (see "Custom search"). |
An as('boolean') column is rendered as an icon (✓/✗), not as the English "Yes"/"No" text.
Columns with an image and a link
Column::make('cover_path')
->as('image')
->circular()
->url(fn (Post $post): ?string => $post->cover_path === null ? null : "/storage/{$post->cover_path}")
->hiddenByDefault()The url() callback receives the Eloquent record and returns the link. The result is added to each row as {column}_url, in the same way that the resolved color of a badge column is added as {column}_color, it is not served as a schema property, it is computed per row.
Custom search
By default, searchable() compares the index's search term against the column with a simple LIKE. Pass using for columns that cannot be compared that way, a virtual column, or an encrypted one:
Column::make('author')
->value(fn (Post $post): ?string => $post->author?->name)
->searchable(using: fn (Builder $query, string $term): Builder => $query->whereHas(
'author',
fn (Builder $author) => $author->where('name', 'like', "%{$term}%")
))The closure receives a fresh nested query builder and the raw search term; it must mutate the builder it receives instead of returning a different one, because Laravel's where(Closure) discards whatever you return.
Searches that SQL can only narrow
An encrypted column, for example, cannot be compared with LIKE, but it can be indexed by a prefix. With refine, using only narrows the candidates and refine decides in PHP which ones match:
Column::make('name')->searchable(
using: fn (Builder $query, string $term): Builder => $query->where('name_index', 'like', mb_substr($term, 0, 3).'%'),
refine: fn (Student $student, string $term): bool => str_contains(mb_strtolower($student->name), mb_strtolower($term)),
)using runs in SQL and yields at most 500 candidates. refine receives each candidate and the term, and returns whether it matches. The candidates that pass are combined with the rest of the listing's searchable columns.
Computed columns
value() and color() receive the whole Eloquent record, not a simple $record->$name: a "logical" column that does not correspond to any attribute is just as valid as a direct one. The status example above does not read any status column from the database, it computes it from is_published.
Column::make('status')
->as('badge')
->value(fn (Post $post): string => $post->is_published ? 'Published' : 'Draft')
->color(fn (Post $post): string => $post->is_published ? 'success' : 'gray')Read-only view
When a Resource has a ViewAction, Arrel reuses columns() to render the record's detail view, with the same as(), value() and color() of each column. To group them into titled sections, declare infolist(). An empty entry is shown as a dash.