Custom Page di Filament untuk Kebutuhan Non-CRUD
Kalau sudah cukup lama bekerja dengan Filament, cepat atau lambat akan ketemu kebutuhan yang terasa janggal kalau dipaksakan jadi Resource. Resource di Filament pada dasarnya dibangun di atas asumsi CRUD: ada model, ada tabel, ada baris data yang bisa dibuat, diubah, dihapus. Tapi tidak semua layar di aplikasi sebenarnya seperti itu. Laporan bulanan, dashboard ringkasan, halaman pengaturan global aplikasi — semua itu tidak punya "baris data" yang natural untuk di-CRUD-kan. Kalau tetap dipaksakan jadi Resource, biasanya hasilnya adalah hack: bikin model dummy, bikin tabel yang isinya cuma satu baris, atau menumpuk action custom di atas ListRecords yang sebenarnya tidak pernah dipakai untuk listing sungguhan. Di titik itulah Custom Page jadi pilihan yang jauh lebih masuk akal.
Kapan sebaiknya pindah ke Custom Page
Pertanyaan sederhana yang saya pakai: apakah layar ini merepresentasikan koleksi record yang bisa ditambah dan dihapus satu-satu? Kalau ya, itu wilayah Resource. Kalau layar itu sebenarnya lebih mirip "satu bentuk tampilan atau satu aksi" — misalnya laporan yang mengagregasi data dari beberapa tabel, form pengaturan yang menyimpan ke satu baris konfigurasi, atau tombol pemicu proses (generate laporan, sinkronisasi data) — itu kandidat kuat untuk Custom Page. Tanda lain yang sering saya pakai: kalau saya sampai menulis kode untuk menyembunyikan tombol create/edit/delete bawaan Resource karena memang tidak relevan, itu sinyal bahwa fondasinya sudah salah sejak awal, bukan soal kurang konfigurasi.
Struktur dasar Custom Page
Secara teknis, Custom Page di Filament adalah kelas Livewire biasa yang didaftarkan ke panel, lengkap dengan view Blade sendiri. Bedanya dengan Resource, kita bebas menentukan apa yang terjadi di dalamnya — tidak terikat pada Table Builder atau Form Builder yang mengarah ke satu model. Contoh kerangka sederhana untuk halaman laporan:
class LaporanKunjungan extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-chart-bar';
protected static string $view = 'filament.pages.laporan-kunjungan';
public $tanggalMulai;
public $tanggalSelesai;
public function mount(): void
{
$this->tanggalMulai = now()->startOfMonth();
$this->tanggalSelesai = now();
}
public function getData(): array
{
// agregasi dari beberapa tabel, bukan satu model
return [];
}
}Kita tetap bisa memanfaatkan komponen Filament seperti Form Builder untuk filter tanggal, atau Infolist untuk menampilkan ringkasan, tapi keduanya dipakai sebagai alat bantu, bukan sebagai kontrak CRUD yang mengikat.
Untuk pengaturan global, pertimbangkan settings page
Kasus lain yang sering muncul: pengaturan aplikasi seperti nama instansi, logo, atau konfigurasi integrasi. Ini secara teknis memang tersimpan di satu baris tabel, tapi dari sisi pengalaman pengguna, ini bukan "daftar record" — ini satu form konfigurasi. Custom Page cocok untuk ini, biasanya dikombinasikan dengan Form Builder yang di-mount langsung ke satu baris data (baik lewat model settings sederhana atau paket manajemen setting), lalu action simpan yang meng-update baris itu. Pendekatan ini membuat niat layar jelas terbaca di kode: ini pengaturan, bukan entitas yang bisa diduplikasi atau dihapus satu-satu.
Yang perlu diwaspadai
Custom Page memang memberi kebebasan lebih, tapi kebebasan itu juga berarti tanggung jawab lebih. Otorisasi (policy atau can check), validasi input, dan penanganan state harus ditangani manual karena tidak lagi otomatis dari resource lifecycle. Saya juga membiasakan diri untuk tidak berlebihan sebaliknya — kalau ternyata kebutuhannya berkembang jadi butuh listing dan filter data yang kompleks, itu tanda untuk pertimbangkan balik ke Resource, atau kombinasi keduanya dalam satu panel.
Intinya
Aturan praktis yang saya pegang: Resource untuk data yang berbentuk koleksi record, Custom Page untuk tampilan, laporan, atau aksi yang tidak alami sebagai CRUD. Memilih yang tepat sejak awal jauh lebih murah daripada memaksakan struktur yang salah lalu menambal dengan banyak override di kemudian hari.