Nested Resource vs Relation Manager
Keduanya sama-sama menampilkan child milik satu record dan sama-sama men-scope seluruh pembacaan ke owner/parent tersebut. Perbedaannya adalah bentuk pengalaman yang diberikan kepada child: relation manager memberi table di samping owner dan dialog untuk mengeditnya; nested resource memberi child page lengkap di bawah URL parent. Halaman ini membantu memilih di antara keduanya sekaligus menjelaskan mekanisme di balik masing-masing pilihan.
Relasi yang sama, dengan dua pendekatan
Tasks milik sebuah Project sebagai relation manager:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Projects\RelationManagers;
use App\Panels\Admin\Resources\Projects\ProjectResource;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Relations\DeleteRelatedAction;
use PandaPanel\Actions\Relations\EditRelatedAction;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\RelationManager;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class TasksRelationManager extends RelationManager
{
protected static string $relationship = 'tasks';
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([TextColumn::make('name')->searchable()->sortable()])
->recordActions([
EditRelatedAction::make(ProjectResource::class, self::class, $owner),
DeleteRelatedAction::make(self::class, $owner),
]);
}
public static function form(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([TextInput::make('name')->required()]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// On ProjectResource
public static function relationManagers(): array
{
return [TasksRelationManager::class];
}2
3
4
5
Relasi yang sama sebagai nested resource:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Tasks;
use App\Models\Task;
use App\Panels\Admin\Resources\Projects\ProjectResource;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\Resource;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class TaskResource extends Resource
{
protected static string $model = Task::class;
protected static ?string $parentResource = ProjectResource::class;
protected static ?string $parentRelationship = 'tasks';
public static function table(TableSchema $table): TableSchema
{
return $table->columns([TextColumn::make('name')->searchable()->sortable()]);
}
public static function form(FormSchema $schema): FormSchema
{
return $schema->schema([TextInput::make('name')->required()]);
}
/**
* @return array<string, class-string>
*/
public static function pages(): array
{
return [
'index' => ListTasks::class,
'create' => CreateTask::class,
'edit' => EditTask::class,
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Pendekatan pertama menghasilkan table pada /admin/projects/7. Pendekatan kedua menghasilkan /admin/projects/7/tasks, /admin/projects/7/tasks/create, dan /admin/projects/7/tasks/12/edit.
Perbandingan langsung
| Relation manager | Nested resource | |
|---|---|---|
| Dideklarasikan oleh | relationManagers() pada owner | $parentResource pada child |
| Class | RelationManager | Resource |
| Muncul di | Di bawah view/edit page owner dan opsional pada ManageRelatedRecords page | Memiliki page sendiri di bawah URL parent |
| Editing | Dialog per record | Full edit page |
| Creating | Dialog, disimpan melalui relation | Create page yang di-scope oleh parent |
| Scope berasal dari | RelationManager::query() → $owner->{relation}() | Resource::query() → $parent->{relation}() |
| Owner/parent di-resolve oleh | Relation endpoint melalui query() milik owner resource | Middleware ResolveParentRecord melalui query() milik parent resource |
| Sidebar | Tidak ada | Tidak ada |
| Membership abilities | attachAny, detach, associateAny, dissociate pada policy owner | tidak ada — child dibuat dan dihapus, bukan di-join |
| Write dikirim ke | {panel}/relations/* | Route page resource sendiri dan {panel}/actions/* dengan key parent |
| Pivot columns | Didukung melalui pivotForm() | Tidak didukung |
| Attach / associate | Didukung | Tidak didukung |
| Table tabs, toolbar actions, editable cells, drag reordering | Tidak di-wire | Didukung seperti resource index biasa |
Dua cara menyatakan "scoped"
Keduanya sampai pada jaminan yang sama melalui jalur berbeda.
Relation manager selalu dimulai dari relation milik owner dan tidak keluar dari scope tersebut:
public static function query(Model $owner): Builder
{
return static::relation($owner)->getQuery(); // $owner->tasks()
}2
3
4
Nested resource dimulai dari relation milik parent yang di-resolve dari route parameter yang sudah dibind:
projects/{parentRecord}/tasks// Resource::query() for a nested resource, roughly:
return static::parentRelation()->getQuery(); // ParentRecord::require() → $parent->tasks()2
Dalam kedua pendekatan, record di bawah owner lain tidak pernah masuk builder. Hasilnya 404 tanpa setiap page perlu membuat pemeriksaan sendiri:
POST /admin/relations/action { relation: 'tasks', record: 7, related: 12 } → 404 when task 12 is project 8's
GET /admin/projects/7/tasks/12/edit → 404, same reason2
Middleware nested resource juga meng-authorize parent melalui canView() milik parent resource. Ini adalah padanan nested untuk relation endpoint yang selalu memanggil Resource::canView($owner) sebelum melakukan pekerjaan lain.
Dua cara menanyakan "bolehkah saya?"
| Pertanyaan | Relation manager | Nested resource |
|---|---|---|
| Bolehkah owner/parent diakses | Resource::canView($owner) pada owner resource | canView() pada parent resource, di middleware |
| Bolehkah child dibaca | RelationManager::canViewAny($owner) → viewAny pada related model | Resource::canViewAny() pada child resource |
| Bolehkah satu child diedit | canEdit($owner, $record) → update pada record | canEdit($record) pada child resource |
| Bolehkah existing record dihubungkan | attachAny / associateAny pada policy owner | bukan pertanyaan yang dimiliki nested resource |
Relation manager memiliki membership abilities karena relasi dapat menerima atau kehilangan member tanpa kedua record dihapus. Nested resource tidak memiliki operasi seperti itu: record dibuat di bawah parent lalu dapat dihapus; memindahkan child berarti mengedit foreign key-nya seperti field biasa.
Detail lengkap: Related record policies dan Resource authorization.
Cara memilih
Gunakan relation manager ketika:
- Child cukup dibaca dan diedit langsung di tempat — beberapa field yang masih nyaman ditaruh dalam dialog.
- Relasi many-to-many atau memiliki pivot columns.
- Existing record di-attach/detach, bukan dibuat lalu dihancurkan.
- Child tidak membutuhkan URL sendiri yang perlu di-bookmark atau dibagikan.
Gunakan nested resource ketika:
- Child memiliki kedalaman nyata: filter sendiri, tabs sendiri, form panjang, dan action sendiri.
- User perlu membuka atau membagikan URL satu child tertentu.
- Form child terlalu panjang untuk dialog.
- Anda membutuhkan index dengan tabs, editable cells, atau drag reordering — relation table tidak me-wire fitur tersebut.
Gunakan keduanya ketika child layak memiliki page sendiri tetapi tetap perlu ringkasan cepat di samping parent. Tidak ada larangan nested resource memiliki relation manager sendiri.
Menggunakan keduanya sekaligus
ManageRelatedRecords page dan nested resource dapat hidup bersama selama path-nya berbeda:
/admin/projects/7/tasks ManageProjectTasks (relation page)
/admin/projects/7/nested-tasks TaskResource (nested resource)2
Dua Resource/Page tidak boleh mengklaim path yang sama. projects/{record}/tasks dan projects/{parentRecord}/tasks adalah bentuk yang sama bagi router — nama parameter dibuang saat bentuk route dibandingkan. Laravel hanya akan mencocokkan route yang diregistrasikan lebih dulu dan yang lain tidak pernah terjangkau. Karena itu PanelRouteRegistrar membandingkan normalized route shape per Panel dan melempar exception saat boot:
PanelRegistrationException: The path [projects/{parentRecord}/tasks] is registered by both
[App\...\ProjectResource] and [App\...\TaskResource]. Only the first would ever match.
Give one of them a different slug or route path.2
3
Berikan slug berbeda pada nested resource atau ubah $routePath relation page.
Migrasi dari satu pendekatan ke pendekatan lain
Dari relation manager ke nested resource:
- Buat child resource dengan
$parentResourcedan$parentRelationship. - Pindahkan columns/filter dari
RelationManager::table()keResource::table(), tanpa parameter$owner. - Pindahkan field dari
RelationManager::form()keResource::form(), juga tanpa$owner. - Ganti relation action menjadi resource action:
EditRelatedActionmenjadi page,DeleteRelatedActionmenjadiPandaPanel\Actions\DeleteAction. - Gunakan slug yang tidak berbenturan dengan relation page.
- Hapus manager dari
relationManagers(), atau pertahankan hanya sebagai inline summary.
Untuk arah sebaliknya, pindahkan field kembali dan tambahkan parameter Model $owner. Foreign key field — jika sebelumnya ada pada form nested resource — dihapus karena relation manager menyimpan record melalui relation, sehingga owner key bukan form input.
Hal yang perlu diperhatikan
- Keduanya tidak muncul di sidebar. Nested resource tidak memiliki "all tasks" global untuk dibuka, sedangkan relation manager bukan Resource. Navigasi menuju keduanya dimulai dari parent record.
- Nested resource tetap membutuhkan policy sendiri.
canView()pada parent hanya menentukan apakah URL boleh dimasuki; ability child menentukan apa yang boleh dilakukan di dalamnya. Resource::url()untuk nested resource membutuhkan parent jika dipanggil di luar request. Tidak ada bound parent untuk dijadikan fallback — berikan argumentparent:.- Action nested resource membawa
parentpada payload. Action endpoint hanya satu per Panel dan tidak mempunyai parent segment pada URL, sehingga table harus mengirimresource.parentKeybersama setiap action. Parent yang hilang menghasilkan 422. - Pivot columns menyingkirkan nested resource sebagai pilihan. Nested resource di-scope oleh foreign key; tidak ada tempat untuk join-row attribute pada form miliknya.
- Relation manager tidak melalui discovery. Nested resource ditemukan seperti Resource lain; relation manager hanya ada ketika
relationManagers()mendeklarasikannya.