Kolom Relasi
Kolom tabel dapat membaca related record alih-alih hanya kolom dari tabelnya sendiri: jumlah post milik author, apakah order memiliki refund, atau nama dari profile yang terhubung. Gunakan ketika value berada di tabel lain tetapi tetap menjelaskan row utama yang sedang ditampilkan.
Ada tiga kebutuhan berbeda yang dibahas di sini dan sengaja dipisahkan karena query melakukan pekerjaan berbeda untuk masing-masing: aggregate dihitung di SELECT, sorting berdasarkan related column menggunakan correlated subquery, sedangkan searching melalui relation menggunakan whereHas. Semuanya tersedia pada base Column, sehingga dapat digunakan oleh semua tipe kolom.
Contoh minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Authors\Tables;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class AuthorsTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table->columns([
TextColumn::make('name')->searchable()->sortable(),
// Computed in the select: one query for the whole page.
NumberColumn::make('posts_count')->counts('posts')->sortable(),
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Aggregate
use PandaPanel\Tables\Columns\Column;
Column::counts(string $relation): static
Column::exists(string $relation): static
Column::sum(string $relation, string $column): static
Column::avg(string $relation, string $column): static
Column::min(string $relation, string $column): static
Column::max(string $relation, string $column): static2
3
4
5
6
7
8
use PandaPanel\Tables\Columns\BooleanColumn;
use PandaPanel\Tables\Columns\NumberColumn;
NumberColumn::make('posts_count')->counts('posts'),
BooleanColumn::make('refunds_exists')->exists('refunds'),
NumberColumn::make('orders_sum_total')->sum('orders', 'total')->prefix('$')->decimals(2),
NumberColumn::make('orders_avg_total')->avg('orders', 'total')->decimals(2),
NumberColumn::make('orders_min_total')->min('orders', 'total'),
NumberColumn::make('orders_max_total')->max('orders', 'total'),2
3
4
5
6
7
8
9
TableSchema::applyColumnQueries() berjalan sebelum paginator dan memberi kesempatan setiap kolom membentuk query. Aggregate hanya diminta sekali untuk seluruh halaman, berapa pun jumlah row-nya. Membaca aggregate per record akan menghasilkan satu query per record — masalah yang biasanya ingin dihindari dengan eager loading atau aggregate select.
Setiap method dipetakan ke satu pemanggilan Eloquent dan atribut yang dihasilkan Eloquent:
| Method | PandaPanel\Tables\Enums\RelationshipAggregate | Pemanggilan Eloquent | Atribut hasil |
|---|---|---|---|
counts('posts') | Count | withCount('posts') | posts_count |
exists('posts') | Exists | withExists('posts') | posts_exists |
sum('orders', 'total') | Sum | withAggregate('orders', 'total', 'sum') | orders_sum_total |
avg('orders', 'total') | Avg | withAggregate(..., 'avg') | orders_avg_total |
min('orders', 'total') | Min | withAggregate(..., 'min') | orders_min_total |
max('orders', 'total') | Max | withAggregate(..., 'max') | orders_max_total |
Titik pada relation path diganti underscore dalam nama atribut, mengikuti aturan Eloquent. Contohnya counts('posts.comments') menghasilkan posts_comments_count.
Cell membaca atribut generated tersebut, bukan nama kolomnya sendiri:
use PandaPanel\Tables\Columns\Column;
Column::aggregateAttribute(): ?string // 'posts_count', or null for a plain column
Column::getAggregateRelation(): ?string // 'posts'
Column::applyQuery(Builder $query): void // called by TableSchema::applyColumnQueries()
Column::summaryUsesAggregate(): bool2
3
4
5
6
use PandaPanel\Tables\Columns\NumberColumn;
// The column may be called anything; it still reads `posts_count`.
NumberColumn::make('published')->label('Posts')->counts('posts')->aggregateAttribute(); // 'posts_count'2
3
4
Mengurutkan aggregate
Generated attribute adalah kolom nyata pada result set, sehingga sorting dapat menggunakan ORDER BY biasa tanpa strategi khusus. Pemanggilan aggregate method mengatur sort column ke generated attribute secara otomatis:
use PandaPanel\Tables\Columns\NumberColumn;
// Name matches the generated attribute — the common case, and the safe one.
NumberColumn::make('posts_count')->counts('posts')->sortable(),2
3
4
sortable(bool $sortable = true, ?string $column = null) mengisi $column tanpa kondisi, sehingga memanggilnya setelah aggregate tanpa argumen akan me-reset sort column kembali ke nama kolom. Jika nama kolom dan generated attribute berbeda, sebutkan secara eksplisit:
use PandaPanel\Tables\Columns\NumberColumn;
NumberColumn::make('published')
->label('Posts')
->counts('posts')
->sortable(column: 'posts_count'),2
3
4
5
6
Membuat summary dari aggregate
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Summaries\Average;
use PandaPanel\Tables\Summaries\Sum;
NumberColumn::make('posts_count')
->counts('posts')
->summarize([Sum::make()->label('Total'), Average::make()]),2
3
4
5
6
7
Column::summaryColumn() mengembalikan aggregate attribute ketika tersedia, sehingga figure menghitung value yang benar-benar dipilih query. Karena posts_count hanya ada pada SELECT list, summary dihitung di luar subquery yang menghasilkan alias tersebut, bukan menggunakan sum(posts_count) langsung terhadap base table yang tidak memiliki kolom itu. Lihat Summary.
Mengurutkan berdasarkan related column
use PandaPanel\Tables\Columns\Column;
Column::sortableByRelation(string $relation, string $column): static
Column::getSortRelation(): ?string
Column::applyRelationshipSort(Builder $query, SortDirection $direction): void2
3
4
5
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('author_name')
->label('Author')
->sortableByRelation('author', 'name'),2
3
4
5
Ini membuat kolom sortable dan mengambil alih ordering. Query yang dihasilkan menggunakan correlated subquery:
order by (select name from profiles where profiles.author_id = authors.id limit 1) ascBukan join. Join terhadap to-many relation dapat menggandakan row dan diam-diam merusak page size maupun total. Test memastikan hal tersebut, misalnya sorting dua project berdasarkan hasOne brief tetap menghasilkan total dua.
Subquery membandingkan qualified foreign key milik relation dengan qualified parent key. Bentuk ini cocok untuk HasOne, HasMany, dan morph variant karena foreign key berada di related table. BelongsTo menyimpan foreign key pada tabel utama, sehingga perbandingan standar tersebut tidak sesuai; gunakan sortUsing() untuk kasus itu.
Mencari melalui relation
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('author.name')->label('Author')->searchable(),2
3
Nama searchable yang mengandung titik diarahkan ke whereHas, bukan LIKE. author.name bukan kolom lokal pada tabel utama; memperlakukannya sebagai kolom akan menghasilkan SQL error. Titik terakhir memisahkan relation path dari column, sehingga author.profile.city mencari city melalui relation author.profile.
TableSchema memisahkan local search column dan relation search:
use PandaPanel\Tables\TableSchema;
TableSchema::getSearchColumns(): array // local names only
TableSchema::getSearchRelations(): array // the dotted ones
TableSchema::isSearchable(): bool // true when either is non-empty2
3
4
5
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
$schema = TableSchema::make()->columns([
TextColumn::make('name')->searchable(),
TextColumn::make('tasks.name')->searchable(),
]);
$schema->getSearchColumns(); // ['name']
$schema->getSearchRelations(); // ['tasks.name']2
3
4
5
6
7
8
9
10
Satu kolom juga dapat diarahkan ke beberapa lokasi sekaligus, mencampur local column dan relation:
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('name')->searchable(columns: ['first_name', 'last_name', 'company.name']),2
3
Setiap kata dari term dicocokkan di dalam group where(...) sendiri, sehingga relation search tidak dapat memperluas filter yang sudah diterapkan. Lihat Pencarian.
Membaca related value di cell
Column::resolveValue() membaca atribut menggunakan data_get(), sehingga dot notation dapat digunakan untuk display maupun search:
use Illuminate\Support\Collection;
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('author.name')->label('Author')->placeholder('Unassigned'),
// A to-many path gives a collection of values, so format it into a string.
TextColumn::make('tags.name')
->label('Tags')
->placeholder('None')
->formatUsing(static fn (mixed $value): ?string => $value instanceof Collection && $value->isNotEmpty()
? $value->implode(', ')
: null),2
3
4
5
6
7
8
9
10
11
12
Membaca relation dengan cara ini dapat memicu lazy load, yaitu satu query per row, kecuali resource melakukan eager loading. Gunakan Resource::$with; nilai tersebut diterapkan pada setiap query yang dibangun resource sehingga serialization kolom tidak memicu query tambahan untuk setiap record:
/**
* @var list<string>
*/
protected static array $with = ['author', 'tags'];2
3
4
Aggregate tidak membutuhkan with() — justru aggregate dihitung dalam SELECT untuk menghindari pemuatan relation satu per satu.
Catatan
- Tiga tool, tiga pekerjaan. Aggregate menjawab "berapa banyak";
sortableByRelation()menjawab "urutkan berdasarkan milik siapa"; dottedsearchable()menjawab "apakah related record cocok". Menggunakan satu fitur untuk pekerjaan fitur lain adalah sumber query yang sulit diprediksi. - Aggregate attribute mengikuti aturan Eloquent, sehingga cell membaca tepat nilai yang ditulis query. Jika nama kolom mengikuti generated attribute, cell, sort, dan summary dapat selaras tanpa deklarasi tambahan.
->sortable()setelah aggregate me-reset sort column. Berikan attribute secara eksplisit, atau panggilsortable()sebelum aggregate method.exists()lebih murah daripadacounts()ketika Anda hanya membutuhkan informasi ada/tidak dan tidak perlu jumlah sebenarnya; hasil cell berupa boolean.- Dotted searchable name tidak otomatis sortable.
Column::getSortColumn()membiarkan dotted name apa adanya; gunakansortableByRelation()untuk ordering relation. - Filter dan query builder tidak melakukan traversal relation secara otomatis. Constraint
QueryBuilderFiltermenunjuk kolom pada queried table. Persempit relation menggunakanFormFilterdan closurewhereHas. Lihat Query builder. - Relation manager adalah fitur berbeda. Relationship column menampilkan related data di resource table, sedangkan relation manager memberikan tabel tersendiri untuk related record milik sebuah record. Lihat Tabel relasi.