Routing
PandaPanel\Routing\PanelRouteRegistrar mendaftarkan satu route group untuk setiap Panel saat application boot.
Setiap group menggunakan:
- path Panel sebagai URL prefix;
panel.{id}.sebagai route-name prefix;- middleware stack milik Panel.
Semua route menunjuk ke controller method, bukan closure, sehingga php artisan route:cache tetap dapat digunakan.
Gunakan dokumentasi ini ketika Anda membutuhkan route name, atau ketika sebuah URL menghasilkan 404 dan Anda perlu mengetahui route apa yang seharusnya mendaftarkannya.
Melihat Route yang Didaftarkan Panel
php artisan route:list --path=adminuse Illuminate\Support\Facades\Route;
Route::has('panel.admin.dashboard'); // true
route('panel.admin.dashboard', absolute: false); // '/admin'
route('panel.admin.resources.users.index', absolute: false);// '/admin/users'2
3
4
5
Bangun route name dari object Panel daripada menuliskannya manual:
panel('admin')->getRouteNamePrefix(); // 'panel.admin.'
panel('admin')->routeName('dashboard'); // 'panel.admin.dashboard'2
Route Group
$attributes = [
'prefix' => $panel->getPath(),
'as' => $panel->getRouteNamePrefix(),
'middleware' => [
...$panel->getMiddleware(),
ResolvePanel::class.':'.$panel->getId(),
RequireTwoFactor::class.':'.$panel->getId(),
RequireEmailCode::class.':'.$panel->getId(),
...($panel->hasTenancy() ? [ResolveTenant::class.':'.$panel->getId()] : []),
],
];
if ($panel->getDomain() !== null) {
$attributes['domain'] = $panel->getDomain();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Panel id dikirim ke ResolvePanel sebagai middleware parameter, bukan dicocokkan ulang dari path.
Dengan begitu Panel resolution tidak bergantung pada runtime path matching dan dua Panel yang kebetulan memiliki prefix serupa tetap tidak ambigu.
Lihat Request Lifecycle.
registerAll(): void menjalankan registration untuk seluruh Panel.
register(Panel $panel): void hanya untuk satu Panel.
Service Provider memanggil registerAll() kecuali:
panda-panel.register_routes = falseRoute Tingkat Panel
Setiap Panel mendapatkan route berikut di bawah prefix-nya sendiri.
| Nama | Verb | Path | Controller |
|---|---|---|---|
panel.{id}.dashboard | GET | / | PanelDashboardController |
panel.{id}.search | GET | search | PanelSearchController |
panel.{id}.options | GET | options | PanelFormOptionsController |
panel.{id}.uploads | POST | uploads | PanelUploadController |
panel.{id}.form-state | POST | form-state | PanelFormStateController |
panel.{id}.export-file | GET | exports/{file} | PanelExportController |
panel.{id}.import-file | GET | imports/{file} | PanelImportController |
panel.{id}.notifications.index | GET | notifications | PanelNotificationController@index |
panel.{id}.notifications.read | POST | notifications/read | PanelNotificationController@read |
panel.{id}.notifications.clear | POST | notifications/clear | PanelNotificationController@clear |
Route export menggunakan nama export-file, bukan exports, karena route name menjadi identifier pada generated Wayfinder module. Nama exports tidak dapat digunakan sebagai binding identifier TypeScript yang aman pada konteks tersebut.
Two-Factor
Route berikut berada di dalam Panel middleware karena digunakan oleh user yang sudah login, tetapi dikecualikan dari Email Code enforcement itu sendiri.
Jika tidak dikecualikan, user tidak akan pernah dapat menjawab challenge karena route untuk menjawabnya justru diblokir oleh challenge tersebut.
| Nama | Verb | Path | Middleware Tambahan |
|---|---|---|---|
panel.{id}.auth.two-factor.challenge | GET | two-factor/challenge | — |
panel.{id}.auth.two-factor.send | POST | two-factor/send | — |
panel.{id}.auth.two-factor.verify | POST | two-factor/verify | — |
panel.{id}.auth.two-factor.enable | POST | two-factor/enable | RequirePassword |
panel.{id}.auth.two-factor.disable | POST | two-factor/disable | RequirePassword |
Actions
Setiap Panel memiliki satu set Action endpoint, bukan satu set per Resource.
Resource dikirim melalui payload lalu di-resolve terhadap registry milik Panel saat ini. Karena itu Resource dari Panel lain tidak dapat dipanggil melalui endpoint ini.
| Nama | Verb | Path | Method |
|---|---|---|---|
panel.{id}.actions.record | POST | actions/record | PanelActionController@record |
panel.{id}.actions.bulk | POST | actions/bulk | PanelActionController@bulk |
panel.{id}.actions.reorder | POST | actions/reorder | PanelActionController@reorder |
panel.{id}.actions.cell | POST | actions/cell | PanelActionController@cell |
panel.{id}.actions.table | POST | actions/table | PanelActionController@table |
panel.{id}.actions.infolist | POST | actions/infolist | PanelActionController@infolist |
panel.{id}.actions.form | GET | actions/form | PanelActionFormController@show |
panel.{id}.actions.submit | POST | actions/form | PanelActionFormController@submit |
Action pada View Page di-resolve melalui scope infolist, bukan record.
Keduanya merupakan whitelist yang berbeda. Menggabungkan lookup tersebut akan membuka kemungkinan Action yang hanya didaftarkan pada salah satu context dapat dijalankan dari context lain.
Relations
| Nama | Verb | Path | Method |
|---|---|---|---|
panel.{id}.relations.form | GET | relations/form | PanelRelationController@form |
panel.{id}.relations.save | POST | relations/form | PanelRelationController@save |
panel.{id}.relations.action | POST | relations/action | PanelRelationController@action |
panel.{id}.relations.bulk | POST | relations/bulk | PanelRelationController@bulk |
Route form dan save mengambil context dari query string sehingga tidak membutuhkan route parameter tambahan.
Guest Routes
Guest route hanya diregistrasikan ketika Panel mengaktifkan login().
Route ini dibuat di luar authenticated Panel middleware. Menempatkan Login Page di balik auth akan membuat guest diarahkan dari route protected menuju route yang juga meminta authentication.
Guest Page tetap menggunakan base middleware Panel sehingga:
- session tersedia;
- CSRF tersedia;
- Inertia tetap berjalan.
$panel
->login()
->registration()
->passwordReset()
->emailVerification();2
3
4
5
| Nama | Verb | Path | Membutuhkan |
|---|---|---|---|
panel.{id}.auth.login | GET | login | login() |
panel.{id}.auth.register | GET | register | registration() |
panel.{id}.auth.password.request | GET | forgot-password | passwordReset() |
panel.{id}.auth.password.reset | GET | reset-password/{token} | passwordReset() |
panel.{id}.auth.verification.notice | GET | verify-email | emailVerification() |
PandaBear hanya menyediakan Page GET-nya.
Form submit tetap menuju Fortify.
Menduplikasi Login POST per Panel berarti framework harus menduplikasi:
- rate limiting;
- two-factor flow;
- Passkey;
- session handling.
Semua concern tersebut harus konsisten di seluruh pintu masuk application, sehingga tetap menjadi milik Fortify.
Standalone Pages
Setiap registered Page mendapatkan satu route GET pada Page::routePath().
Page class dimasukkan ke route defaults saat registration sehingga controller tidak pernah me-resolve arbitrary class name dari request:
$this->router
->get($page::routePath(), PanelPageController::class)
->defaults('page', $page)
->name('pages.'.$page::slug());2
3
4
| Page API | Signature | Default |
|---|---|---|
slug | static slug(): string | Str::kebab(class_basename(static::class)) |
routePath | static routePath(): string | slug, ditambah cluster prefix jika ada |
middleware | static middleware(): list<string> | [], hanya ditambahkan pada route ini |
routeName | static routeName(Panel|string|null $panel = null): string | panel.{id}.pages.{slug} |
url | static url(Panel|string|null $panel = null): string | relative URL |
use App\Panels\Admin\Pages\Settings;
Settings::slug(); // 'settings'
Settings::routeName('admin'); // 'panel.admin.pages.settings'
Settings::url('admin'); // '/admin/settings'2
3
4
5
Dashboard tambahan milik Panel juga diregistrasikan sebagai standalone Page.
Class di-deduplicate sehingga Page yang juga terdeteksi melalui discovery hanya akan diregistrasikan satu kali.
Resource Routes
Route Resource dibangun dari Resource::pages().
Key dalam array tersebut menentukan page name sehingga route name dan Page class tetap terhubung di satu tempat.
public static function pages(): array
{
return [
'index' => ListUsers::class,
'create' => CreateUser::class,
'view' => ViewUser::class,
'edit' => EditUser::class,
];
}2
3
4
5
6
7
8
9
Empat standard key memiliki bentuk route tetap:
| Key | Verb | Path | Method | Route Name Suffix |
|---|---|---|---|---|
index | GET | / | render | index |
create | GET | create | render | create |
create | POST | create | handle | store |
create | POST | create/step | validateStep | validateCreateStep |
view | GET | {record} | render | view |
edit | GET | {record}/edit | render | edit |
edit | PUT | {record}/edit | handle | update |
edit | POST | {record}/edit/step | validateStep | validateEditStep |
Static segment diregistrasikan sebelum wildcard {record}.
Jika tidak, URL /create dapat salah dianggap sebagai record key bernama create.
Write verb juga memiliki route name berbeda karena Laravel mensyaratkan route name unik.
Entry lain di pages() dianggap custom Resource Page.
Custom Page mendapatkan satu route GET pada:
ResourcePage::routePath($key)Jika Page mendeklarasikan $routePath, value tersebut digunakan. Jika tidak, key dari pages() digunakan.
final class UserActivity extends ResourcePage
{
protected static ?string $routePath = '{record}/activity';
}2
3
4
// 'activity' => UserActivity::class → GET /admin/users/{record}/activity
// panel.admin.resources.users.activity2
ManageRelatedRecords secara default mendeklarasikan:
{record}/{key}Jadi:
'posts' => ManageUserPosts::classmenghasilkan:
/admin/users/{record}/postsAttribute Route Group Resource
| Kasus | Prefix |
|---|---|
| Resource biasa | {slug} |
| Resource di dalam Cluster | {cluster-slug}/{slug} |
| Nested Resource | {parent-slug}/{parentRecord}/{slug} |
Route name selalu menggunakan:
resources.{slug}.Cluster hanya mengubah path, bukan route name.
Dengan begitu existing Resource::url() tetap valid ketika Resource dipindahkan ke Cluster. Hanya URL hasil akhirnya yang berubah.
Nested Resource group juga mendapatkan:
ResolveParentRecord::class.':'.$resourceMiddleware dipasang pada group, bukan masing-masing Page, karena seluruh route nested Resource harus memiliki parent scope.
Jika satu Page lupa menambahkan middleware secara individual, query dapat berjalan tanpa scope. Menaruh middleware pada group menghilangkan kemungkinan tersebut.
Jika Parent Resource tidak diregistrasikan pada Panel yang sama, boot gagal melalui:
PanelRegistrationException::unregisteredParentResource()daripada menghasilkan dead link.
Singular Resources
Resource dengan:
protected static bool $singular = true;tidak memiliki pilihan record, sehingga segment {record} dihapus.
| Key | Path |
|---|---|
index | / |
create | create |
view | / |
edit | edit |
index dan view sama-sama menjadi /.
Karena itu singular Resource sebaiknya mendeklarasikan salah satunya, bukan keduanya.
Bentuk umum yang dituju adalah:
/settings
/settings/edit2
Membangun URL
Selalu gunakan route name, bukan merangkai path manual.
Hardcode seperti:
/admin/usersakan rusak ketika Panel mengganti path dan juga mengabaikan registration check.
API:
public static function routeName(string $page = 'index', Panel|string|null $panel = null): string
public static function url(
string $page = 'index',
Model|int|string|null $record = null,
Panel|string|null $panel = null,
Model|int|string|null $parent = null,
): string2
3
4
5
6
7
8
Contoh:
use App\Panels\Admin\Resources\Users\UserResource;
UserResource::url(); // '/admin/users'
UserResource::url('edit', $user); // '/admin/users/3/edit'
UserResource::url(panel: 'admin'); // '/admin/users'
UserResource::routeName('index', 'admin'); // 'panel.admin.resources.users.index'2
3
4
5
6
Dua kondisi sengaja menghasilkan exception daripada menebak:
UserResource::url(); // outside a panel: PanelRegistrationException, 'no current panel'
UserResource::url(panel: 'app'); // not registered there: 'is not registered in the panel [app]'2
Nested Resource URL dapat mengambil parent dari request-scoped parent yang sudah ter-bind. Karena itu link antar-Page pada nested Resource normalnya tidak membutuhkan argument parent tambahan.
Gunakan parent: jika membangun URL untuk owner yang berbeda.
Integration Routes
Route Integration hanya diregistrasikan untuk Resource yang:
integrationSettings()->enabled()bernilai true.
Resource yang tidak pernah mengaktifkan integration tidak mendaftarkan route apa pun, sehingga URL menghasilkan 404, bukan 403.
Artinya memang tidak ada screen/endpoint yang tersedia, bukan ada endpoint yang user tidak boleh akses.
| Nama | Verb | Path |
|---|---|---|
panel.{id}.resources.{slug}.integrations | GET | integrations |
panel.{id}.resources.{slug}.integrations.store | POST | integrations |
panel.{id}.resources.{slug}.integrations.update | PUT | integrations/{integration} |
panel.{id}.resources.{slug}.integrations.destroy | DELETE | integrations/{integration} |
panel.{id}.resources.{slug}.integrations.send | POST | integrations/{integration}/send |
panel.{id}.resources.{slug}.integrations.rotate | POST | integrations/{integration}/rotate |
Slug Resource dikirim sebagai route default, bukan sebagai path segment tambahan.
Route tersebut sudah berada di dalam Resource prefix. Mengulang slug di URL akan menciptakan dua sumber nilai yang dapat saling tidak cocok.
Collision Path
Laravel menggunakan route pertama yang cocok dan route berikutnya dapat menjadi tidak terjangkau tanpa error.
Karena itu PanelRouteRegistrar menormalisasi setiap path yang diklaim.
Nama parameter dihapus saat perbandingan, sehingga:
{record}dan:
{parentRecord}dianggap bentuk path yang sama.
Jika ada claim kedua untuk shape yang sama, framework melempar:
PanelRegistrationException::collidingRoutePath()Contoh:
projects/{record}/tasksdari ManageRelatedRecords dan:
projects/{parentRecord}/tasksdari Nested Resource sebenarnya memiliki matching shape yang sama.
Framework menangkap collision tersebut saat boot daripada membiarkannya muncul sebagai Page yang merender content yang salah.
Collision tracking dilakukan per Panel. Dua Panel dengan prefix berbeda tidak dapat saling menimpa.
Catatan
- Panel diregistrasikan berdasarkan urutan id sehingga route registration order stabil antar run dan output
route:cachedeterministik. PanelManager::resolveFromRequest()mencocokkan path prefix terpanjang terlebih dahulu. Panel/admin/reportsmenang atas Panel/adminuntuk request/admin/reports/x. Method juga menghormatidomain().- Tidak ada closure pada route PandaBear. Jika
route:cachegagal setelah Anda menambahkan Page, penyebabnya kemungkinan berasal dari route application lain atau route default yang menyimpan object. panda-panel.register_routes => falsemelewati seluruh proses route registration. Registry tetap dibangun, sehinggaPandaPanel::resources('admin')->all()masih dapat digunakan.- Wayfinder dibangun dari route name. Pertahankan route name stabil; path adalah bagian yang aman untuk dipindahkan.