Relation Managers
Relation manager merepresentasikan related records milik satu owner record: sebuah table dengan schema, action, authorization, dan scope miliknya sendiri, semuanya dibatasi hanya pada satu owner. Gunakan relation manager ketika sebuah record memiliki child/related records yang lebih tepat dibaca dan diedit di samping owner daripada dijadikan Resource terpisah.
Relation manager minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\RelationManagers;
use App\Panels\Admin\Resources\Users\UserResource;
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 PostsRelationManager extends RelationManager
{
protected static string $relationship = 'posts';
protected static ?string $recordTitleAttribute = 'title';
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([
TextColumn::make('title')->searchable()->sortable(),
])
->recordActions([
EditRelatedAction::make(UserResource::class, self::class, $owner),
DeleteRelatedAction::make(self::class, $owner),
]);
}
public static function form(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([
TextInput::make('title')->required()->maxLength(255),
]);
}
}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
Daftarkan hanya pada Resource pemiliknya:
use PandaPanel\Resources\RelationManager;
/**
* @return list<class-string<RelationManager>>
*/
public static function relationManagers(): array
{
return [PostsRelationManager::class];
}2
3
4
5
6
7
8
9
Table sekarang muncul di bawah view dan edit page milik user. Tidak ada registration lain. Resource::relationManagers() adalah satu-satunya daftar yang diakui; seluruh relation table, endpoint, dan relation page di-resolve melalui daftar tersebut.
Relation adalah scope
RelationManager::query() dimulai dari $owner->{relationship}() dan menjadi satu-satunya jalur menuju related record — perannya sama dengan Resource::query() pada Resource.
public static function query(Model $owner): Builder;PostsRelationManager::query($user); // Builder over $user->posts()
PostsRelationManager::resolveRecord($user, 12); // ?Model — null when post 12 is someone else's2
Key yang sebenarnya milik owner lain di-resolve menjadi nothing, bukan row milik orang lain. Karena itu page maupun endpoint tidak perlu menulis check tambahan:
POST /admin/relations/action
{ "resource": "users", "record": 3, "relation": "posts", "action": "delete", "related": 99 }
→ 404 when post 99 belongs to user 4, and nothing is deleted2
3
Deklarasi
Seluruh konfigurasi manager dideklarasikan sebagai static property pada class.
| Property | Type | Default | Arti |
|---|---|---|---|
$relationship | string | wajib | Nama method relation pada owner model |
$key | ?string | Str::kebab($relationship) | Key yang digunakan untuk meng-address manager pada URL dan action payload |
$title | ?string | Str::headline($relationship) | Heading di atas table |
$icon | ?string | null | Icon registry key, digunakan pada record sub-navigation |
$recordTitleAttribute | ?string | 'name' saat title record dibaca | Attribute yang digunakan untuk menamai record pada option list dan confirmation |
$with | list<string> | [] | Relation yang di-eager-load pada setiap row |
$softDeletes | bool | false | Mengaktifkan arti dari trashed filter serta restore/force-delete action |
final class BlogPostsRelationManager extends RelationManager
{
protected static string $relationship = 'blogPosts';
// Without this the key would be 'blog-posts'.
protected static ?string $key = 'posts';
protected static ?string $title = 'Articles';
protected static ?string $icon = 'file-text';
protected static ?string $recordTitleAttribute = 'title';
/** @var list<string> */
protected static array $with = ['author'];
protected static bool $softDeletes = true;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$with ada untuk alasan yang sama seperti Resource::$with: column yang men-serialize value dari relation lain akan melakukan lazy load satu kali per row jika relation tidak di-eager-load. $softDeletes sengaja dideklarasikan, bukan dideteksi otomatis. Related model yang memakai SoftDeletes untuk kebutuhan lain tidak seharusnya tiba-tiba mendapatkan filter dan action yang tidak pernah dimaksud manager. Lihat Soft deleted relations.
Dua schema
abstract public static function table(TableSchema $table, Model $owner): TableSchema;
public static function form(FormSchema $schema, Model $owner): FormSchema;
public static function pivotForm(FormSchema $schema, Model $owner): FormSchema;2
3
4
5
table() bersifat abstract karena manager tanpa table tidak memiliki sesuatu untuk ditampilkan. form() dan pivotForm() secara default mengembalikan schema tanpa perubahan. Manager yang hanya melakukan list, attach, atau detach tidak memerlukan form, dan inherited empty form tidak boleh otomatis menghasilkan create button yang menyimpan nothing.
Owner dikirim ke ketiga method karena relation action membahas pasangan owner + related record. Apakah sebuah row boleh di-detach adalah pertanyaan dengan dua subject; Action yang tidak membawa owner tidak dapat menanyakan ability tersebut.
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([TextColumn::make('title')])
->recordActions([
// Both arguments are needed: the manager for the scope, the
// owner for the ability.
DeleteRelatedAction::make(self::class, $owner),
]);
}2
3
4
5
6
7
8
9
10
Lihat Relation tables, Relation forms, dan Pivot fields.
Identity dan lookup
| Method | Signature | Return |
|---|---|---|
relationship() | static relationship(): string | Nama relation yang dideklarasikan |
key() | static key(): string | $key, atau relationship dalam kebab-case |
title() | static title(): string | $title, atau relationship yang diubah menjadi headline |
icon() | static icon(): ?string | $icon |
recordTitle() | static recordTitle(Model $record): string | Title attribute record, atau key jika value bukan scalar |
withRelations() | static withRelations(): list<string> | $with |
PostsRelationManager::key(); // 'posts'
PostsRelationManager::title(); // 'Posts'
PostsRelationManager::recordTitle($post); // 'Hello world'2
3
Membaca relation
| Method | Signature | Catatan |
|---|---|---|
relation() | static relation(Model $owner): Relation | Melempar PanelRegistrationException jika nama tersebut bukan relation pada owner |
query() | static query(Model $owner): Builder | Builder relation dengan $with diterapkan |
relationForTable() | static relationForTable(Model $owner): Relation | Object relation dengan $with, digunakan table untuk pagination |
resolveRecord() | static resolveRecord(Model $owner, int|string $key): ?Model | Mengembalikan null, bukan exception |
getRelatedModel() | static getRelatedModel(Model $owner): class-string<Model> | Class related model |
use PandaPanel\Resources\RelationManager;
$relation = PostsRelationManager::relation($user); // HasMany
$builder = PostsRelationManager::query($user); // Builder
$related = PostsRelationManager::getRelatedModel($user); // App\Models\Post::class2
3
4
5
Table melakukan pagination melalui relationForTable(), bukan query(). Pada many-to-many, pivot di-hydrate di dalam BelongsToMany::paginate(). Builder yang sudah dilepas dari relation menghasilkan row dengan pivot column yang semuanya terbaca null.
resolveRecord() mengembalikan null daripada langsung abort karena caller yang menentukan arti record hilang: 404 untuk row action, atau record yang dilewati dalam flow tertentu. Pada manager yang mendukung soft delete, method ini juga melepas SoftDeletingScope karena lookup yang tidak dapat melihat trashed record tidak akan pernah bisa melakukan restore.
Bentuk relation
Bentuk Eloquent relation, bukan class manager, yang menentukan operation apa saja yang tersedia.
| Method | Signature | True untuk |
|---|---|---|
isManyToMany() | static isManyToMany(Model $owner): bool | BelongsToMany dan MorphToMany |
isOneToMany() | static isOneToMany(Model $owner): bool | HasMany, HasOne, dan morph equivalent-nya (HasOneOrMany) |
usesSoftDeletes() | static usesSoftDeletes(Model $owner): bool | $softDeletes dan related model benar-benar memakai trait SoftDeletes |
LabelsRelationManager::isManyToMany($project); // true — belongsToMany
TasksRelationManager::isManyToMany($project); // false — hasMany
TasksRelationManager::isOneToMany($project); // true2
3
Attach dan associate saling eksklusif secara konstruksi. Masing-masing disembunyikan pada relation shape milik pasangan lainnya, sehingga satu relasi hanya menawarkan satu cara untuk membawa existing record ke dalam relasi.
Operations
| Action class | Relation | Yang dilakukan |
|---|---|---|
CreateRelatedAction | apa pun | Membuat record melalui relation sehingga form tidak mendeklarasikan foreign key |
EditRelatedAction | apa pun | Mengedit related record dan pivot row jika ada |
DeleteRelatedAction | apa pun | Menghapus related record itu sendiri |
AttachAction / DetachAction / DetachBulkAction | many-to-many | Menambah/menghapus join row tanpa menghapus kedua record |
AssociateAction / DissociateAction | one-to-many | Mengisi/mengosongkan foreign key child |
RestoreAction / ForceDeleteAction beserta bulk variant | soft-deleting | Membatalkan atau menyelesaikan soft delete |
Seluruh action berada di namespace PandaPanel\Actions\Relations. Create, attach, dan associate adalah header actions yang di-resolve framework, bukan dideklarasikan manager. Jika manager harus menuliskannya sendiri, developer dapat tanpa sengaja menawarkan attach pada hasMany. Action lainnya ditaruh pada recordActions() dan bulkActions().
Lihat Attach dan detach, Associate dan dissociate, dan Soft deleted relations.
Attachable options
public static function attachableOptions(
Model $owner,
?string $search = null,
int $limit = 50,
): array; // list<array{value: string, label: string}>2
3
4
5
Method mengembalikan seluruh related record yang belum berada dalam relation, diberi label melalui recordTitle(), dan diurutkan berdasarkan title attribute. Search menggunakan like pada attribute yang sama, dengan \, %, dan _ di-escape agar search term tidak dapat memperluas match menggunakan wildcard SQL.
LabelsRelationManager::attachableOptions($project);
// [['value' => '2', 'label' => 'Later'], ...]
LabelsRelationManager::attachableOptions($project, 'urg', limit: 10);2
3
4
Browser hanya menerima pasangan value/label dan tidak pernah menerima query. Attach dialog diisi dari method ini dan searchable select memanggil method yang sama melalui options endpoint. Lihat Relation forms.
Authorization
Ada dua jenis pertanyaan dengan subject berbeda dan keduanya sengaja dipisahkan.
| Ability | Ditanyakan kepada | Method manager |
|---|---|---|
viewAny | related model | canViewAny(Model $owner): bool |
view | related record | canView(Model $owner, Model $record): bool |
create | related model | canCreate(Model $owner): bool |
update | related record | canEdit(Model $owner, Model $record): bool |
delete | related record | canDelete(Model $owner, Model $record): bool |
restore | related record | canRestore(Model $owner, Model $record): bool |
forceDelete | related record | canForceDelete(Model $owner, Model $record): bool |
attachAny | owner | canAttach(Model $owner): bool |
detach | owner, bersama record | canDetach(Model $owner, Model $record): bool |
associateAny | owner | canAssociate(Model $owner): bool |
dissociate | owner, bersama record | canDissociate(Model $owner, Model $record): bool |
Apakah tag boleh dipasang pada post adalah urusan post, bukan tag. Mengakses relation sama sekali juga membutuhkan Resource::canView() pada owner. Tanpa check ini, relation endpoint dapat menjadi jalan menghindari refused view. canViewAny() berjalan sebelum query, sehingga manager yang ditolak tidak muncul pada page dan tidak menjalankan query.
Semua ability melewati RelationManager::authorize(), yang mendelegasikan ke PandaPanel\Support\PolicyGate, sehingga Panel::strictAuthorization() mencakup relation ability sama seperti record ability. Detail lengkap ada di Related record policies.
Tempat manager muncul
| Surface | Yang ditampilkan |
|---|---|
Page ViewRecord dan EditRecord | Seluruh manager yang dideklarasikan Resource, melalui prop relations |
ManageRelatedRecords page | Satu manager tertentu melalui prop relation pada URL sendiri |
{panel}/relations/* | Empat endpoint untuk form dan write relation manager |
Resource yang memiliki relation page untuk suatu manager tetap mendapatkan manager tersebut secara inline pada record page. Page yang menentukan di mana manager muncul. Override ResourcePage::relationTables() jika sebuah page hanya ingin menampilkan sebagian manager.
Membuat manager dengan generator
php artisan make:panel-relation-manager posts --panel=Admin --resource=Users
php artisan make:panel-relation-manager labels --panel=Admin --resource=Projects --type=belongs-to-many
php artisan make:panel-relation-manager tasks --panel=Admin --resource=Projects --soft-deletes --page2
3
| Option | Default | Efek |
|---|---|---|
--panel= | wajib | Directory Panel tempat manager ditulis |
--resource= | wajib | Owning Resource; singular maupun plural menghasilkan Resource yang sama |
--type= | has-many | has-many atau belongs-to-many; menentukan apakah detach action dan pivotForm() dibuat |
--soft-deletes | off | Menambahkan TrashedFilter, RestoreAction, dan ForceDeleteAction |
--page | off | Juga membuat ManageRelatedRecords page |
--force | off | Menimpa file yang sudah ada |
Relation shape dijadikan option karena generator tidak dapat mengetahui bentuk relation hanya dari nama class. Memilih type salah akan menghasilkan manager yang menawarkan operation yang relation tidak dapat lakukan, bukan manager yang diam-diam kehilangan fitur. Command mencetak satu hal yang tidak dapat dilakukan otomatis: menambahkan class ke relationManagers().
Hal yang perlu diperhatikan
- Manager yang tidak dideklarasikan Resource dianggap tidak ada. Relation endpoint me-resolve key melalui
Resource::relationManager(). Request yang menyebut manager tidak terdaftar menghasilkan 404, apa pun ejaannya. - Dua manager tidak boleh berbagi key.
Resource::relationManager()melemparPanelRegistrationExceptiondaripada membiarkan hasil bergantung pada declaration order. Berikan$keyeksplisit pada salah satunya. $relationshipdiperiksa saat digunakan. Nama yang bukan method pada owner model atau method yang tidak mengembalikanRelationmelemparPanelRegistrationExceptionyang menyebut owner, relation, dan manager.table()danform()bersifat static. Owner diterima sebagai argument; tidak ada$this->getOwnerRecord().- Relation manager tidak melalui discovery. Berbeda dari Resource, Page, dan Widget, manager hanya ditemukan melalui
relationManagers(). $recordTitleAttributefallback kenamesaat dibaca. Related model tanpa columnnamefallback ke record key alih-alih error. Pada option list hal ini terlihat seperti bug label; deklarasikan attribute yang benar.- Owner di-load melalui
Resource::query(). Record yang dikeluarkan scope Resource juga menghasilkan 404 untuk relasinya.