Tabel dengan Data Array
PandaPanel\Tables\ArrayTableData merender tabel dari record yang tidak berasal dari database, misalnya response API, file konfigurasi, atau report hasil perhitungan. Deklarasi kolom, search, sort, dan serialization tetap menggunakan table builder yang sama. Perbedaannya hanya pada sumber row dan siapa yang melakukan pemrosesan.
Gunakan pada custom page yang memiliki row untuk ditampilkan tetapi tidak memiliki model database di belakangnya. Untuk data berbasis Eloquent, gunakan resource table agar database yang menangani filtering, sorting, dan pagination.
Contoh minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Pages;
use App\Support\WeatherRow;
use PandaPanel\Pages\Page;
use PandaPanel\Tables\ArrayTableData;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class Weather extends Page
{
protected static ?string $title = 'Weather';
protected static string $component = 'Panels/Admin/Pages/Weather';
/**
* @return array<string, mixed>
*/
public function props(): array
{
$schema = TableSchema::make()->columns([
TextColumn::make('city')->searchable()->sortable(),
NumberColumn::make('temperature')->suffix('°C')->sortable(),
]);
$records = collect([
['city' => 'Oslo', 'temperature' => 4],
['city' => 'Lisbon', 'temperature' => 19],
['city' => 'Cairo', 'temperature' => 33],
])->map(static fn (array $row): WeatherRow => WeatherRow::make($row));
$data = ArrayTableData::make($schema, $records, request());
$page = $data->paginate();
return [
'table' => $schema->toArray(),
'state' => $data->state(),
'rows' => $data->rows($page),
'pagination' => $data->pagination($page),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Empat key payload tersebut sama dengan yang dikirim resource index, sehingga komponen Vue table yang sama dapat merendernya.
Row harus berupa model
Kolom membaca value menggunakan data_get() dan seluruh renderer setelahnya mengharapkan shape tersebut. Karena itu ArrayTableData menerima instance Model. Model yang tidak dipersist adalah cara termurah untuk mendapatkannya: tidak membutuhkan tabel database dan tidak membutuhkan migration.
<?php
declare(strict_types=1);
namespace App\Support;
use Illuminate\Database\Eloquent\Model;
final class WeatherRow extends Model
{
protected $guarded = [];
public $timestamps = false;
}2
3
4
5
6
7
8
9
10
11
12
13
14
Model::make($attributes) pada class dengan $guarded = [] sudah cukup. Row key yang dikirim ke frontend berasal dari $record->getKey(), jadi berikan atribut id jika row memerlukan identity stabil — misalnya untuk action.
API
public function __construct(
TableSchema $schema,
Collection $records,
Request $request,
?string $namespace = null,
)
public static function make(
TableSchema $schema,
iterable $records,
Request $request,
?string $namespace = null,
): self2
3
4
5
6
7
8
9
10
11
12
13
| Method | Signature | Return |
|---|---|---|
paginate() | paginate(): LengthAwarePaginator | halaman saat ini setelah search dan sort |
rows() | rows(LengthAwarePaginator $records): array | row yang diserialisasi melalui TableSchema::toRow() |
pagination() | pagination(LengthAwarePaginator $records): array | page, perPage, total, lastPage, from, to |
state() | state(): array | state yang benar-benar diterapkan |
sortableColumns() | sortableColumns(): list<Column> | kolom yang dapat diurutkan data source ini |
ArrayTableData bersifat readonly. Buat instance baru untuk setiap request; tidak ada state internal yang perlu digunakan ulang.
Fitur yang didukung
Search menggunakan pencocokan substring case-insensitive terhadap kolom yang mendeklarasikan searchable():
TextColumn::make('city')->searchable();Aturan whitelist sama seperti query layer: kolom yang tidak mendeklarasikan searchable tidak pernah dicocokkan, dan tabel tanpa searchable column mengabaikan ?search= sepenuhnya.
Sort membaca ?sort= dan ?direction=, lalu menerapkannya hanya jika kolom yang disebut mendeklarasikan sortable(). Ordering dilakukan dengan Collection::sortBy() terhadap Column::getSortColumn(), sehingga sortable(column: 'other_attribute') tetap berfungsi. Kolom tidak dikenal atau tidak sortable diabaikan dan state()['sort'] bernilai null.
Pagination membaca ?page= dan menggunakan TableSchema::getDefaultPerPage():
$schema->perPageOptions([2, 10])->defaultPerPage(2);Namespacing bekerja seperti pada relation table:
$data = ArrayTableData::make($schema, $records, $request, namespace: 'readings');
// reads ?readings[page]=2&readings[sort]=city2
Row serialization melalui TableSchema::toRow(), sehingga cells, cellMeta, dan action per record dibuat dengan cara yang sama seperti resource index. Semua tipe kolom dapat digunakan, termasuk CustomColumn.
Fitur yang tidak didukung
| Fitur | Perilaku |
|---|---|
| Filters | tidak diterapkan; state()['filters'] dan filterIndicators selalu kosong |
| Pencarian per kolom | tidak dibaca; state()['columnSearches'] selalu kosong |
| Grouping | tidak diterapkan; state()['group'] selalu null |
| Column manager | state()['columns'] selalu melaporkan visibility dan order yang dideklarasikan schema |
| Session persistence | tidak ada session key, sehingga tidak ada state yang diingat |
?perPage= | diabaikan; default dari schema yang digunakan |
| Relationship aggregates | applyColumnQueries() tidak dipanggil, sehingga counts(), sum(), dan sejenisnya tidak menghasilkan nilai |
| Relation search | getSearchRelations() tidak digunakan; dotted searchable name tidak dicocokkan |
| Summaries | tidak dihitung di sini; panggil TableSchema::summaries() sendiri jika dibutuhkan dan ingat method tersebut mengharapkan Eloquent builder |
Gunakan resource table ketika daftar memerlukan fitur-fitur tersebut. Ini bukan sekadar kekurangan yang belum diimplementasikan: filter dan grouping terhadap in-memory set akan membutuhkan implementasi perilaku kedua yang sebenarnya sudah dimiliki query layer.
Batas sebenarnya adalah skala
Search, sort, dan pagination semuanya dilakukan di memory terhadap seluruh set. Fitur ini cocok untuk puluhan atau ratusan row — ukuran yang realistis untuk config file atau satu halaman response API. Data yang lebih besar sebaiknya berada di query agar database dapat mengerjakannya.
Pengujian
use Illuminate\Http\Request;
use PandaPanel\Tables\ArrayTableData;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
$schema = TableSchema::make()->columns([
TextColumn::make('city')->searchable()->sortable(),
]);
$data = ArrayTableData::make(
$schema,
collect([['city' => 'Oslo'], ['city' => 'Cairo']])
->map(static fn (array $row): WeatherRow => WeatherRow::make($row)),
Request::create('/', 'GET', ['sort' => 'city', 'direction' => 'asc']),
);
$cities = collect($data->rows($data->paginate()))->pluck('cells.city')->all();
expect($cities)->toBe(['Cairo', 'Oslo'])
->and($data->state()['sort'])->toBe('city');2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hal yang perlu diperhatikan
- Row tanpa key diserialisasi sebagai
key => null. Berikan atributidpada model jika row harus dapat dibedakan, terutama karena row action mengalamatkan record berdasarkan key. - Sorting menggunakan semantics PHP, bukan database.
Collection::sortBy()mengikuti perbandingan PHP sehingga mixed types dapat memiliki urutan berbeda dariORDER BYdatabase. - Search menggunakan substring, bukan
LIKE. Tidak ada wildcard, tidak ada escaping pattern, dan tidak ada traversal relation. state()['search']berisi raw parameter. Berbeda dengan query layer, nilai tersebut tidak di-trim atau dipotong sebelum dikembalikan.- Seluruh set berada di memory untuk setiap request. Jika membangun collection mahal, cache collection-nya, bukan table instance.
- Action tetap dapat digunakan, tetapi membutuhkan tempat untuk dijalankan. Row action melakukan POST ke panel action endpoint yang me-resolve record melalui sebuah resource. Tabel yang berisi row tanpa resource tidak memiliki mekanisme lookup tersebut.