Relationship Forms
Ada dua jenis data pada form yang dapat dimiliki table lain: related record yang diedit bersama owner record, dan pilihan related record yang disimpan sebagai foreign key atau sekumpulan pivot rows. Jenis pertama menggunakan PandaPanel\Forms\Layouts\Relationship; jenis kedua menggunakan PandaPanel\Forms\Components\Select dengan relationship(). Gunakan keduanya ketika form sebuah resource tidak hanya menulis satu row.
Contoh minimal
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Forms\Layouts\Relationship;
public static function form(FormSchema $schema): FormSchema
{
return $schema->schema([
TextInput::make('name')->required(),
// Foreign key pada record ini.
Select::make('author')->relationship('author', 'name'),
// Sekumpulan pivot rows.
Select::make('labels')->relationship('labels', 'name'),
// Field milik record lain.
Relationship::make('brief')
->heading('Project brief')
->schema([TextInput::make('summary')->required()]),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Field milik related record diberi nama brief.summary pada wire dan validation rules. Laravel mendukung nested key secara native, sehingga error kembali menggunakan dotted key yang sama dengan field renderer. Dengan namespace ini, column summary pada owner dapat coexist dengan summary milik related record.
Relationship
Relationship adalah kelompok field yang dimiliki satu related record—BelongsTo, HasOne, atau MorphOne—dan diedit dari form owner.
| Method | Signature | Default |
|---|---|---|
make() | static make(string $relation): self | |
schema() | schema(array<array-key, FormComponent> $components): self | [] |
heading() | heading(string $heading): self | Str::headline($relation) |
description() | description(string $description): self | null |
columns() | columns(int $columns): self | 1, di-clamp 1–4 |
createsMissing() | createsMissing(bool $createsMissing = true): self | true |
getRelation() | getRelation(): string | |
shouldCreateMissing() | shouldCreateMissing(): bool | |
children() | children(): list<FormComponent> | |
fields() | fields(): list<Field> | seluruh field secara recursive |
save() | save(Model $owner, array $validated): void | dipanggil schema |
toArray() | toArray(?Model $record, string $page): array |
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Components\Textarea;
use PandaPanel\Forms\Layouts\Relationship;
Relationship::make('profile')
->heading('Profile')
->description('Shown on the public author page.')
->columns(2)
->schema([
TextInput::make('headline'),
Textarea::make('bio')->columnSpanFull(),
]);2
3
4
5
6
7
8
9
10
11
12
Nama, value, dan rules
schema() memberi prefix nama relation kepada setiap field yang dimilikinya satu kali saat components diset. Dua reader memisahkan dua pertanyaan berbeda:
$field->getName(); // 'profile.bio' — wire, rules, error key
$field->getAttribute(); // 'bio' — attribute pada related record2
Value dibaca dari related record, bukan owner. Group menyerialisasi children menggunakan $owner->getRelationValue($relation), atau tanpa record jika relation belum memiliki related record.
use App\Models\Project;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Forms\Layouts\Relationship;
$schema = FormSchema::make()
->model(Project::class)
->schema([
Relationship::make('brief')->schema([TextInput::make('summary')]),
]);
array_keys($schema->validationRules()); // ['brief.summary']
$schema->dehydrate(['brief' => ['summary' => 'x']]); // [] — bukan attribute owner2
3
4
5
6
7
8
9
10
11
12
13
Browser mengirim {"brief": {"summary": "x"}}. Renderer memperluas dotted field name ke nested shape yang dipahami Laravel, tetapi tetap mempertahankan flat dotted key secara internal agar error brief.summary jatuh ke field yang benar.
Proses write
FormSchema::saveRelations(Model $record, array $validated) memanggil Relationship::save() untuk setiap group setelah owner disimpan, di dalam transaction yang sama. Urutan ini wajib: HasOne atau MorphOne membutuhkan foreign key ke owner row yang belum ada sebelum owner disimpan. BelongsTo tetap menggunakan path yang sama agar related write tidak memiliki dua implementation berbeda.
save() melakukan langkah berikut:
- Me-resolve
$owner->{$relation}(). Method yang tidak mengembalikan Eloquent relation dilewati. - Mengumpulkan attributes dari child fields melalui dehydration, keyed by
getDehydrateKey()dan melewatimutate(). Path yang tidak ada pada validated data dilewati; missing dannulladalah dua kondisi berbeda. - Jika tidak ada attribute yang di-dehydrate, method berhenti. Group dengan seluruh value absent tidak membuat row kosong.
- Jika related record sudah ada, jalankan
forceFill($attributes)->save(). - Jika related record belum ada dan
createsMissing()aktif, buat record baru. UntukBelongsTo, related record disimpan terlebih dahulu lalu foreign key owner diperbarui, karena owner tidak dapat menunjuk row yang belum ada.
use PandaPanel\Forms\Layouts\Relationship;
// Jika related record belum ada, relation dibiarkan tetap kosong.
Relationship::make('brief')->createsMissing(false)->schema([...]);2
3
4
createsMissing() default-nya aktif. Form yang menampilkan empty inputs untuk profile yang belum dimiliki user tetapi kemudian membuang input tersebut secara silent akan menjadi UX yang buruk; default framework memilih membuat related record ketika user memang mengisinya.
Relation-backed Select
use PandaPanel\Forms\Components\Select;
Select::make('author')->relationship('author', 'name'); // BelongsTo
Select::make('labels')->relationship('labels', 'name'); // BelongsToMany2
3
4
| Relation | Render | Validation | Ditulis oleh |
|---|---|---|---|
BelongsTo | single select | exists pada related table | dehydrate(), ke foreign key |
BelongsToMany, MorphToMany | multiple select | array + exists untuk setiap element | saveRelations(), melalui sync() |
Schema menurunkan behavior tersebut dari model class. Karena itu form tidak perlu menuliskan ->dehydrateTo('author_id') di samping ->relationship('author'), dan tidak perlu mengingat bahwa pivot baru dapat ditulis setelah owner record tersedia.
$schema->dehydrate(['author' => '3']); // ['author_id' => '3']
$schema->dehydrate(['labels' => ['1', '2']]); // [] — disinkronkan setelah owner tersimpan2
Many-to-many Select juga di-hydrate dari pivot. FormSchema::toArray() menggunakan Select::relatedKeys($record) dan memasukkan key yang sedang attached ke field value.
sync() mengganti seluruh set. Empty submission berarti detach seluruh relation. Submitted value yang bukan array diperlakukan sebagai empty set.
Options, search, dan limits dijelaskan pada Options endpoints.
Membaca seluruh relationship group
$schema->relationshipGroups(); // list<Relationship>, ditemukan pada kedalaman apa punFormSchema berjalan melalui seluruh component tree. Karena itu group yang berada di dalam Section atau Tab tetap ditemukan dan ditulis. Schema sengaja tidak masuk lagi ke Relationship group yang sudah ditemukan. Nested Relationship group berarti dua related record dari satu nesting level tanpa boundary ownership yang jelas, sehingga tidak didukung.
Pivot fields pada relation form
Relation Manager memiliki bentuk relationship form lain: fields milik related record dan fields milik pivot ditampilkan berdampingan. PandaPanel\Resources\RelationForm menggabungkan dua schema untuk rendering dan validation, tetapi memisahkannya kembali saat persistence. Pivot half diberi namespace pivot. sehingga column role pada join table tidak dapat menimpa role pada related record. Lihat Pivot fields.
Yang tidak didukung di sini
- To-many relation di dalam
Relationshipgroup. Layout ini menulis satu related record. Daftar related records harus menggunakan relation manager, nested resource, atau—jika row hanya plain data—Repeater. - Nested relationship group. Children dari group adalah fields, bukan Relationship group lain.
- Menghapus related record. Group hanya create/update. Removal merupakan action.
- Membuat related record untuk
MorphTo. Single-record relation yang didukung adalahBelongsTo,HasOne, danMorphOne.
Catatan
- Related record ditulis menggunakan
forceFill(). Attribute list berasal dari schema, bukan request langsung. Itulah alasan melewati$fillabletetap aman, sama seperti write owner record. - Field Relationship tidak pernah masuk attributes owner.
dehydrate()mengecualikannya berdasarkan nama, sehinggabrief.summarytidak dapat salah ditulis kesummarycolumn milik project. - Semua write berada dalam satu transaction. Owner, related records, dan pivot rows ditulis bersama ketika Page menggunakan
$hasDatabaseTransactions, sehingga form tidak berhenti dalam keadaan half-saved. getRelationValue()menentukan apakah related record dianggap existing. Relation yang belum loaded akan diload oleh method tersebut; value yang bukan model dianggap absent.- Duplicate full field name tetap ditolak. Prefix diterapkan sebelum uniqueness check.
profile.biodanbioadalah dua nama berbeda, tetapi duabiodi dalam satu group tetap duplicate. - Group tetap dirender ketika related record belum ada. Layout selalu render; hanya field yang dapat hilang berdasarkan Page visibility.