Request Lifecycle
Setiap screen PandaBear pada dasarnya adalah request Laravel biasa yang melewati urutan middleware yang tetap. Setelah itu Page class melakukan authorization, membangun metadata, lalu Inertia menserialisasi response untuk Vue.
Dokumentasi ini mengikuti lifecycle tersebut secara berurutan, menyebut class yang terlibat, serta menjelaskan asumsi apa yang boleh dan tidak boleh dibuat oleh setiap layer.
Gunakan dokumentasi ini ketika request menghasilkan 403 atau 404 dan Anda perlu mengetahui layer mana yang sebenarnya memberikan response tersebut.
Bentuk Sebuah Request
request
↓ web middleware ResetPanelContext clears any previous panel
↓ panel route group panel middleware, then ResolvePanel:{id}
↓ PanelContext the current panel, request-scoped
↓ page or resource page authorize → build metadata → serialize
↓ Inertia shared props (panel, navigation) + page props
↓ Vue PanelLayout → page component → renderers2
3
4
5
6
7
Stack middleware nyata untuk route tertentu dapat dilihat melalui:
php artisan route:list --path=adminatau langsung dari router:
use Illuminate\Support\Facades\Route;
Route::getRoutes()->getByName('panel.admin.dashboard')?->gatherMiddleware();
// ['web', 'auth', 'verified', 'PandaPanel\Http\Middleware\ResolvePanel:admin', ...]2
3
4
Tahap 1 — Web Group
PandaPanelServiceProvider menambahkan empat middleware ke group web dengan urutan berikut, kecuali panda-panel.register_web_middleware bernilai false:
| Class | Berjalan untuk | Tujuan |
|---|---|---|
PandaPanel\Http\Middleware\ResetPanelContext | setiap request web | Membersihkan request-scoped current Panel |
PandaPanel\Http\Middleware\RedirectPanelHome | setiap request web | Mengalihkan /dashboard ke Panel pertama yang dapat diakses |
PandaPanel\Http\Middleware\ShareFlashToast | setiap request web | Mengubah Laravel flash key menjadi toast |
PandaPanel\Http\Middleware\SharePanelData | setiap request web | Membagikan tujuh shared prop PandaBear ke Inertia |
Middleware tersebut sengaja ditempatkan pada group web, bukan hanya Panel group.
ResetPanelContext harus tetap berjalan untuk request yang tidak pernah memasuki Panel.
ShareFlashToast juga harus berjalan untuk redirect yang keluar dari Panel menuju Page application lain.
ResetPanelContext
public function handle(Request $request, Closure $next): Response
{
$this->context->forget();
return $next($request);
}2
3
4
5
6
Pada lifecycle PHP klasik, container dibangun ulang setiap request sehingga kebocoran state antar-request sulit terlihat.
Di bawah Octane, atau dalam test yang mengirim beberapa request dalam process yang sama, state dapat bertahan jika tidak dibersihkan.
Middleware ini menegakkan invariant:
di luar Panel route, tidak ada current Panel.
Behavior tersebut benar di semua environment karena desain, bukan kebetulan.
RedirectPanelHome
Hanya bekerja pada request GET, dan tidak berjalan ketika request expectsJson().
Middleware memanggil:
PandaPanel\Support\PanelHomeRedirect::for($request)Method tersebut mengembalikan null dan tidak mengubah request ketika:
- URL tidak cocok dengan configured redirect path;
- request berasal dari guest;
- user tidak memiliki Panel yang dapat diakses;
- request sebenarnya sudah berada di dalam Panel.
Jika tujuan ditemukan, middleware memberikan 302 ke dashboard Panel tersebut.
// config/panda-panel.php
'home_redirect' => [
'enabled' => true,
'paths' => ['dashboard'],
],2
3
4
5
paths menggunakan pattern Request::is(). Karena itu pattern seperti:
reports/*dapat menyerahkan seluruh section kepada Panel.
ShareFlashToast
Middleware membaca session key berikut secara berurutan:
error
warning
success
info2
3
4
Value non-empty pertama diubah menjadi:
flash.toastJika application sudah menetapkan:
Inertia::flash('toast', …)secara eksplisit, value tersebut tidak akan ditimpa.
Request tanpa session dilewati.
SharePanelData
Middleware ini membagikan tujuh shared prop menggunakan Inertia::share(). Karena Inertia::share() melakukan merge, middleware HandleInertiaRequests milik application tidak diganti.
| Prop | Value di Luar Panel |
|---|---|
panel | null |
navigation | [] |
panels | [] |
broadcasting | ['enabled' => false, 'channel' => null] |
search | ['enabled' => false, 'url' => null, 'debounce' => 300, 'keyBindings' => []] |
notifications | ['enabled' => false, …, 'unread' => 0] |
tenancy | null |
Semua value disediakan melalui closure. Request yang tidak masuk ke Panel tidak menghitung data tersebut.
Tidak ada data user-specific di sini yang di-cache. Visibility, badge count, active state, dan unread count semuanya dihitung per-user dan per-URL.
Lihat Metadata Server ke Vue.
Ada satu ordering constraint penting: shared props dibangun di middleware sebelum request mencapai Page.
Artinya middleware mengetahui current Panel, tetapi belum mengetahui Page final yang akan dirender. Karena itu filtering scope Render Hook dilakukan di Vue, setelah Page metadata sudah tersedia.
Tahap 2 — Panel Route Group
PandaPanel\Routing\PanelRouteRegistrar::register() membuat satu route group untuk setiap Panel:
$attributes = [
'prefix' => $panel->getPath(),
'as' => $panel->getRouteNamePrefix(),
'middleware' => [
...$panel->getMiddleware(), // base + auth stacks
ResolvePanel::class.':'.$panel->getId(),
RequireTwoFactor::class.':'.$panel->getId(),
RequireEmailCode::class.':'.$panel->getId(),
...($panel->hasTenancy() ? [ResolveTenant::class.':'.$panel->getId()] : []),
],
];2
3
4
5
6
7
8
9
10
11
Panel id dikirim sebagai parameter middleware, bukan di-infer kembali dari URL.
Panel mana yang memiliki sebuah route adalah fakta registration. Jika current Panel ditentukan dari URL runtime, request berpotensi memilih context yang seharusnya sudah ditetapkan saat route dibuat.
ResolvePanel
Middleware ini berjalan setelah bagian authentication stack seperti auth dan verified, sehingga $request->user() sudah tersedia.
$this->manager->setCurrentPanel($panel);
abort_unless($panel->isAccessibleTo($request->user()), 403);
$panel->boot();2
3
4
5
Urutannya penting:
- set current Panel;
- cek access;
- jalankan Panel boot callback.
User yang ditolak menghasilkan 403, bukan redirect. Menyembunyikan navigation bukan security boundary.
Boot callback hanya berjalan setelah user lolos access check, sehingga user yang ditolak tidak dapat memicu pekerjaan boot milik Panel.
Jika ResolvePanel dipanggil tanpa parameter, middleware melakukan fallback ke PanelManager::resolveFromRequest().
Namun route registrar selalu mengirim Panel id secara eksplisit.
RequireTwoFactor
Middleware ini menjadi no-op kecuali Panel mengaktifkan:
requireTwoFactor()Jika aktif, user ditahan pada Security Settings sampai account memiliki setidaknya satu second factor:
- authenticator/TOTP melalui
hasEnabledTwoFactorAuthentication(); - Email Code milik PandaBear;
- Passkey.
Security Settings sendiri dikecualikan.
Route lain bernama panel.{id}.pages.* juga dikecualikan sehingga user tetap memiliki jalur keluar dari Panel jika tidak ingin mengaktifkan factor.
Jika Panel mewajibkan second factor tetapi tidak memiliki Security Page, framework fail-closed dengan 403 daripada mengabaikan requirement.
RequireEmailCode
Middleware menjadi no-op kecuali signed-in account memang mengaktifkan Email Code.
Behavior-nya mirip password confirmation: session menyimpan marker yang menunjukkan challenge sudah diselesaikan.
Selama marker belum ada, request ditahan.
Semua route:
panel.{id}.auth.two-factor.*dikecualikan agar challenge tidak diblokir oleh middleware yang sedang ingin dipenuhi.
Sebelum redirect, target original disimpan pada:
url.intendedResolveTenant
Hanya diregistrasikan jika Panel mendeklarasikan tenant().
Tiga tahap dilakukan sebelum query Resource dijalankan:
- Identification menggunakan resolver milik Panel. Jika tidak ditemukan →
404. - Authorization menggunakan
HasPanelTenants::canAccessPanelTenant(). Jika ditolak →403. - Binding dilakukan setelah identification dan authorization berhasil, hanya satu kali.
Pembedaan 404 dan 403 sengaja dipertahankan: tenant tidak ditemukan berbeda dengan tenant yang ada tetapi tidak dapat diakses user.
ResolveParentRecord
Middleware ini tidak dipasang pada seluruh Panel, tetapi pada route group nested Resource:
$attributes['middleware'] = [ResolveParentRecord::class.':'.$resource];Parent di-resolve melalui query() milik parent Resource, lalu di-authorize dengan canView() milik parent.
Jika parent tidak dapat dilihat user, nested route juga menghasilkan 404.
Tanpa boundary ini, route:
/users/9/postsdapat menjadi celah untuk membaca child milik user 9 walaupun /users/9 sendiri tidak diizinkan.
Page Middleware
Standalone Page dapat menambahkan middleware sendiri:
use Illuminate\Auth\Middleware\RequirePassword;
final class BillingSettings extends Page
{
protected static array $middleware = [RequirePassword::class];
}2
3
4
5
6
Route registrar hanya menambahkan middleware tersebut pada route Page itu.
Gunakan mekanisme ini untuk concern yang harus dieksekusi sebelum Page dibangun, seperti:
- password confirmation;
- signed URL;
- middleware lain yang membutuhkan redirect.
canAccess() tidak cocok untuk kebutuhan tersebut karena hanya dapat menghasilkan keputusan yes/no.
Tahap 3 — Page
Semua route PandaBear mengarah ke controller method, bukan closure. Karena itu:
php artisan route:cachetetap dapat digunakan.
| Route | Controller |
|---|---|
panel.{id}.dashboard | PanelDashboardController → $panel->getDashboard() |
panel.{id}.pages.{slug} | PanelPageController → class yang di-bind pada route defaults |
panel.{id}.resources.{slug}.* | [PageClass::class, 'render'|'handle'|'validateStep'] |
PanelPageController tidak pernah menerima arbitrary class name dari request.
Page class ditentukan saat route registration:
$this->router
->get($page::routePath(), PanelPageController::class)
->defaults('page', $page)
->name('pages.'.$page::slug());2
3
4
Apa yang Dilakukan Sebuah Page
Bentuk dasar Page::render():
public function render(): Response
{
abort_unless(static::canAccess(), 403);
$filters = $this->resolveFilters();
$widgets = $this->resolveWidgets($filters);
$schema = $this->filterSchema();
return Inertia::render(static::$component, [
'page' => $this->metadata(),
'widgets' => $widgets->definitions(),
'widgetData' => $widgets->deferred(),
'filters' => $schema === null ? null : ['form' => $schema->toArrayWithState(null, $filters->dashboard())],
...$this->props(),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Flow-nya adalah:
authorize
↓
build metadata/schema
↓
serialize
↓
Inertia response2
3
4
5
6
7
Resource Page mengikuti pola yang sama dengan komponen lebih banyak.
ListRecords::render() misalnya:
- memanggil
canViewAny(); - membangun Table Schema;
- membaca state query string melalui
TableQuery; - melakukan pagination dari
Resource::query(); - menserialisasi rows, summaries, pagination, dan action endpoints.
Authorization diputuskan di Page/endpoint, bukan diturunkan dari button yang kebetulan dirender.
Lihat Authorization.
Tahap 4 — Inertia dan Vue
Response Inertia membawa:
shared props
+
page-specific props2
3
resources/js/app.ts me-resolve nama component menjadi file di bawah resources/js/pages/.
Component kemudian mendeklarasikan layout-nya sendiri:
defineOptions({ layout: PanelLayout });PanelLayout membaca panel dari shared props, kemudian memilih SidebarPanelLayout atau HeaderPanelLayout berdasarkan:
panel.sidebar.variantShell yang aktif membaca navigation melalui useNavigation().
Auth Page menggunakan PanelBlankLayout karena guest tidak memiliki:
- sidebar/navigation;
- notification bell;
- user menu.
Write Request
Write request menggunakan lifecycle yang sama dengan HTTP verb berbeda.
Contohnya:
POST create
PUT {record}/edit
POST action endpoint2
3
semuanya tetap melewati middleware stack Panel yang identik.
Artinya current Panel sudah di-resolve dan access check sudah selesai sebelum handler write menerima request.
Database transaction configuration di-resolve dari scope yang paling spesifik:
Action databaseTransaction(bool)
↓
Page $hasDatabaseTransactions
↓
Panel databaseTransactions()
↓
default2
3
4
5
6
7
Value null pada sebuah layer berarti:
layer ini tidak mengambil keputusan.
null bukan berarti transaction dimatikan.
Catatan
- Web middleware PandaBear ditambahkan melalui HTTP Kernel pada
afterResolvinghook, bukan langsung dipush ke router.bootstrap/app.phpmengonfigurasi groupwebmelalui lifecycle Kernel yang sama. Package yang memodifikasi router terlalu awal dapat kehilangan middleware ketika application menyusun ulang group tersebut. RedirectPanelHomemenggunakan302, bukan Inertia location visit. Tujuannya tetap Page dalam application yang sama dan Inertia request dapat mengikuti redirect tanpa full reload yang tidak diperlukan.- Guest yang membuka URL Panel ditangani oleh Laravel
authmiddleware sebelumResolvePanelberjalan. Target login ditentukan olehPandaPanel\Support\PanelLoginRedirect: Login Page milik Panel jika tersedia, atauroute('login')jika tidak. panel()bernilainullsepanjang request non-Panel, termasuk saatSharePanelDataberjalan pada starter-kit Page. Semua consumer wajib mampu menangani kondisi tersebut.- Middleware alias tersedia jika application ingin menggunakannya sendiri:
panel,panel.two-factor,panel.email-code,panel.parent. Route registrar PandaBear sendiri menggunakan class secara langsung, bukan alias.