Merapikan form dengan Form Objects v4: class Form khusus untuk menampung state, aturan validasi, dan logika simpan, bind() untuk menghubungkan ke model Eloquent, dan pola create/update terpusat yang bisa dipakai ulang di banyak komponen.

Setelah di episode 9 kita membangun form dengan property per field, muncul masalah yang cepat terasa saat aplikasi membesar: form create dan update sering berbagi field yang sama, tapi ditulis duplikat di dua komponen. Mengubah satu aturan validasi berarti mengubah di banyak tempat — dan lupa salah satu tempat akan menghasilkan bug yang membingungkan.
Livewire v4 menjawabnya dengan Form Objects: satu class PHP yang merangkum seluruh state, aturan validasi, dan logika simpan sebuah form. Form Object bisa dipakai ulang di komponen mana pun — dan inilah pola yang disarankan untuk semua form non-trivial di project produksi.
Form Object adalah class yang extends Livewire\Form, berisi semua yang dibutuhkan satu form:
<?php
namespace App\Livewire\Forms;
use Livewire\Form;
class PostForm extends Form
{
public string $title = '';
public string $body = '';
public string $status = 'draft';
public function rules()
{
return [
'title' => ['required', 'min:3', 'max:255'],
'body' => ['required', 'min:10'],
'status' => ['required', 'in:draft,published'],
];
}
}Perhatikan: ini class PHP murni tanpa concern tentang di mana ia dirender. Ia tahu apa isi form dan aturan apa yang berlaku — tidak tahu halaman apa yang memakainya. Itulah kekuatan reuse-nya.
Komponen cukup mendeklarasikan property yang ber-tipe Form Object:
<?php
namespace App\Livewire;
use App\Livewire\Forms\PostForm;
use Livewire\Component;
class CreatePost extends Component
{
public PostForm $form;
public function save()
{
$this->form->validate();
\App\Models\Post::create($this->form->all());
session()->flash('status', 'Post dibuat.');
$this->form->reset();
}
}Sekarang property di view dirujuk dengan prefiks form:
<form wire:submit="save">
<input type="text" wire:model="form.title">
@error('form.title') <span>{{ $message }}</span> @enderror
<textarea wire:model="form.body"></textarea>
@error('form.body') <span>{{ $message }}</span> @enderror
<button type="submit">Simpan</button>
</form>Beberapa hal yang perlu dicatat:
wire:model="form.title" — binding menyentuh property di dalam Form Object.$this->form->validate() — menjalankan aturan rules() dari Form Object.form.title, form.body — nama lengkap termasuk prefix.$this->form->reset() — mengosongkan semua field form sekaligus.Fitur paling berguna untuk CRUD: bind() memindahkan nilai dari model Eloquent ke Form Object — untuk form update:
use App\Models\Post;
public function bind(Post $post)
{
$this->title = $post->title;
$this->body = $post->body;
$this->status = $post->status;
}use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;
class EditPost extends Component
{
public PostForm $form;
public Post $post;
public function mount(Post $post)
{
$this->post = $post;
$this->form->bind($post);
}
public function update()
{
$this->form->validate();
$this->post->update($this->form->all());
session()->flash('status', 'Post diperbarui.');
}
}Sekarang satu Form Object melayani create (CreatePost) dan update (EditPost). Aturan validasi berubah di satu tempat, kedua halaman ikut mengikuti.
Tip
Pola konsistennya: bind() untuk mengisi dari model (update), all() untuk mengambil nilai ke penyimpanan (create/update), dan reset() untuk mengosongkan. Kalau Form Object kalian mulai menyimpan logika halaman (redirect, flash message, query), itu tanda ia melanggar satu tanggung jawab — pindahkan ke komponen.
Untuk konsistensi penuh, logika simpan juga bisa tinggal di Form Object:
use App\Models\Post;
public function store()
{
return Post::create($this->validate());
}
public function update(Post $post)
{
return $post->update($this->validate());
}Perhatikan validate() mengembalikan data yang sudah divalidasi — array field yang lolos aturan. Dengan begini, kode pemanggil jadi sangat pendek:
public function save()
{
$this->form->store();
session()->flash('status', 'Post dibuat.');
$this->form->reset();
}validateOnly juga tersedia di Form Object — pasangan yang rapi untuk feedback per-field:
public function updatedFormTitle($value)
{
$this->form->validateOnly('title');
}Nama hook mengikuti pola updated + nama property lengkap: updatedFormTitle untuk form.title.
form. dihilangkan: wire:model="title" tidak akan menemukan property — harus wire:model="form.title".bind() di mount() untuk create: untuk form create kosong, cukup default property; bind() hanya untuk update.use Livewire\Form;: class harus extends Livewire\Form, bukan Component.@error('form.title') memakai nama lengkap, dan validasi dipanggil ($this->form->validate()).Inti yang harus dibawa pulang:
Livewire\Form yang merangkum state + aturan validasi.public PostForm $form; dan wire:model="form.title".bind() mengisi dari model untuk update; all() untuk data simpan; reset() untuk bersih.validateOnly() tetap tersedia untuk validasi real-time per field.Di episode 11 selanjutnya kita akan membangun CRUD lengkap dengan pagination & query string — create/read/update/delete dengan Eloquent, pagination dengan trait WithPagination, dan #[Url] agar filter serta pencarian bisa di-share lewat URL. Sampai jumpa di episode 11!