Field Select
PandaPanel\Forms\Components\Select adalah field pilihan: satu value dari sebuah daftar, atau beberapa value sekaligus. Daftarnya dapat bersifat statis — ditulis langsung di schema — atau di-resolve dari Eloquent relation pada server. Gunakan field ini ketika value yang diterima merupakan closed set, atau ketika field menunjuk ke record lain.
Form minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts\Forms;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
final class PostForm
{
public static function configure(FormSchema $schema): FormSchema
{
return $schema->schema([
Select::make('status')
->options([
'draft' => 'Draft',
'review' => 'In review',
'published' => 'Published',
])
->required(),
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Rule yang dihasilkan menjadi required|in:"draft","review","published". Daftar option berfungsi sebagai whitelist, sehingga value yang tidak pernah ditawarkan oleh schema bukan sekadar value yang tidak terduga — value tersebut invalid.
Static options dan relation options divalidasi dengan cara berbeda
Ini adalah hal utama yang perlu dipahami sebelum membaca bagian lain pada halaman ini.
Static options() | relationship() | |
|---|---|---|
| Sumber daftar | array yang Anda deklarasikan | query terhadap related table |
| Rule untuk value | Rule::in(...) terhadap key | Rule::exists($table, $key) |
| Batas jumlah | tidak ada; seluruh array dikirim | optionLimit(), default 50 |
| Searchable | search box dapat muncul, tetapi server mengembalikan daftar yang sama | ya, difilter di server |
Static list adalah whitelist dan biasanya cukup kecil untuk dikirim seluruhnya. Relation berbeda: daftar yang dirender hanyalah satu bounded page dari table yang dapat memiliki ribuan row. Karena itu validitas value ditentukan database, sedangkan options hanya merepresentasikan bagian yang kebetulan dapat ditampilkan browser. Memvalidasi relation hanya berdasarkan halaman yang sedang ditampilkan dapat menolak key yang benar hanya karena row tersebut berada terlalu jauh dalam urutan.
Method
options(array $options): self
Menerima array<array-key, string>, dengan key sebagai value yang akan disimpan. Default [].
use PandaPanel\Forms\Components\Select;
Select::make('locale')->options([
'en' => 'English',
'id' => 'Bahasa Indonesia',
]);2
3
4
5
6
Key dibandingkan sebagai string ketika rule in dibangun. Karena itu array dengan integer key tetap bekerja dan divalidasi sebagai in:"1","2".
Jika tidak ada options dan tidak ada relation, field tidak menghasilkan rule in sama sekali, karena empty whitelist akan membuat semua value mustahil valid. Akibatnya field hanya divalidasi oleh required atau nullable. Jadi empty option list bukan default yang aman — bangun list tersebut secara eksplisit:
use App\Enums\PostStatus;
Select::make('status')->options(
collect(PostStatus::cases())
->mapWithKeys(static fn (PostStatus $case): array => [$case->value => $case->name])
->all(),
);2
3
4
5
6
7
relationship(string $relation, string $titleAttribute): self
$relation adalah method relation pada model milik schema; $titleAttribute adalah column yang digunakan sebagai label.
use PandaPanel\Forms\Components\Select;
// The resource has already called $schema->model(Post::class), which is what
// lets the field find out what `author` points at.
Select::make('author')
->relationship('author', 'name')
->searchable()
->required();2
3
4
5
6
7
8
Behavior bergantung pada tipe relation:
| Relation | Bentuk field | Ditulis oleh |
|---|---|---|
BelongsTo | single select | FormSchema::dehydrate(), ke foreign key |
BelongsToMany / MorphToMany | multiple select | FormSchema::saveRelations(), melalui sync() |
Untuk BelongsTo, field dinamai berdasarkan relation dan dipersist ke foreign key. Karena itu form tidak perlu menuliskan ->dehydrateTo('author_id') di samping ->relationship('author').
Untuk many-to-many, tidak ada column pada record utama yang harus ditulis. dehydrate() melewati field tersebut, lalu pivot di-sync() setelah record tersimpan dan masih dalam transaction yang sama — relation untuk record baru memang belum dapat disinkronkan sebelum row utamanya ada.
Nama relation, related model class, dan query tidak pernah dikirim ke browser. Yang melewati wire hanyalah daftar pasangan {value, label}.
existsIn(string $table, string $column): self
Mengubah validasi menjadi "row dengan value ini harus ada pada table/column tersebut", bukan "value harus termasuk option yang sedang terlihat".
Select::make('country_code')
->searchable()
->existsIn('countries', 'code');2
3
relationship() otomatis mengisi konfigurasi ini dari table dan key related model, kecuali Anda telah memanggil existsIn() lebih dulu. Dalam kasus itu existsIn() memiliki prioritas. Ini berguna ketika constraint sebenarnya lebih sempit daripada "row apa pun di related table".
multiple(bool $multiple = true): self
Default false. Value berubah menjadi array dan renderer menampilkan checkbox list, bukan dropdown.
Select::make('tags')
->options(['php' => 'PHP', 'vue' => 'Vue'])
->multiple();2
3
Rule dibagi menjadi dua level: field utama divalidasi sebagai array, lalu setiap element divalidasi pada tags.* dengan rule in atau exists. Laravel tidak menginfer rule level element dari rule array, sehingga schema menambahkannya secara eksplisit.
hydrateRelationship() otomatis mengaktifkan multiple untuk BelongsToMany, jadi memanggil multiple() secara manual pada relation tersebut redundan.
searchable(bool $searchable = true): self
Default false. Menampilkan search box di atas control. Input di-debounce 250 ms lalu meminta filtered list ke endpoint options milik Panel.
Select::make('author')->relationship('author', 'name')->searchable();Pencarian dilakukan di server. resolveOptions() menggunakan where($title, 'like', '%term%') dengan \, %, dan _ di-escape, melakukan order berdasarkan title attribute, lalu membatasi hasil dengan optionLimit().
Option yang sudah terpilih tetap dipertahankan di daftar apa pun hasil search-nya. Tanpa behavior tersebut, user dapat memilih sebuah item lalu mengetik pencarian dan label untuk value yang masih selected bisa menghilang.
Jika request gagal, daftar sebelumnya tetap dipertahankan. List kosong memiliki arti "tidak ada yang cocok", sehingga mengosongkan list saat request error akan memberikan jawaban yang salah kepada user.
optionLimit(int $limit): self
Default 50. Menentukan jumlah row maksimum yang di-resolve oleh relation-backed select, baik pada first render maupun setiap pencarian. Nilai di bawah 1 di-clamp menjadi 1.
Select::make('author')
->relationship('author', 'name')
->searchable()
->optionLimit(20);2
3
4
Tidak berpengaruh pada static list karena static options dikirim seluruhnya.
resolveOptions(?string $modelClass = null, ?string $search = null): array
Mengembalikan pasangan value/label yang diterima browser sebagai list<array{value: string, label: string}>. Method ini dipanggil schema dan options endpoint, serta berguna untuk test langsung.
use App\Models\Post;
use PandaPanel\Forms\Components\Select;
$options = Select::make('author')
->relationship('author', 'name')
->resolveOptions(Post::class, 'ada');2
3
4
5
6
Untuk static list, kedua argument diabaikan dan mapped options langsung dikembalikan. Untuk relation tanpa $modelClass, method melempar InvalidArgumentException yang menyebut nama field.
relatedKeys(Model $record): array
Mengembalikan key yang saat ini terpilih pada many-to-many sebagai list<string>. FormSchema::toArray() memanggil method ini untuk mendapatkan value dari pivot, bukan dari attribute record yang memang tidak ada. Mengembalikan [] untuk relation selain BelongsToMany.
foreignKeyFor(string $modelClass): ?string
Mengembalikan column yang ditulis oleh BelongsTo select. Mengembalikan null untuk relation lain atau field tanpa relation. Inilah cara FormSchema mengubah field author menjadi write ke author_id saat dehydration.
writesToPivot(string $modelClass): bool
Menentukan apakah value harus ditulis ke related table/pivot, bukan ke column record. Menghasilkan true untuk BelongsToMany dan MorphToMany.
hydrateRelationship(string $modelClass): void
Me-resolve relation: mengaktifkan multiple untuk many-to-many, mengisi existsIn dari related model, dan memuat options. FormSchema memanggilnya secara idempotent sebelum membangun rules, serialization, dan dehydration. Ketiga proses tersebut membutuhkan informasi relation dan tidak boleh mengasumsikan proses lain sudah memanggilnya terlebih dahulu.
Schema adalah satu-satunya layer yang mengetahui model class, sehingga pemanggilan ini berada di schema, bukan field.
getRelation(): ?string dan isMultiple(): bool
Accessor read-only yang digunakan schema dan options endpoint.
Options endpoint
Searchable select membutuhkan endpoint untuk meminta data. URL dibangun server-side oleh PandaPanel\Support\FormEndpoints dan dikirim bersama form sebagai optionsUrl. Client hanya menambahkan field dan search, sehingga keystroke user tidak dapat mengganti form mana yang sedang diminta.
GET {panel}/options?resource={slug}&page=create&field=author&search=ada
GET {panel}/options?resource={slug}&page=edit&record=42&field=author&search=ada2
Route bernama panel.{panelId}.options dan ditangani oleh PandaPanel\Http\Controllers\PanelFormOptionsController. Controller tersebut:
- me-resolve Resource berdasarkan slug dan abort 404 jika Panel tidak memilikinya;
- memeriksa
canCreate()untuk create form,canEdit($record)untuk edit form, ataucanViewAny($owner)ditambah ability operasi terkait pada relation form; - mencari field di schema dan abort 404 jika field tidak dideklarasikan, atau 400 jika field bukan
Select; - memotong search term maksimum 255 karakter.
Field yang tidak dideklarasikan schema dianggap tidak ada, apa pun nama yang dikirim request. Prinsip yang sama juga digunakan pada sorting dan filtering. Lihat options endpoints.
Hal yang perlu diperhatikan
searchable()membutuhkan form yang menyediakan endpoint. Resource create/edit Page dan relation form dialog menyediakannya. Action form dan widget filter tidak, sehingga search box tidak dirender di sana; field hanya menampilkan options yang sudah diberikan.searchable()pada static list tidak melakukan filtering. Search box dapat muncul jika endpoint tersedia, tetapiresolveOptions()mengabaikan search term untuk static list dan mengembalikan array yang sama. Gunakan relation atau pertahankan list tetap pendek.- Relationship select membutuhkan
FormSchema::model(). Resource mengaturnya otomatis. Schema yang dibangun manual tanpa->model(Post::class)akan merender relation field tanpa options dan tanpa ruleexists, secara silent, karena layer tersebut tidak tahu relation menunjuk ke model apa. - Value datang sebagai string.
castForForm()mempertahankan single value hanya jika berupa string atau int, sedangkan multiple value dipetakan kelist<string>. Gunakan cast atau perbandingan yang sesuai saatvisibleWhen()bergantung pada select; condition memang membandingkan sebagai string. - Rule utama multiple select hanya
array. Pemeriksaanin/existsberada diname.*. MenambahkanRule::in(...)melaluirules()pada multiple select akan menerapkannya ke array, bukan element-elementnya. required()pada multiple select menolak empty array. RulerequiredLaravel gagal untuk Countable dengan length nol. Biasanya behavior ini tepat; gunakanrequired(false)jika empty set valid.- Title attribute harus real database column. Nilainya digunakan dalam
orderBy(), pencarianlike, danpluck(). Virtual accessor murni tidak dapat digunakan karena query gagal di database. Cast atau mutator pada column nyata tetap berlaku terhadap label. Tidak ada callback untuk menyusun label dari dua column; gunakan stored column jika itu dibutuhkan.