URL dan Nama Route
Setiap tautan (link) menuju sebuah resource selalu dibangun dari nama route, bukan ditulis manual menggunakan string (hardcode). Inilah yang membuat path sebuah panel dapat diubah dengan mudah, slug dapat dikonfigurasi per panel, dan jika ada tautan yang mengarah ke panel yang tidak mendaftarkan resource tersebut, sistem akan langsung menampilkan error dengan jelas (loud failure) alih-alih error 404 yang baru disadari belakangan. Halaman ini membahas Resource::url() dan Resource::routeName(), nama-nama route yang didaftarkan oleh sebuah resource, serta siapa yang menentukan slug untuk menyusun nama-nama tersebut.
Membangun URL
use App\Models\User;
use App\Panels\Admin\Resources\Users\UserResource;
$user = User::query()->firstOrFail();
UserResource::url(); // '/admin/users'
UserResource::url('create'); // '/admin/users/create'
UserResource::url('view', $user); // '/admin/users/3'
UserResource::url('edit', $user); // '/admin/users/3/edit'
UserResource::url('edit', $user, 'admin'); // hasilnya sama, panel disebutkan secara eksplisit
UserResource::routeName(); // 'panel.admin.resources.users.index'
UserResource::routeName('edit', 'admin'); // 'panel.admin.resources.users.edit'2
3
4
5
6
7
8
9
10
11
12
13
14
Keduanya bersifat statis, keduanya berfungsi di mana saja selama sebuah panel berhasil di-resolve, dan url() mengembalikan URL relatif — ia dibangun di balik layar menggunakan route(..., absolute: false).
url()
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
| Parameter | Tipe | Default | Arti |
|---|---|---|---|
$page | string | 'index' | Key dari halaman, atau sufiks aksi penulisan seperti store atau update |
$record | `Model | int | string |
$panel | `Panel | string | null` |
$parent | `Model | int | string |
Jika Anda meneruskan sebuah Model pada parameter record maupun parent, nilainya akan disederhanakan menjadi $record->getKey(). URL pada panel berbasis ID: model yang melakukan override pada getRouteKeyName() tidak akan mengubah bentuk akhir URL-nya.
UserResource::url('view', 3); // menggunakan ID biasa berfungsi dengan baik
UserResource::url('view', $user); // menggunakan model juga berfungsi
UserResource::url(panel: panel('admin')); // menerima instance Panel maupun ID string2
3
4
Ada tiga hal yang terjadi di balik layar secara berurutan: panel di-resolve (ditemukan), status registrasi divalidasi, dan parameter-parameter URL dirakit.
routeName()
public static function routeName(string $page = 'index', Panel|string|null $panel = null): string2
UserResource::routeName(); // 'panel.admin.resources.users.index'
UserResource::routeName('store', 'admin'); // 'panel.admin.resources.users.store'
route(UserResource::routeName('edit'), ['record' => 3], absolute: false);2
3
4
5
Strukturnya akan selalu sama:
panel.{panelId}.resources.{slug}.{page}2
Metode routeName() tidak memvalidasi apakah resource tersebut terdaftar atau tidak — metode ini akan tetap menghasilkan nama route untuk panel yang tidak pernah mendaftarkan resource tersebut, menggunakan slug bawaan (default) dari class-nya. url() lah yang melakukan pengecekan tersebut. Gunakan url() kecuali jika Anda benar-benar hanya membutuhkan nama route-nya.
Nama yang didaftarkan oleh sebuah resource
Key di dalam metode Resource::pages() akan menjadi sufiks untuk nama route. Empat key halaman standar mendaftarkan lebih dari satu route masing-masing, karena sebuah verb penulisan (seperti POST/PUT) memerlukan nama route-nya sendiri.
| Key Halaman | Verb dan Path | Sufiks Nama Route |
|---|---|---|
index | GET / | index |
create | GET create | create |
create | POST create | store |
create | POST create/step | validateCreateStep |
view | GET {record} | view |
edit | GET {record}/edit | edit |
edit | PUT {record}/edit | update |
edit | POST {record}/edit/step | validateEditStep |
| key lainnya | GET ResourcePage::routePath($key) | menggunakan nama key itu sendiri |
Jadi, URL target (action) dari form pembuatan (create) pada dasarnya adalah URL biasa seperti yang lain:
UserResource::url('store'); // '/admin/users/create' (POST)
UserResource::url('update', $user); // '/admin/users/3/edit' (PUT)
UserResource::url('validateEditStep', $user);2
3
4
Class CreateRecord dan EditRecord menggunakan URL ini secara persis. Inilah alasan mengapa sebuah panel bisa mengubah path URL-nya tanpa merusak form sama sekali.
Sebuah resource yang mendeklarasikan integrasi akan mendaftarkan enam nama route tambahan di bawah resources.{slug}.integrations*. Lihat Routing.
Siapa yang menentukan slug
Class yang mengusulkan; panel yang memutuskan.
public static function defaultSlug(): string; // slug bawaan dari class itu sendiri
public static function slug(): string; // slug pada panel saat ini
public static function slugIn(?Panel $panel): string; // slug pada panel yang disebutkan
public static function configurationIn(?Panel $panel): ?ResourceConfiguration;2
3
4
5
public static function defaultSlug(): string
{
return static::$slug ?? Str::of(class_basename(static::getModel()))->plural()->kebab()->toString();
}2
3
4
5
| Model | $slug | defaultSlug() |
|---|---|---|
App\Models\User | tidak diatur | users |
App\Models\BlogPost | tidak diatur | blog-posts |
App\Models\Category | tidak diatur | categories |
App\Models\User | 'people' | people |
final class UserResource extends Resource
{
protected static ?string $slug = 'team-members'; // /admin/team-members
}2
3
4
5
Metode slug() akan mengembalikan slug untuk panel saat ini dan akan fallback (kembali) menggunakan defaultSlug() jika tidak ada panel yang aktif — misalnya, saat sedang menjalankan console command. Saat proses pendaftaran route, sistem akan menggunakan ResourceRegistry::slugFor() karena pada proses boot, belum ada panel aktif yang bisa dicek:
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->resources('admin')->slugFor(UserResource::class); // 'users'
app(PanelManager::class)->resources('admin')->bySlug('users'); // UserResource::class2
3
4
5
Sebuah panel mengidentifikasi resource-nya berdasarkan slug. Jika ada dua class yang mencoba menggunakan slug yang sama, sistem akan melempar error PanelRegistrationException::duplicateResourceSlug() saat registrasi. Hal yang sama juga berlaku jika satu class didaftarkan dua kali dengan dua slug yang berbeda pada panel yang sama — registrasi kedua akan membuat Resource::url() menjadi ambigu, karena tidak ada cara untuk mengetahui slug mana yang sebenarnya dimaksud.
Satu class, dua panel
Class resource yang sama dapat ditempatkan di lebih dari satu panel dengan slug yang berbeda, dan URL untuk masing-masing panel akan dibangun berdasarkan slug-nya masing-masing:
use PandaPanel\Resources\ResourceConfiguration;
$panel->resources([
ResourceConfiguration::for(UserResource::class)
->slug('people')
->pluralLabel('People'),
]);2
3
4
5
6
7
8
UserResource::url(panel: 'admin'); // '/admin/users'
UserResource::url(panel: 'staff'); // '/staff/people'2
3
Meminta URL pada sebuah panel yang tidak mendaftarkan resource tersebut akan menghasilkan error (throw exception). Inilah yang menjadikan isolasi panel dapat dibuktikan dengan pasti, bukan hanya kebetulan. Lihat Per-panel configuration.
Cluster mengubah path, bukan nama route
Sebuah resource yang berada di dalam cluster (kelompok) akan didaftarkan di bawah URL {cluster-slug}/{slug}, tetapi nama route-nya akan tetap menggunakan format resources.{slug}.:
protected static ?string $cluster = SettingsCluster::class;2
UserResource::routeName(); // 'panel.admin.resources.users.index' — tidak berubah
UserResource::url(); // '/admin/settings/users' — berpindah path2
3
Setiap pemanggilan Resource::url() yang sudah Anda tulis di dalam aplikasi akan tetap berfungsi normal. Inilah sebabnya memindahkan resource ke dalam sebuah cluster tidak akan merusak aplikasi (non-breaking change). Lihat Clusters.
Nested resources menyertakan parent-nya
Setiap route dari nested resource (resource bersarang) diletakkan di bawah {parentRecord}, sehingga jika Anda membangun URL tanpa menyertakan parent-nya, scope (cakupan) resource tersebut akan hilang secara otomatis. Parent yang terikat pada request yang sedang berjalan (request's own bound parent) akan digunakan secara otomatis (default):
TaskResource::url(); // '/admin/projects/7/tasks'
TaskResource::url('edit', $task); // '/admin/projects/7/tasks/12/edit'
TaskResource::url(parent: $otherProject); // '/admin/projects/8/tasks'2
3
4
Namun, di luar request (misalnya, di dalam console command atau queued job), tidak ada data otomatis yang bisa digunakan (terikat). Oleh karena itu, ParentRecord::require() akan melempar exception alih-alih menghasilkan URL yang tidak valid karena kehilangan scope. Saat berada di situasi ini, Anda harus meneruskan parameter parent: secara eksplisit. Lihat Nested resources.
Singular resources menghilangkan parameter record
if ($record !== null && ! static::isSingular()) {
$parameters['record'] = $record instanceof Model ? $record->getKey() : $record;
}2
3
4
Route untuk singular resource (resource tunggal) tidak memiliki {record}, sehingga parameter ini akan diabaikan (bukan ditolak) jika diteruskan — hal inilah yang memungkinkan shared code (kode yang digunakan bersama antar halaman) untuk tetap bisa membangun URL dengan cara yang sama untuk kedua jenis resource (jamak dan tunggal). Lihat Singular resources.
Bagaimana URL diteruskan ke Vue
Frontend tidak pernah membangun URL panel secara mandiri. Setiap halaman resource selalu mengirimkan URL yang dibutuhkan oleh komponennya melalui props, dan komponen tersebut akan menggunakannya persis apa adanya.
| Prop | Dibangun dari | Digunakan untuk |
|---|---|---|
resource.indexUrl | Resource::url() | Navigasi tabel — setiap sorting, filter, dan perubahan halaman akan menulis ulang query string pada URL ini |
submitUrl | Resource::url('store') / Resource::url('update', $record) | Aksi POST atau PUT pada form |
validateStepUrl | Resource::url('validateCreateStep') / Resource::url('validateEditStep', $record) | Pengecekan per-langkah pada wizard, bernilai null jika form tidak menggunakan wizard |
optionsUrl, uploadUrl, formStateUrl | PandaPanel\Support\FormEndpoints | Select pencarian (searchable), proses unggah (upload), dan live fields |
actionEndpoints | Panel::routeName('actions.*') | Ketujuh endpoint untuk aksi (actions) |
page.breadcrumbs[].href | Resource::url() dan Resource::url('view', $record) | Jejak breadcrumb (trail) |
// resources/js/panel/composables/useResource.ts
router.get(
query === '' ? resource().indexUrl : `${resource().indexUrl}?${query}`,
{},
{ preserveState: true, preserveScroll: true, replace: true },
);2
3
4
5
6
7
Alasan utama mengapa url() dirancang berjalan di sisi server-side adalah: ketika sebuah panel dipindahkan dari /admin ke /backoffice, Anda hanya perlu mengubah satu baris di provider aplikasi, dan Anda tidak perlu mengubah satu baris kode pun di Vue.
URL hasil pencarian global
Pencarian global (global search) akan bertanya pada resource, ke mana arah URL jika datanya diklik (di-hit):
public static function globalSearchResultUrl(Model $record): string
{
$pages = static::pages();
if (array_key_exists('view', $pages)) {
return static::url('view', $record);
}
return array_key_exists('edit', $pages)
? static::url('edit', $record)
: static::url();
}2
3
4
5
6
7
8
9
10
11
12
13
Logikanya: arahkan ke halaman view jika ada, lalu ke halaman edit jika halaman view tidak ada, dan arahkan ke halaman index sebagai opsi terakhir. Setiap halaman akan melakukan pengecekan otorisasi secara mandiri ketika dibuka, sehingga keamanan data tidak hanya bergantung pada tautan ini saja. Anda dapat melakukan override metode ini untuk resource yang hasil pencariannya perlu diarahkan ke halaman (atau URL) lain. Lihat Global search.
Kapan fungsi ini akan menolak (error)
Terdapat dua kondisi kegagalan (error) yang sengaja dijadikan sebagai exception, bukan sekadar tebakan sistem belaka. Keduanya menggunakan PandaPanel\Exceptions\PanelRegistrationException.
| Pemanggilan | Kondisi | Pesan Error |
|---|---|---|
UserResource::url() | Tidak ada panel yang aktif dan tidak ada panel yang diteruskan | There is no current panel for this request. Resolve one through panel middleware or pass an explicit panel. |
UserResource::url(panel: 'app') | Panel tersebut tidak mendaftarkan resource terkait | The resource [App\...\UserResource] is not registered in the panel [app], so it has no URL there. |
protected static function assertRegisteredIn(Panel $panel): void
{
if (! app(PanelManager::class)->resources($panel)->contains(static::class)) {
throw PanelRegistrationException::resourceNotInPanel(static::class, $panel->getId());
}
}2
3
4
5
6
7
Jika sistem memaksakan memilih (secara diam-diam/silent) sembarang panel, tautan lintas panel akan terlihat benar namun mengarah ke tempat yang salah. Dan jika mengembalikan string kosong, ini akan memunculkan broken link di halaman yang mungkin luput dari pengujian Anda.
Kegagalan ketiga berasal dari Laravel, bukan dari panel: yakni ketika Anda meminta page key (kunci halaman) yang tidak pernah dideklarasikan oleh resource, sistem akan melempar RouteNotFoundException sembari menyebutkan nama route yang tidak dapat ditemukannya.
UserResource::url('audit'); // Route [panel.admin.resources.users.audit] not defined.2
Catatan Penting
url()mengembalikan URL relatif. Tambahkan prefix Anda sendiri jika Anda membutuhkan URL absolut (absolute URL) — misalnya untuk template email — atau gunakanroute(UserResource::routeName('view'), ['record' => $key]).routeName()tidak mengecek status registrasi. Ia akan menjawab untuk panel mana pun, termasuk panel di mana route tersebut sebetulnya tidak ada. Gunakanurl()jika Anda ingin sistem melakukan validasi (checked form).- Parameter record akan selalu menjadi primary key. Custom route keys belum/tidak didukung.
storedanupdateadalah bagian dari page keys juga. Kedua key ini dinamai secara terpisah karena Laravel mewajibkan nama route untuk selalu unik, dan ini adalah URL target bagi form (POST/PUT).- Resource yang tidak memiliki halaman
indextidak akan memiliki nilai defaulturl().ResourcePage::baseBreadcrumbs()akan memanggilResource::url()pada setiap halaman record, sehingga key halamanindexharus ada, atau proses render komponen breadcrumb akan rusak (error). Sidebar menanganinya dengan lebih ramah:navigationItem()akan mengembalikan nilainulldaripada memaksa membangun tautan menuju route yang tidak pernah terdaftar. - Dua resource tidak bisa mengklaim bentuk path yang sama di dalam satu panel. Saat membandingkan route, registrar akan menghapus nama parameter di dalamnya terlebih dulu. Jadi
{record}dan{parentRecord}akan saling bentrok, dan sistem akan langsung melempar error saat boot alih-alih menampilkan halaman rusak yang tak bisa dijangkau. - Nama route adalah acuan untuk generate data bagi Wayfinder. Jaga agar nama-nama route ini tetap konsisten (stabil); yang boleh Anda pindah-pindahkan dengan bebas hanyalah path-nya. Lihat Wayfinder.
Lihat juga
- Creating resources (Membuat resources)
- List, create, view and edit pages (Halaman list, create, view dan edit)
- Resource pages (Halaman-halaman resource)
- Singular resources (Resource tunggal)
- Nested resources (Resource bersarang)
- Per-panel configuration (Konfigurasi per-panel)
- Labels and navigation (Label dan navigasi)
- Global search (Pencarian global)
- Routing
- Clusters
- Wayfinder