Keputusan Arsitektur
Package ini memiliki satu Architecture Decision Record (ADR) yang sudah diterima, ADR 001, dan dokumen tersebut menjelaskan mengapa sebagian besar framework dirancang seperti sekarang. Baca ringkasan ini sebelum mengusulkan perubahan pada batas antara PHP dan Vue, mekanisme discovery, caching, atau cara setiap Panel diisolasi. Hal-hal tersebut sudah diputuskan dengan alasan tertulis; pull request yang membuka kembali keputusan tersebut tanpa membahas alasannya akan dikembalikan untuk diperbaiki.
Contoh minimal
Format header ADR sangat sederhana:
# ADR 001 — A panel framework on Laravel, Inertia, and Vue
- **Status:** accepted
- **Date:** 2026-08-14
- **Supersedes:** nothing2
3
4
5
Setelah tiga field tersebut, ADR berisi Context, Decision, alternatif yang ditolak beserta alasannya, trade-off yang diterima, dan keputusan-keputusan yang dicatat selama implementasi.
Apa yang diputuskan oleh ADR 001
Ada sembilan keputusan utama. Masing-masing menjelaskan bagian framework yang mungkin terlihat mengejutkan jika dilihat tanpa konteks.
Dua namespace. PandaPanel\* adalah framework; App\Panels\* adalah Panel milik application. PHP bertanggung jawab atas registration, routing, authorization, query composition, validation, actions, dan serialization. Vue bertanggung jawab atas rendering dan interaction. Inertia adalah satu-satunya bridge, dan tidak ada SPA API terpisah.
Tidak menggunakan Filament, dan tidak menyalin Filament. Filament merender melalui Livewire, sedangkan frontend ini menggunakan Vue, shadcn-vue, dan strict TypeScript. Menggunakan keduanya akan menghasilkan dua model component, dua model state, dan dua build story dalam satu application. Menyalin source Filament juga ditolak secara terpisah: yang diambil adalah bentuk API-nya — Panel, Resource, Table, Form, Action — bukan implementasi yang dibangun berdasarkan lifecycle Livewire. Yang dipinjam adalah vocabulary, fluent schema builder, pemisahan Resource Page, dan model discovery. Tidak ada source code Filament yang disalin.
Kontrak serialization, bukan object graph. Class PHP mendeskripsikan column, filter, field, layout, action, widget, dan navigation sebagai scalar serta array. Vue merender deskripsi tersebut melalui discriminated union dengan exhaustive checks, sehingga tipe PHP yang tidak memiliki renderer menjadi compile error, bukan cell kosong. Closure hanya hidup di server; closure badge atau predicate visible() dievaluasi saat serialization, lalu hanya hasilnya yang dikirim. Value yang masuk ke Vue divalidasi, bukan sekadar di-assert — guard function mempersempit tipe sehingga shape yang tidak cocok turun menjadi cell kosong, bukan exception di dalam table.
Resource::query() adalah satu-satunya entry point. Semua record yang dapat dijangkau Resource — list, view, edit, update, delete, bulk, dan action lookup — melewati method ini. Karena itu satu override dapat menerapkan tenant, module, atau permission scope ke seluruh operasi. Page yang melakukan query langsung ke model adalah bug, dan ada test yang memastikan aturan ini tetap berlaku.
Page adalah controller yang nyata. Resource Page diregistrasikan sebagai [Page::class, 'render'] untuk GET dan [Page::class, 'handle'] untuk write verb. Dengan demikian seluruh Panel route tetap kompatibel dengan route:cache.
Isolasi Panel bersifat struktural. Setiap Panel memiliki registry Resource, Page, Widget, dan Navigation sendiri. Route diregistrasikan per Panel, Resource::url() melempar exception jika diminta membuat URL untuk Panel yang tidak mendaftarkan Resource tersebut, dan action endpoint me-resolve Resource berdasarkan registry Panel itu sendiri. Jadi session yang valid pada satu Panel tidak dapat digunakan untuk memanggil Resource milik Panel lain melalui endpoint tersebut. Ini adalah titik paling masuk akal untuk cross-panel request, dan ada test yang secara eksplisit melindunginya.
Panel didaftarkan secara eksplisit, isi Panel ditemukan melalui discovery. Jumlah Panel harus terlihat jelas di satu tempat, sedangkan urutan eksekusi Panel distabilkan berdasarkan id. Path file diubah menjadi class name melalui PSR-4 prefix yang didaftarkan Composer; discovery tidak mem-parse atau mengevaluasi source karena autoloader sudah mengetahui class yang dideklarasikan. Hanya concrete class yang mengimplementasikan contract yang sesuai yang dimasukkan, dan hasilnya diurutkan agar dua mesin menghasilkan manifest yang sama.
Cache hanya menyimpan nama class. php artisan panel:cache menulis bootstrap/cache/panels.php secara atomik. Yang tidak pernah di-cache: authorization result, navigation active state, badge value, record data, dan widget data. Semua hal tersebut bergantung pada current user atau URL; menyimpannya di cache dapat membuat jawaban milik satu user diberikan kepada user lain. Test memastikan manifest tidak berisi closure. Jika manifest tersedia, discovery tidak dijalankan sama sekali; test membuktikannya dengan mengarahkan Panel ke directory yang tidak ada lalu memastikan class tetap dapat di-resolve.
Security mengikuti boundary tersebut. ADR menjelaskan implikasinya sebagai daftar, dan negative test suite menyatakan setiap implikasi sebagai sesuatu yang tidak boleh terjadi. Lihat Security.
Trade-off juga merupakan keputusan
Trade-off dicatat agar tidak diperdebatkan kembali seolah-olah merupakan bug:
| Yang diterima | Biaya / konsekuensi |
|---|---|
| Server round trip untuk setiap interaksi table | Lebih lambat daripada client-side table; sebagai gantinya URL menjadi state dan tidak ada client store duplikat |
| Metadata PHP + renderer Vue | Menambahkan column type baru perlu mengubah dua sisi; sebagai gantinya boundary eksplisit dan type-checked di kedua sisi |
| Explicit dibanding magic | Lebih verbose daripada convention Filament pada beberapa bagian, misalnya getId() dibanding accessor gabungan |
| SVG chart tanpa dependency | Tidak ada tooltip, zoom, atau animation; sebagai gantinya tidak perlu charting library dan widget union tetap lengkap |
| Panel didaftarkan manual | Satu edit untuk setiap Panel baru; sebagai gantinya daftar Panel terlihat jelas |
| Tidak ada browser test runner | Interaction sisi client hanya dijamin melalui types, build, dan server-side request tests |
"Tidak ada browser test runner" adalah keputusan, bukan kekurangan yang belum dikerjakan. Proposal untuk menambahkannya berarti membalik trade-off yang sudah dicatat dan membutuhkan ADR.
Tabel keputusan
ADR diakhiri dengan delapan belas keputusan implementasi yang lebih kecil, D1 sampai D18. Keputusan ini mengubah API tanpa mengubah arsitektur utama:
| # | Keputusan |
|---|---|
| D1 | Install @tanstack/vue-table dan enam component shadcn |
| D2 | Shared props adalah panel dan navigation |
| D3 | panel/ untuk generic page, Panels/{Panel}/ untuk application page |
| D4 | Tambahkan is_admin ke users |
| D5 | Gunakan SVG chart tanpa dependency, bukan charting library |
| D6 | Status user berasal dari email_verified_at; tidak ada column status |
| D7 | Laravel Precognition dievaluasi lalu ditolak |
| D8 | ->with('success', ...) dipetakan ke single toast bridge yang sudah ada |
| D9 | Fluent setter tetap memakai nama sederhana; reader memakai prefix get |
| D10 | Panel access menggunakan satu mekanisme: ->canAccess(Closure) |
| D11 | NavigationBuilder tidak menerima argument user karena tidak dapat menjaminnya secara benar |
| D12 | is_admin dikirim pada phase 2 dan memiliki default pada model maupun database |
| D13 | Shared props ditipkan melalui InertiaConfig.sharedPageProps |
| D14 | Panel::sidebar(variant:), karena tanpa ini header shell tidak pernah dapat digunakan |
| D15 | Page tidak mengatur layout sendiri; usePanelPage() membaca dan memvalidasi metadata prop |
| D16 | Routing render/handle, dengan route name store dan update |
| D17 | Delete hooks berada pada Action, bukan page trait, karena endpoint berjalan tanpa Page instance |
| D18 | Field::dehydrateTo() memetakan field ke attribute yang berbeda |
Tiga keputusan sebenarnya memperbaiki kesalahan desain sebelumnya: D17 menggantikan dua hook terdokumentasi yang tidak mungkin terpanggil, D14 menggantikan shell yang tidak pernah dapat dijangkau, dan pekerjaan phase 8 menggantikan PanelContext yang bocor antar-request. Mencatat koreksi lebih berguna daripada memperbaikinya secara diam-diam, karena developer berikutnya dapat memahami mengapa pendekatan lama tidak berhasil.
Perubahan dengan ukuran seperti ini — keputusan API yang memiliki alasan penting tetapi tidak mengubah arsitektur — cukup ditambahkan sebagai row baru, bukan ADR baru.
Kapan perubahan membutuhkan ADR baru
Buat ADR baru jika perubahan membuat salah satu kalimat ADR 001 menjadi tidak benar atau membalik salah satu trade-off-nya.
| Perubahan | Perlu ADR? |
|---|---|
| Mengirim sesuatu selain scalar dan array ke Vue | Ya — ini keputusan inti |
| Menambah bridge kedua selain Inertia, misalnya JSON API untuk Panel | Ya |
Menambahkan runtime dependency ke composer.json atau dependencies di package.json | Ya — D1 menjadi precedent |
| Men-cache sesuatu yang bergantung pada user atau URL di Panel manifest | Ya |
| Mengubah cara Panel diisolasi atau cara action endpoint me-resolve Resource | Ya |
| Discovery membaca/mengevaluasi source daripada memakai PSR-4 | Ya |
| Client-side routing di dalam Panel | Ya — membalik trade-off yang dicatat |
| Browser test runner | Ya — alasan yang sama |
| Mengimplementasikan salah satu future extension yang sudah dicatat | Tidak — seam-nya sudah diputuskan |
| Column, field, atau widget type baru | Tidak |
| Fluent setter baru yang mengikuti D9 | Tidak |
| Bug fix, sebesar apa pun | Tidak |
Extension yang sudah diperkirakan ADR 001 beserta seam-nya:
| Extension | Seam |
|---|---|
| Relation managers | Resource::pages() + array relations() |
| Clusters | Group ordering pada NavigationRegistry + prefix level Panel |
| Global search | GlobalSearchProvider dan Resource::globalSearchable() |
| Notifications | Flash toast bridge + shared prop slot |
| Import dan export | Header actions + action endpoint yang sudah ada |
| Wizard forms, tabs, fieldsets | Layout component pada FormSchema |
| Infolists | Display serializer pada ViewRecord |
| Kanban, calendar | Alternative page class yang didaftarkan melalui pages() |
| Tenant panels | Context tambahan pada PanelContext + satu scope point di Resource::query() |
| Modules | discover*() dan navigationGroups() bersifat accumulating, bukan overwrite |
Beberapa extension tersebut kini sudah dibangun — relation manager, cluster, global search, notification, import/export, wizard, infolist, dan tenancy — tanpa membutuhkan ADR baru karena semuanya menggunakan seam yang sudah dicatat. Module seam juga diuji secara sengaja: test mendaftarkan dua discovery path pada satu Panel dan memastikan keduanya tetap tersedia, karena implementasi single-path akan menjadi jalan buntu bagi module system.
Menulis ADR baru
Tambahkan record di samping ADR pertama pada area dokumentasi internal repository.
Nomori secara berurutan, beri nama file berdasarkan keputusannya, lalu gunakan tiga field yang sama:
# ADR 002 — A title that states the decision, not the topic
- **Status:** proposed
- **Date:** 2026-08-16
- **Supersedes:** nothing2
3
4
5
Status bernilai proposed sampai perubahan di-merge, kemudian menjadi accepted. Jika ADR menggantikan keputusan sebelumnya, isi Supersedes dengan nomor ADR tersebut dan ubah status ADR lama menjadi superseded by 002. ADR tidak dihapus atau diedit menjadi keputusan baru, karena fungsi utamanya adalah menunjukkan apa yang diyakini dan diputuskan pada saat itu.
Gunakan section yang sama seperti ADR 001, dengan urutan:
- Context — kondisi dan constraint saat pertanyaan muncul, bukan kesimpulan.
- Decision — satu atau dua paragraf dalam present tense.
- Why not X — alternatif yang dipertimbangkan, disebutkan namanya, beserta alasan ditolak. Bagian ini biasanya paling berguna untuk pembaca.
- Implications — konsekuensi yang mengikuti secara mekanis, termasuk security.
- Trade-offs — tabel tentang apa yang diterima dan biayanya. Keputusan yang seolah tidak memiliki biaya biasanya belum dianalisis cukup dalam.
- Known gaps — ditulis eksplisit, bukan disembunyikan. ADR 001 misalnya menyebut
Select::relationship()sudah diimplementasikan tetapi belum memiliki feature test, serta middlewareverifiedtidak aktif secara efektif karena exampleUsertidak mengimplementasikanMustVerifyEmail.
Link ADR baru dari pull request yang mengimplementasikannya. Tambahkan entry Changed pada CHANGELOG.md jika keputusan tersebut mengubah sesuatu yang terlihat oleh application.
Catatan
- Source ADR yang diarsipkan berada di luar public docs. File tersebut adalah planning document repository, bukan referensi package yang diinstall.
- Source file adalah authority. Dokumen planning internal menjelaskan design intent; jika berbeda dengan command name, method signature, atau config key di source, source yang menang.
- Known gap bukan bug report. Dua gap yang disebut ADR 001 memang sengaja dicatat. Menutup gap tersebut adalah perubahan normal dan tidak membutuhkan ADR baru.
Supersedes: nothingadalah value yang nyata. Tulis secara eksplisit agar pembaca tahu field tersebut memang dipertimbangkan.- Tidak ada enforcement otomatis. Tidak ada test yang gagal jika ADR tidak dibuat. Ini adalah pertanyaan saat review, karena itu kriterianya ditulis eksplisit di atas.
Lihat juga
- Coding standards — convention yang dihasilkan oleh D9 dan serialization boundary
- Pull requests — tempat pertanyaan tentang ADR diperiksa
- Security — implikasi security ADR yang diwujudkan sebagai test
- Releases — cara mencatat keputusan yang mengubah behavior application
- Architecture at a glance
- Package limits and trade-offs
- Comparison with Filament concepts
- Server metadata to Vue
- Discovery dan Caching
- Multi-panel applications