Arsitektur sekilas
Bagaimana sebuah request menjadi layar panel: middleware mana yang berjalan, di mana panel saat ini disimpan, registry mana yang menjawab, apa yang diserialisasi, dan apa yang merendernya. Baca halaman ini ketika sebuah layar berperilaku di luar penjelasan panduan lain, atau sebelum menulis apa pun yang menyisip ke jalur request.
Seluruh jalurnya dalam satu contoh
// config/panda-panel.php
'panels' => [App\Panels\Admin\AdminPanelProvider::class],2
namespace App\Panels\Admin\Pages;
use PandaPanel\Pages\Page;
final class Reports extends Page
{
protected static ?string $title = 'Reports';
protected static ?string $navigationIcon = 'layout-grid';
}2
3
4
5
6
7
8
9
10
Kelas itu mendeklarasikan; ia tidak pernah me-routing dirinya sendiri. Page::slug() mengubah nama basis kelas menjadi kebab-case, jadi halaman ini adalah reports, dan Page::render() — yang tidak menerima argumen — dipanggil oleh PanelPageController setelah route mem-binding kelasnya.
php artisan route:list --name=panel.admin.pages
# GET|HEAD admin/reports panel.admin.pages.reports › PandaPanel\Http\Controllers\PanelPageController2
Membuka /admin/reports menjalankan ini:
request
↓ web middleware ResetPanelContext membersihkan panel sebelumnya
↓ grup route panel middleware panel, lalu ResolvePanel:admin
↓ PanelContext panel saat ini, ber-scope request
↓ page / resource page otorisasi → bangun metadata → serialisasi
↓ Inertia props bersama (panel, navigasi) + props halaman
↓ Vue PanelLayout → komponen halaman → renderer2
3
4
5
6
7
Dua namespace di backend
| Namespace | Peran |
|---|---|
PandaPanel\* | isi framework yang dipakai ulang |
App\Panels\* | panel milik aplikasi ini |
Tiga lokasi di frontend
Pemisahan ini tidak opsional, karena @inertiajs/vite hanya mem-glob resources/js/pages/**:
| Lokasi | Peran | Bisa di-resolve Inertia |
|---|---|---|
resources/js/panel/** | layout, komponen, renderer, composable, registry, tipe | tidak |
resources/js/pages/panel/** | halaman generik milik framework | ya |
resources/js/pages/Panels/{Panel}/** | halaman dan komponen khusus aplikasi | ya |
Apa yang dilakukan service provider
PandaPanel\PandaPanelServiceProvider mendaftarkan binding container, lalu mengerjakan delapan hal di boot(), dengan urutan ini:
| Langkah | Yang dilakukannya |
|---|---|
registerPanels() | membangun setiap provider yang disebut di panda-panel.panels, lalu memperingatkan bila manifest sudah basi |
registerMiddleware() | memberi alias middleware panel, dan menambahkan empat middleware web kecuali register_web_middleware bernilai false |
registerGuestRedirect() | mengarahkan Authenticate::redirectUsing() ke PanelLoginRedirect kecuali register_guest_redirect bernilai false |
registerRoutes() | satu grup route per panel, kecuali register_routes bernilai false |
registerIntegrations() | menyambungkan event model untuk setiap resource yang mengaktifkan integrasi |
registerMigrations() | memuat migrasi paket kecuali load_migrations bernilai false |
registerPublishing() | empat tag publish, hanya di konsol |
registerCommands() | tiga belas perintah, plus hook optimize untuk panel:cache / panel:clear |
Binding container:
| Binding | Masa hidup | Alasannya |
|---|---|---|
PandaPanel\Core\PanelRegistry | singleton | registrasi hanya terjadi sekali |
PandaPanel\Support\PanelContext | scoped | panel satu request tidak boleh bocor ke request berikutnya |
PandaPanel\Discovery\PanelDiscoverer | singleton | tanpa state |
PandaPanel\Cache\PanelManifest | singleton | membaca satu berkas |
PandaPanel\Core\PanelManager | singleton | menyimpan registry per panel |
PandaPanel\Support\NavigationBuilder | singleton | membangun navigasi per pemanggilan, tidak menyimpan apa pun |
PandaPanel\Routing\PanelRouteRegistrar | singleton | berjalan sekali saat boot |
Panel provider yang terdaftar di config tetapi kelasnya sudah tidak bisa di-resolve akan dilewati, bukan membuat boot gagal total, sehingga kelas yang diganti nama tidak membuat aplikasi tak bisa diakses.
Middleware web
Empat bagian, ditambahkan ke seluruh grup web dengan urutan ini:
| Middleware | Kenapa di web, bukan di grup panel |
|---|---|
ResetPanelContext | harus berjalan untuk request yang tidak pernah mencapai panel, kalau tidak route non-panel akan menyimpan sisa dari request sebelumnya |
RedirectPanelHome | request yang dijawabnya tidak pernah mencapai layar panel, jadi tidak ada props yang perlu dibagikan |
ShareFlashToast | ia berjalan pada redirect yang keluar dari panel |
SharePanelData | props baru di versi berikutnya akan merusak setiap aplikasi yang tidak menyalinnya ke HandleInertiaRequests miliknya sendiri |
Keempatnya ditambahkan lewat HTTP kernel, di dalam hook afterResolving, bukan didorong ke router. bootstrap/app.php mengonfigurasi grup itu di hook afterResolving miliknya sendiri lalu menimpa apa pun yang sedang dipegang router, jadi hook yang berjalan belakangan adalah satu-satunya urutan yang bertahan.
Setel register_web_middleware ke false untuk mendaftarkannya sendiri di posisi yang Anda pilih.
Middleware grup route panel
Setiap grup route panel membawa, berurutan:
getMiddleware()milik panel itu sendiri —['web']kecuali diganti dengan->middleware([...])ResolvePanel:{id}RequireTwoFactor:{id}RequireEmailCode:{id}ResolveTenant:{id}, hanya bila panelnya mendeklarasikan tenancy
ResolvePanel berjalan setelah auth, jadi $request->user() sudah terisi sebelum canAccess dievaluasi. Pengguna yang sudah login tetapi gagal melewatinya mendapat 403, tidak pernah redirect: menyembunyikan navigasi bukan kontrol akses. Callback boot berjalan setelah pemeriksaan itu, jadi pengguna yang ditolak masuk panel tidak pernah memicu pekerjaan boot panel tersebut.
Alias tersedia untuk aplikasi yang ingin menyebutnya di definisi route sendiri. Registrar menyebut kelasnya secara langsung, jadi alias ini tidak pernah menjadi jalan framework sendiri untuk menjangkaunya:
| Alias | Kelas |
|---|---|
panel | PandaPanel\Http\Middleware\ResolvePanel |
panel.two-factor | PandaPanel\Http\Middleware\RequireTwoFactor |
panel.email-code | PandaPanel\Http\Middleware\RequireEmailCode |
panel.parent | PandaPanel\Http\Middleware\ResolveParentRecord |
Panel saat ini
PandaPanel\Support\PanelContext adalah binding container ber-scope — direset di awal setiap request oleh ResetPanelContext. Tidak ada bagian dari panel saat ini yang bersifat statis, dan itulah yang membuat invarian "tidak ada panel saat ini di luar panel" tetap benar di bawah Octane dan di dalam test yang mengirim beberapa request.
namespace PandaPanel\Support;
final class PanelContext
{
public function setPanel(?Panel $panel): void;
public function panel(): ?Panel;
public function hasPanel(): bool;
public function set(string $key, mixed $value): void;
public function get(string $key, mixed $default = null): mixed;
public function forget(): void;
}2
3
4
5
6
7
8
9
10
11
Bacalah lewat helper-nya, bukan lewat kelasnya:
panel(); // Panel|null
panel('admin'); // Panel, melempar PanelRegistrationException bila tidak dikenal2
Controller halaman yang dipanggil langsung di dalam test membutuhkan konteks yang biasanya disetel ResolvePanel:
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->setCurrentPanel(panel('admin'));2
3
Registry
PandaPanel\Core\PanelManager menyimpan empat registry per panel, dibangun sekali saat registrasi:
use PandaPanel\Facades\PandaPanel;
PandaPanel::resources($panel); // ResourceRegistry — di-key berdasarkan slug efektif
PandaPanel::pages($panel); // PageRegistry — divalidasi terhadap slug resource
PandaPanel::widgets($panel); // WidgetRegistry — di-key berdasarkan id widget
PandaPanel::navigation($panel); // NavigationRegistry — urutan grup dan sifat collapsible-nya2
3
4
5
6
Registrasi eksplisit dan discovery digabungkan. Kelas yang disebut di keduanya hanya muncul sekali, karena registry-nya di-key berdasarkan slug dan id. ResourceConfiguration didaftarkan lebih dulu, sehingga kelas yang sama tidak bisa sekaligus didaftarkan polos dan mengklaim slug default-nya.
ResourceRegistry::slugFor($resource) adalah yang ditanyakan saat pendaftaran route, karena selama boot belum ada panel saat ini untuk ditanyai kelasnya sendiri. Resource::slug() menjawab untuk panel saat ini dan jatuh kembali ke defaultSlug() di luar panel.
PanelRegistry menolak dua bentuk ambiguitas saat registrasi, keduanya dengan melempar PandaPanel\Exceptions\PanelRegistrationException: id panel ganda, dan pasangan path/domain ganda. Keduanya kalau dibiarkan akan muncul sebagai satu route yang diam-diam menutupi route lain.
Route
Satu grup per panel, diberi prefix path-nya, dinamai panel.{id}.. Setiap route menunjuk ke metode controller, tidak pernah ke closure, sehingga php artisan route:cache tetap berfungsi.
| Nama route | Verb dan path | Controller |
|---|---|---|
dashboard | GET / | PanelDashboardController |
search | GET search | PanelSearchController |
options | GET options | PanelFormOptionsController |
uploads | POST uploads | PanelUploadController |
form-state | POST form-state | PanelFormStateController |
export-file | GET exports/{file} | PanelExportController |
import-file | GET imports/{file} | PanelImportController |
notifications.index / .read / .clear | GET, POST read, POST clear | PanelNotificationController |
auth.two-factor.challenge / .send / .verify / .enable / .disable | di bawah two-factor | PanelTwoFactorController |
actions.record / .bulk / .reorder / .cell / .table / .infolist | POST actions/* | PanelActionController |
actions.form / .submit | GET/POST actions/form | PanelActionFormController |
relations.form / .save / .action / .bulk | di bawah relations | PanelRelationController |
pages.{slug} | GET di path halaman itu sendiri | PanelPageController |
resources.{slug}.index / .create / .store / .view / .edit / .update | di bawah prefix resource | kelas halamannya |
resources.{slug}.validateCreateStep / .validateEditStep | POST .../step | kelas halamannya |
resources.{slug}.integrations (+ .store, .update, .destroy, .send, .rotate) | di bawah integrations | PanelIntegrationController |
Panel dengan ->login() juga mendaftarkan route tamu — auth.login, serta auth.register, auth.password.request, auth.password.reset, auth.verification.notice bila fitur terkait aktif — di luar tumpukan auth panel tetapi di dalam middleware dasarnya. Mengirim orang yang belum bisa masuk ke halaman yang menyuruhnya masuk adalah lingkaran tanpa ujung.
Halaman resource di-routing sebagai [Page::class, 'render'] untuk GET dan [Page::class, 'handle'] untuk verb tulis, jadi halaman adalah controller sungguhan. Dua resource yang mengklaim bentuk path yang sama akan melempar exception saat boot alih-alih membuat salah satunya tak terjangkau: nama parameter dihapus sebelum dibandingkan, karena {record} dan {parentRecord} adalah wildcard yang sama di mata router.
Props bersama
SharePanelData membagikan tujuh props lewat Inertia::share(), yang sifatnya menggabungkan — HandleInertiaRequests milik aplikasi tidak disentuh. Setiap nilainya berupa closure, jadi request yang tidak pernah mencapai panel tidak membayar satu pun dari semuanya, dan tidak ada satu pun di sini yang di-cache.
| Prop | Bentuknya |
|---|---|
panel | definisi panel, atau null di luar panel |
navigation | grup sidebar untuk panel dan URL ini |
panels | panel yang boleh dimasuki pengguna ini, untuk pemindah panel |
broadcasting | {enabled, channel} |
search | {enabled, url, debounce, keyBindings} |
notifications | {enabled, indexUrl, readUrl, clearUrl, unread} |
tenancy | {current, available}, atau null untuk panel tanpa tenancy |
Panel::toSharedArray() menentukan apa yang dikatakan sebuah panel tentang dirinya. Transaksi, otorisasi ketat, path discovery, middleware, dan callback boot tetap di server; hanya pengaturan yang ditindaklanjuti frontend yang menyeberang.
Discovery dan manifest
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'))2
3
PandaPanel\Discovery\PanelDiscoverer mengubah path berkas menjadi nama kelas lewat prefix PSR-4 yang terdaftar di Composer. Ia tidak mem-parsing maupun mengeksekusi kode sumber — autoloader sudah tahu apa yang dideklarasikan sebuah berkas. Hanya kelas konkret yang mengimplementasikan kontrak yang diharapkan (ResourceContract, PageContract, WidgetContract) yang disertakan, dan hasilnya diurutkan sehingga dua mesin menghasilkan manifest yang sama.
php artisan panel:cache
php artisan panel:clear2
PandaPanel\Cache\PanelManifest::path() adalah bootstrapPath('cache/panels.php') — bersebelahan dengan cache config, route, dan event, sehingga optimize:clear menemukannya. Isinya hanya nama kelas:
return array (
'admin' =>
array (
'resources' => array ( 0 => 'App\\Panels\\Admin\\Resources\\Users\\UserResource' ),
'pages' => array ( 0 => 'App\\Panels\\Admin\\Pages\\Settings' ),
'widgets' => array ( /* ... */ ),
),
);2
3
4
5
6
7
8
Ketika manifest ada, discovery sama sekali tidak berjalan. Yang tidak pernah di-cache: hasil otorisasi, status aktif navigasi, nilai badge, data record, data widget. Semuanya bergantung pada pengguna atau URL saat ini, jadi men-cache-nya berarti menyajikan jawaban milik satu orang kepada orang lain.
Batas yang berlaku di mana-mana
- Hanya metadata. Skema diserialisasi menjadi skalar dan array. Closure dievaluasi di server dan hanya hasilnya yang dikirim.
- Otorisasi ada di sisi server. Menyembunyikan tombol atau item navigasi hanyalah kenyamanan. Route, action, page, dan widget masing-masing melakukan otorisasi sendiri.
- Satu query.
Resource::query()adalah satu-satunya sumber untuk list, view, edit, update, delete, bulk, dan pencarian action. - URL adalah state tabel. Halaman, jumlah per halaman, pencarian, sorting, arah, dan filter hidup di query string, sehingga back, forward, refresh, dan bookmark berperilaku benar.
- Tidak ada yang dinamis dari request. Ikon dan komponen kustom di-resolve lewat registry saat build; nama yang tidak terdaftar tidak merender apa pun alih-alih diambil dari suatu tempat.
- Cache nama kelas, jangan pernah cache jawaban.
Hal yang mudah terlewat
ResolvePanelmenerima id panel sebagai parameter middleware alih-alih mencocokkan path. Karena itu penentuan panel tidak pernah bergantung pada pencocokan path, sehingga dua panel yang berbagi prefix tetap tidak ambigu.PanelManager::resolveFromRequest()tersedia untuk kode di luar route panel, dan mencocokkan path terpanjang lebih dulu.- Mematikan
register_web_middlewareikut menghapusResetPanelContext. Di bawah Octane, itulah yang tidak boleh dilewatkan. - Transaksi di-resolve dari yang paling spesifik:
->databaseTransaction(bool)milik action, lalu$hasDatabaseTransactionsmilik halaman, lalu panel, lalu seterusnya.nulldi level mana pun berarti "belum memutuskan", bukan "mati". Di luar panel jawabannya menyala.DeleteBulkActionselalu transaksional apa pun kata panelnya. - Render hook disaring di Vue, bukan di server. Props bersama dibangun di middleware, sebelum request mencapai halaman, jadi shell tahu halaman apa yang sedang dirender sementara middleware tidak.
- Pekerjaan berantre berada di luar request dan karenanya di luar
PanelContext. Job milik paket ini membawa id panel dan me-resolve-nya kembali dihandle(); pekerjaan ber-scope tenant harus masuk ke tenant secara eksplisit denganTenancy::for().
Baca juga
- Pendekatan Inertia dan Vue — separuh lain dari jalur ini
- Siklus Hidup Request dan Konteks Panel
- Routing, Discovery, Caching
- Metadata Server ke Vue
- Konfigurasi Middleware
- Ikhtisar fitur — apa yang disimpan registry-registry ini
- Batasan dan trade-off