Relation Pages
Relation page adalah relation manager yang sama dengan yang biasanya ditampilkan inline pada record page, tetapi diberikan route sendiri dan tempat pada record sub-navigation. Gunakan relation page ketika sebuah relasi cukup besar sehingga user perlu melakukan pagination, search, dan filter secara serius. Table sebesar itu layak memiliki URL sendiri; beberapa row saja biasanya lebih baik tetap ditampilkan di samping record.
Relation page minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Pages;
use App\Panels\Admin\Resources\Users\RelationManagers\PostsRelationManager;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Resources\Pages\ManageRelatedRecords;
final class ManageUserPosts extends ManageRelatedRecords
{
protected static string $resource = UserResource::class;
protected static string $relationManager = PostsRelationManager::class;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Daftarkan pada Resource::pages() seperti page lain agar route, route name, dan authorization menggunakan machinery yang sama:
/**
* @return array<string, class-string>
*/
public static function pages(): array
{
return [
'index' => ListUsers::class,
'create' => CreateUser::class,
'view' => ViewUser::class,
'edit' => EditUser::class,
'posts' => ManageUserPosts::class,
];
}2
3
4
5
6
7
8
9
10
11
12
13
Manager tetap harus ada di relationManagers(). Daftar page bukan registration kedua; page hanya menunjuk ke manager yang sebelumnya sudah diregistrasikan Resource.
Hasilnya:
/admin/users/3/posts route name: panel.admin.resources.users.postsserta tab "Posts" pada record sub-navigation di samping View dan Edit.
Deklarasi
| Property | Type | Default | Arti |
|---|---|---|---|
$resource | class-string<Resource> | wajib | Owning Resource |
$relationManager | class-string<RelationManager> | wajib | Manager yang ditampilkan page |
$routePath | ?string | {record}/{page key} | Path relatif terhadap Resource |
$title | ?string | RelationManager::title() | Document title |
$heading | ?string | mengikuti title | Heading di atas content |
$subheading | ?string | title owner record | Baris di bawah heading |
$component | string | panel/resources/ManageRelated | Vue page component |
final class ManageUserPosts extends ManageRelatedRecords
{
protected static string $resource = UserResource::class;
protected static string $relationManager = PostsRelationManager::class;
protected static ?string $routePath = '{record}/articles';
protected static ?string $title = 'Articles';
}2
3
4
5
6
7
8
9
10
Pertahankan {record} di $routePath. Page merepresentasikan relation milik satu record dan registrar meneruskan segment tersebut ke render(). Path tanpa {record} membuat $record bernilai null lalu fallback ke singular resolution Resource — record pertama pada query — yang hampir tidak pernah merupakan arti relation page yang benar.
Method
public function render(Request $request, ?string $record = null): Response;
public static function relationManager(): string; // class-string<RelationManager>
public static function relationPageKey(): string; // the manager's key
public static function routePath(string $key): string; // '{record}/'.$key by default2
3
4
5
ManageUserPosts::relationManager(); // App\...\PostsRelationManager::class
ManageUserPosts::relationPageKey(); // 'posts'
ManageUserPosts::routePath('posts'); // '{record}/posts'2
3
relationPageKey() menggunakan key manager, bukan page key. Sub-navigation menandai active state berdasarkan identitas manager. Page key adalah apa pun yang dipilih pages(), sedangkan dua hal yang menunjuk relation yang sama perlu memiliki satu identitas yang konsisten.
Override heading method seperti resource page biasa:
use Illuminate\Database\Eloquent\Model;
protected function defaultSubheading(?Model $record): ?string
{
return $record === null ? null : $record->getAttribute('email');
}2
3
4
5
6
Yang dilakukan render()
- Menghasilkan 404 jika
Resource::relationManager($key)tidak menunjuk manager milik page ini. Page untuk manager yang tidak dideklarasikan Resource akan menjadi jalur mengakses relation yang tidak pernah diregistrasikan. - Me-resolve owner melalui
query()milik Resource. Key di luar scope menghasilkan 404. - Membangun relation melalui
RelationTable::forManager(). Jika hasilnya null — yaituRelationManager::canViewAny()menolak — response menjadi 403. - Me-render
panel/resources/ManageRelateddengan props berikut.
| Prop | Isi |
|---|---|
page | Title, heading, subheading, breadcrumbs, cluster, sub-navigation, render-hook scope |
resource | Metadata Resource seperti resource page lain |
recordKey | Key owner |
relation | Satu serialized relation manager — lihat Relation tables |
| widget props | Props dari widget page, menggunakan page context milik record |
page.headerActions kosong. Create, attach, dan associate milik relation hidup pada relation payload di atas table, bukan pada header action page.
Sub-navigation
Relation page otomatis bergabung dengan record sub-navigation. PandaPanel\Support\RecordSubNavigation membaca Resource::pages(), mencari seluruh subclass ManageRelatedRecords, lalu memasukkan yang boleh dibaca user:
[
['key' => 'view', 'label' => 'View', 'href' => '/admin/users/3', 'icon' => 'search', 'active' => false],
['key' => 'edit', 'label' => 'Edit', 'href' => '/admin/users/3/edit', 'icon' => 'settings', 'active' => false],
['key' => 'posts', 'label' => 'Posts', 'href' => '/admin/users/3/posts', 'icon' => null, 'active' => true],
]2
3
4
5
| Detail | Behavior |
|---|---|
| Key | RelationManager::key(), bukan page key |
| Label | RelationManager::title() |
| Icon | RelationManager::icon(); icon tidak terdaftar hanya tidak dirender |
| Dimasukkan ketika | RelationManager::canViewAny($record) dan manager ada di relationManagers() |
| Navigation dirender | hanya jika ada lebih dari satu item — satu link bukan navigation |
Item langsung hilang ketika viewAny manager ditolak. Route tetap melakukan authorization independen, sehingga hidden tab bukan security boundary.
Breadcrumbs
Dashboard › Users › Ada Lovelace › PostsRecord crumb menjadi link ke view page jika Resource memiliki view page dan user boleh membukanya. Jika tidak, crumb hanya plain text — bukan link yang akan berakhir 403. Crumb relation adalah current item.
Inline dan page terpisah
Mendeklarasikan relation page tidak otomatis menghilangkan manager dari record page lain. ViewRecord dan EditRecord men-serialize seluruh manager yang dideklarasikan Resource, karena page-lah yang menentukan manager mana yang ingin ditampilkan. Untuk hanya menampilkan manager pada relation page, persempit list:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
// On ViewUser / EditUser
protected function relationTables(Request $request, Model $record): array
{
return array_values(array_filter(
parent::relationTables($request, $record),
static fn (array $relation): bool => $relation['key'] !== 'posts',
));
}2
3
4
5
6
7
8
9
10
11
Collision pada path
Dua Resource/Page tidak boleh mengklaim path yang sama dan registration menolaknya saat boot. ManageRelatedRecords pada projects/{record}/tasks dan nested resource pada projects/{parentRecord}/tasks adalah bentuk route yang sama — nama parameter tidak memengaruhi matching — sehingga salah satunya akan tidak dapat dijangkau:
PanelRegistrationException: The path [projects/{parentRecord}/tasks] is registered by both
[App\...\ProjectResource] and [App\...\TaskResource]. Only the first would ever match.2
PanelRouteRegistrar membandingkan normalized path shape per Panel lalu melempar exception, bukan membiarkan masalah baru terlihat sebagai page yang me-render data salah. Berikan slug atau $routePath berbeda pada salah satu. Lihat Nested resource vs relation manager.
Membuat relation page dengan generator
php artisan make:panel-relation-manager posts --panel=Admin --resource=Users --page--page membuat manager sekaligus ManageRelatedRecords page di bawah app/Panels/Admin/Resources/Users/. Generated page tetap memberi reminder terhadap dua langkah manual: menambahkan page key ke pages() dan manager ke relationManagers().
Hal yang perlu diperhatikan
- Relation page hanya GET. Seluruh write tetap dikirim ke relation endpoints milik Panel yang URL-nya dibawa relation payload. Page tidak mendaftarkan POST.
- Page key dan relation key adalah dua hal berbeda.
pages()memilih URL segment;RelationManager::key()menentukan sub-navigation key dan payload identity. Biasanya keduanya menggunakan kata yang sama, tetapi tidak diwajibkan. - Page dengan manager yang hilang dari
relationManagers()menghasilkan 404. Bukan exception saat boot; page memang ada dan check dilakukan pada request. - Sub-navigation hanya dirender jika ada minimal dua item. Resource yang hanya memiliki satu relation page tanpa view/edit page tidak menampilkan tabs karena tidak ada destination lain.
- Satu manager seharusnya memiliki paling banyak satu relation page. Dua page yang menunjuk manager sama akan menandai sub-navigation key yang sama sebagai active.
canViewAny()manager adalah authorization relation page.canView()pada owner Resource ditanyakan lebih dulu saat record di-resolve.