Relation Manager
Halaman ini membahas dua relation manager pada Product Resource dari Product Resource: variants yang dimiliki product (hasMany) dan tags yang dibagikan antar-product (belongsToMany dengan pivot column). Gunakan relation manager ketika sebuah record memiliki child/related records yang lebih masuk akal ditampilkan di samping owner-nya daripada dibuat menjadi resource terpisah. Kedua bentuk relation dibahas bersamaan karena membutuhkan action, policy, dan pertanyaan authorization yang berbeda. Memilih bentuk relation yang salah adalah kesalahan utama yang ingin dicegah halaman ini.
Contoh minimal yang berfungsi
php artisan make:panel-relation-manager variants --panel=Admin --resource=Product --type=has-many// app/Panels/Admin/Resources/Products/ProductResource.php
use PandaPanel\Resources\RelationManager;
/**
* @return list<class-string<RelationManager>>
*/
public static function relationManagers(): array
{
return [VariantsRelationManager::class];
}2
3
4
5
6
7
8
9
10
11
Table relation sekarang muncul di bawah view dan edit page milik product. Resource::relationManagers() adalah satu-satunya daftar relation manager yang diakui. Manager yang tidak dideklarasikan resource tidak dapat di-address oleh request apa pun.
Generator
php artisan make:panel-relation-manager
{name} # the relation method on the owner model: variants
--panel= # required
--resource= # required; Product or Products, either works
--type=has-many # has-many or belongs-to-many
--soft-deletes # trashed filter, restore and force-delete actions
--page # also generate a ManageRelatedRecords page
--force2
3
4
5
6
7
8
--type merupakan keputusan eksplisit, bukan sesuatu yang diturunkan dari nama relation. Bentuk relation menentukan action yang valid, dan generator tidak dapat mengetahui bentuk itu hanya dari nama class. hasMany membuat dan menghapus record; belongsToMany melakukan attach dan detach. Memilih tipe yang salah menghasilkan manager yang menawarkan operasi yang memang tidak dapat dilakukan relation — kegagalan yang terlihat jelas, bukan fitur yang diam-diam hilang.
--resource di-singularize lalu diubah menjadi StudlyCase, kemudian di-pluralize kembali untuk directory. Karena itu --resource=Products dan --resource=Product sama-sama menemukan app/Panels/Admin/Resources/Products/.
Command diakhiri dengan reminder untuk satu langkah yang tidak dapat dilakukan generator secara otomatis:
Add VariantsRelationManager::class to ProductResource::relationManagers(). Nothing is registered until you do.Model
// app/Models/Variant.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Variant extends Model
{
protected $fillable = ['name', 'sku', 'price_cents', 'stock'];
/**
* @return BelongsTo<Product, $this>
*/
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/Models/Product.php — the two relations the managers read
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @return HasMany<Variant, $this>
*/
public function variants(): HasMany
{
return $this->hasMany(Variant::class);
}
/**
* @return BelongsToMany<Tag, $this>
*/
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withPivot('position');
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
withPivot('position') membuat pivot column dapat dibaca melalui pivot.position pada table column dan dapat ditulis melalui pivotForm(). Tanpanya Eloquent tidak men-select column tersebut sehingga cell akan terlihat null.
Manager hasMany
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Products\RelationManagers;
use App\Panels\Admin\Resources\Products\ProductResource;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Relations\DeleteRelatedAction;
use PandaPanel\Actions\Relations\DissociateAction;
use PandaPanel\Actions\Relations\EditRelatedAction;
use PandaPanel\Forms\Components\NumberInput;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\RelationManager;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\Alignment;
use PandaPanel\Tables\TableSchema;
final class VariantsRelationManager extends RelationManager
{
protected static string $relationship = 'variants';
protected static ?string $title = 'Variants';
protected static ?string $icon = 'layers';
protected static ?string $recordTitleAttribute = 'name';
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('sku')->label('SKU')->searchable(),
NumberColumn::make('price_cents')
->label('Price')
->prefix('$')
->decimals(2)
->formatUsing(static fn (mixed $value): float => (int) $value / 100)
->alignment(Alignment::End),
NumberColumn::make('stock')->alignment(Alignment::End)->sortable(),
])
->recordActions([
// Both arguments are needed: the manager for the scope, the
// owner for the ability.
EditRelatedAction::make(ProductResource::class, self::class, $owner),
DissociateAction::make(self::class, $owner),
DeleteRelatedAction::make(self::class, $owner),
])
->emptyState(
heading: 'No variants yet',
description: 'Add sizes, colours, or bundles for this product.',
);
}
public static function form(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([
TextInput::make('name')->required()->maxLength(255),
TextInput::make('sku')->label('SKU')->required()->maxLength(64),
NumberInput::make('price_cents')->label('Price (cents)')->integer()->min(0)->required(),
NumberInput::make('stock')->integer()->min(0)->default(0)->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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Tidak ada field product_id. Seluruh write dilakukan melalui $owner->variants(), sehingga foreign key ditentukan relation. Menambahkan field foreign key justru memberi request kesempatan untuk mencoba mengarahkannya ke owner lain.
Manager belongsToMany
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Products\RelationManagers;
use App\Panels\Admin\Resources\Products\ProductResource;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Relations\DetachAction;
use PandaPanel\Actions\Relations\DetachBulkAction;
use PandaPanel\Actions\Relations\EditRelatedAction;
use PandaPanel\Forms\Components\NumberInput;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\RelationManager;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class TagsRelationManager extends RelationManager
{
protected static string $relationship = 'tags';
protected static ?string $title = 'Tags';
protected static ?string $icon = 'tag';
protected static ?string $recordTitleAttribute = 'name';
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
// Pivot columns read through the same dotted path any
// relation column uses.
TextColumn::make('pivot.position')->label('Position'),
])
->recordActions([
EditRelatedAction::make(ProductResource::class, self::class, $owner),
DetachAction::make(self::class, $owner),
])
->bulkActions([
DetachBulkAction::make(self::class, $owner),
]);
}
public static function form(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([
TextInput::make('name')->required()->maxLength(255),
]);
}
/**
* The pivot columns an attach or an edit may write. Only fields declared
* here are validated and persisted to the join row.
*/
public static function pivotForm(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([
NumberInput::make('position')->integer()->min(0)->default(0),
]);
}
}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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Key tambahan pada request body dibuang seperti pada resource form biasa. Hanya field yang dideklarasikan pivotForm() yang divalidasi dan ditulis ke join row.
Dua bagian form tetap dipisahkan. Field milik related record menggunakan nama aslinya, sedangkan field pivot ditempatkan di namespace pivot.. Dengan demikian column position pada join row tidak bertabrakan dengan field position yang mungkin juga ada pada model Tag. PandaPanel\Resources\RelationForm mendeklarasikan kedua nama tersebut:
public const RELATED_FIELD = 'related'; // the select naming the record to attach
public const PIVOT_PREFIX = 'pivot'; // where the pivot fields live2
Mendaftarkan kedua manager
/**
* @return list<class-string<RelationManager>>
*/
public static function relationManagers(): array
{
return [
VariantsRelationManager::class,
TagsRelationManager::class,
];
}2
3
4
5
6
7
8
9
10
Manager dirender mengikuti urutan deklarasi di bawah view dan edit page record. Masing-masing memiliki table state yang di-namespace berdasarkan key manager, sehingga sorting pada satu relation tidak mengubah state relation lainnya.
Semua deklarasi Relation Manager
| Property | Type | Default | Arti |
|---|---|---|---|
$relationship | string | wajib | Nama method relation pada owner model |
$key | ?string | Str::kebab($relationship) | Key yang digunakan 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 | null; saat membaca title akan memakai 'name' | Cara record dinamai pada option list dan confirmation |
$with | list<string> | [] | Relation yang eager loaded pada setiap row |
$softDeletes | bool | false | Mengaktifkan trashed filter dan restore action |
abstract public static function table(TableSchema $table, Model $owner): TableSchema;
public static function form(FormSchema $schema, Model $owner): FormSchema; // returns $schema unchanged
public static function pivotForm(FormSchema $schema, Model $owner): FormSchema; // returns $schema unchanged2
3
4
table() bersifat abstract karena manager tanpa table tidak memiliki sesuatu untuk ditampilkan. form() dan pivotForm() secara default mengembalikan schema tanpa perubahan. Manager yang hanya menampilkan, attach, atau detach relation tidak memerlukan form, dan framework tidak boleh membuat create button untuk form yang menyimpan nothing.
Owner selalu diberikan ke ketiga method karena relation action membahas pasangan owner + related record, bukan related record saja. Apakah sebuah tag boleh dilepas dari product adalah authorization question dengan dua subject; action tanpa owner tidak dapat mengajukan pertanyaan tersebut.
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 headline relationship |
icon() | static icon(): ?string | $icon |
relation() | static relation(Model $owner): Relation | instance relation; melempar jika method tersebut bukan relation |
query() | static query(Model $owner): Builder | builder relation dengan $with diterapkan |
relationForTable() | static relationForTable(Model $owner): Relation | relation asli untuk pagination |
resolveRecord() | static resolveRecord(Model $owner, int|string $key): ?Model | null jika key milik owner lain |
getRelatedModel() | static getRelatedModel(Model $owner): class-string<Model> | class related model |
recordTitle() | static recordTitle(Model $record): string | title record |
isManyToMany() | static isManyToMany(Model $owner): bool | true untuk BelongsToMany dan MorphToMany |
isOneToMany() | static isOneToMany(Model $owner): bool | true untuk HasOneOrMany |
usesSoftDeletes() | static usesSoftDeletes(Model $owner): bool | $softDeletes dan related model benar-benar menggunakan trait |
attachableOptions() | static attachableOptions(Model $owner, ?string $search = null, int $limit = 50): array | pasangan value/label record yang dapat di-attach |
Table melakukan pagination melalui relationForTable(), bukan query(). Pada many-to-many, pivot di-hydrate oleh BelongsToMany::paginate(). Builder yang dilepas dari relation akan menghasilkan row yang pivot column-nya terbaca null.
Relation adalah scope
public static function query(Model $owner): Builder
{
return static::relation($owner)->getQuery(); // plus $with
}2
3
4
Seluruh read dan write dimulai dari $owner->{relationship}(), perannya sama seperti Resource::query() bagi resource. Record milik owner lain tidak pernah berada pada builder tersebut:
VariantsRelationManager::resolveRecord($apollo, 12); // ?Model — null when variant 12 is another product'sPOST /admin/relations/action
{ "resource": "products", "record": 3, "relation": "variants", "action": "delete", "related": 99 }
→ 404 when variant 99 belongs to product 4, and nothing is deleted2
3
Owner sendiri dimuat melalui Resource::query(). Jika Product tidak dapat dijangkau oleh resource, relation endpoint juga menghasilkan 404 daripada menyajikan relation untuk record yang tidak boleh dibuka user.
Action yang tersedia dan asalnya
Create, attach, dan associate tidak dideklarasikan manual pada manager. RelationTable me-resolve ketiganya untuk setiap relation karena keberadaan action tersebut bergantung pada bentuk relation, bukan preferensi manager. Jika manager harus mendaftarkannya sendiri, developer dapat tanpa sengaja menawarkan attach pada hasMany.
CreateRelatedAction::make(string $resource, string $manager, Model $owner): Action
AttachAction::make(string $resource, string $manager, Model $owner): Action
AssociateAction::make(string $resource, string $manager, Model $owner): Action2
3
Attach dan associate saling eksklusif berdasarkan konstruksi. Masing-masing disembunyikan untuk bentuk relation yang dimiliki action lainnya, sehingga satu relation hanya menawarkan satu cara membawa existing record masuk.
Action lainnya dideklarasikan melalui recordActions() dan bulkActions() manager:
| Action | Signature | Berlaku untuk |
|---|---|---|
EditRelatedAction | make(string $resource, string $manager, Model $owner) | semua relation |
DeleteRelatedAction | make(string $manager, Model $owner) | semua relation |
DetachAction | make(string $manager, Model $owner) | many-to-many |
DetachBulkAction | make(string $manager, Model $owner) | many-to-many |
DissociateAction | make(string $manager, Model $owner) | one-to-many |
PandaPanel\Actions\Relations\RestoreAction | make(string $manager, Model $owner) | soft-deleting related model |
PandaPanel\Actions\Relations\ForceDeleteAction | make(string $manager, Model $owner) | soft-deleting related model |
Delete dan detach adalah operasi yang berbeda. Menghapus variant menghapus record. Detach tag hanya menghapus join row dan tag tetap ada. Dissociate variant mengosongkan foreign key — itulah sebabnya action ini tersedia untuk one-to-many, bukan attach.
Authorization: dua policy, dua pertanyaan
Inilah pemisahan yang paling penting. Membaca dan mengubah related record adalah ability pada policy related model. Attach/detach adalah ability pada policy owner, karena apakah sebuah Tag boleh dikaitkan ke Product adalah urusan Product, bukan Tag.
// On TagPolicy — about the tag itself.
public function viewAny(User $user): bool;
public function view(User $user, Tag $tag): bool;
public function create(User $user): bool;
public function update(User $user, Tag $tag): bool;
public function delete(User $user, Tag $tag): bool;
public function restore(User $user, Tag $tag): bool;
public function forceDelete(User $user, Tag $tag): bool;2
3
4
5
6
7
8
// On ProductPolicy — about membership of the product's relations.
public function attachAny(User $user, Product $product): bool;
public function detach(User $user, Product $product, Tag $tag): bool;
public function associateAny(User $user, Product $product): bool;
public function dissociate(User $user, Product $product, Variant $variant): bool;2
3
4
5
Method manager yang menanyakannya:
| Method manager | Ability | Policy |
|---|---|---|
canViewAny(Model $owner) | viewAny | policy related model |
canView(Model $owner, Model $record) | view | policy related model |
canCreate(Model $owner) | create | policy related model |
canEdit(Model $owner, Model $record) | update | policy related model |
canDelete(Model $owner, Model $record) | delete | policy related model |
canRestore(Model $owner, Model $record) | restore | policy related model |
canForceDelete(Model $owner, Model $record) | forceDelete | policy related model |
canAttach(Model $owner) | attachAny | policy owner |
canDetach(Model $owner, Model $record) | detach | policy owner, dengan record sebagai argument kedua |
canAssociate(Model $owner) | associateAny | policy owner |
canDissociate(Model $owner, Model $record) | dissociate | policy owner, dengan record sebagai argument kedua |
Manager yang ditolak authorization akan tidak ada dan tidak menjalankan query. RelationTable::toArray() memanggil canViewAny() terlebih dahulu dan mengembalikan null. Ini penting: manager yang query dulu lalu menyembunyikan row tetap telah membaca data.
Memberi relation page sendiri
Record page yang berisi empat relation table sekaligus biasanya sulit dibaca. ManageRelatedRecords memindahkan satu relation ke route tersendiri dan memasukkannya ke record sub-navigation.
php artisan make:panel-relation-manager variants --panel=Admin --resource=Product --type=has-many --page<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Products\Pages;
use App\Panels\Admin\Resources\Products\RelationManagers\VariantsRelationManager;
use App\Panels\Admin\Resources\Products\ProductResource;
use PandaPanel\Resources\Pages\ManageRelatedRecords;
final class ManageProductsVariants extends ManageRelatedRecords
{
protected static string $resource = ProductResource::class;
protected static string $relationManager = VariantsRelationManager::class;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Daftarkan di pages() menggunakan key relation:
public static function pages(): array
{
return [
'index' => ListProducts::class,
'create' => CreateProduct::class,
'view' => ViewProduct::class,
'edit' => EditProduct::class,
'variants' => ManageProductsVariants::class,
];
}2
3
4
5
6
7
8
9
10
Page tersedia di /admin/products/{record}/variants dan otomatis bergabung ke record sub-navigation.
Endpoint
Empat route tersedia per Panel. Semuanya me-resolve resource dan relation terhadap registry Panel saat ini:
| Route name | Method dan path | Digunakan untuk |
|---|---|---|
panel.admin.relations.form | GET /admin/relations/form | mengambil form untuk create, edit, attach, atau associate |
panel.admin.relations.save | POST /admin/relations/form | submit form |
panel.admin.relations.action | POST /admin/relations/action | menjalankan record action |
panel.admin.relations.bulk | POST /admin/relations/bulk | menjalankan bulk action |
Form endpoint mengambil context dari query string — resource, record, relation, operation — bukan dari request body. Dengan demikian field yang kebetulan bernama resource tidak dapat mengarahkan request ke resource lain.
operation menggunakan PandaPanel\Support\RelationOperation, sebuah closed set:
enum RelationOperation: string
{
case Create = 'create';
case Edit = 'edit';
case Attach = 'attach';
case Associate = 'associate';
}2
3
4
5
6
7
Setiap case menentukan schema yang dibangun, ability yang diperiksa, dan jenis write yang dilakukan. Value tidak dikenal menghasilkan 404, bukan fallback. Attach dan Associate juga memeriksa bentuk relation; attach pada one-to-many bukan operasi yang "ditolak", tetapi operasi yang memang tidak ada.
Test
<?php
declare(strict_types=1);
use App\Models\Product;
use App\Models\Tag;
use App\Models\User;
use App\Panels\Admin\Resources\Products\RelationManagers\VariantsRelationManager;
use PandaPanel\Support\RelationOperation;
beforeEach(function (): void {
$this->actingAs(User::factory()->admin()->create());
$this->product = Product::factory()->create(['name' => 'Keyboard']);
$this->other = Product::factory()->create(['name' => 'Mouse']);
});
/**
* @return array<string, mixed>
*/
function relationsFor(Product $product): array
{
return test()
->get("/admin/products/{$product->getKey()}")
->assertOk()
->viewData('page')['props']['relations'];
}
it('lists only this product\'s variants', function (): void {
$this->product->variants()->create(['name' => 'Blue', 'sku' => 'KB-B', 'price_cents' => 100, 'stock' => 1]);
$this->other->variants()->create(['name' => 'Red', 'sku' => 'MS-R', 'price_cents' => 100, 'stock' => 1]);
$variants = collect(relationsFor($this->product))->firstWhere('key', 'variants');
expect(collect($variants['rows'])->pluck('cells.name')->all())->toBe(['Blue']);
});
it('resolves nothing for a variant belonging to another product', function (): void {
$theirs = $this->other->variants()->create([
'name' => 'Red', 'sku' => 'MS-R', 'price_cents' => 100, 'stock' => 1,
]);
expect(VariantsRelationManager::resolveRecord($this->product, $theirs->getKey()))->toBeNull()
->and(VariantsRelationManager::resolveRecord($this->other, $theirs->getKey()))->not->toBeNull();
});
it('refuses an action on a variant from another product', function (): void {
$theirs = $this->other->variants()->create([
'name' => 'Red', 'sku' => 'MS-R', 'price_cents' => 100, 'stock' => 1,
]);
$this->post('/admin/relations/action', [
'resource' => 'products',
'record' => $this->product->getKey(),
'relation' => 'variants',
'action' => 'delete',
'related' => $theirs->getKey(),
])->assertNotFound();
expect($this->other->variants()->count())->toBe(1);
});
it('creates a variant through the relation, without a foreign key field', function (): void {
$this->post('/admin/relations/form?'.http_build_query([
'resource' => 'products',
'record' => $this->product->getKey(),
'relation' => 'variants',
'operation' => RelationOperation::Create->value,
]), [
'name' => 'Blue',
'sku' => 'KB-B',
'price_cents' => 12900,
'stock' => 3,
])->assertRedirect();
expect($this->product->variants()->count())->toBe(1);
});
it('writes only the pivot columns the pivot form declared', function (): void {
$tag = Tag::query()->create(['name' => 'Featured']);
$this->post('/admin/relations/form?'.http_build_query([
'resource' => 'products',
'record' => $this->product->getKey(),
'relation' => 'tags',
'operation' => RelationOperation::Attach->value,
]), [
// `related` names the record being attached; the pivot fields are
// namespaced under `pivot.` so a `position` column on the join row
// cannot collide with one on the tag itself.
'related' => $tag->getKey(),
'pivot' => ['position' => 3],
// Not a field on either form, so it is discarded rather than written.
'created_at' => '1999-01-01',
])->assertRedirect();
expect($this->product->tags()->first()?->pivot->position)->toBe(3);
});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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
php artisan test --compact --filter=Relationtests/Feature/Panel/RelationManagerTest.php adalah versi framework dari test tersebut. Fixture-nya menggunakan Project / Task / Label dan secara bersamaan mencakup hasMany, belongsToMany dengan pivot column, dan hasOne.
Hal yang perlu diperhatikan
- Tidak ada manager yang aktif sampai
relationManagers()mendeklarasikannya. Generator hanya menulis class lalu memberi reminder; relation manager tidak menggunakan discovery. --typeadalah keputusan nyata. Managerbelongs-to-manypada relationhasManymenghasilkan detach action yang tidak pernah dapat berlaku, karenaRelationOperation::Attachmemeriksa bentuk relation selain ability.$softDeletesmembutuhkan dua sisi.usesSoftDeletes()hanya true jika$softDeletes = truedan related model benar-benar menggunakan trait. Mendeklarasikan soft delete pada model yang tidak mendukungnya menghasilkan filter yang tidak melakukan apa-apa.- Restore action membutuhkan trashed filter. Filter itulah yang membuat deleted record muncul pada screen; tanpa filter, restore button tidak pernah dapat tampil.
--soft-deletesmenambahkan keduanya. pivot.positionmembutuhkanwithPivot('position'). Eloquent tidak men-select pivot column yang tidak diminta. Cell akannull, bukan melempar error.- Relation Manager bukan Resource. Tidak memiliki route sendiri kecuali Anda menambahkan
ManageRelatedRecordspage, tidak ikut global search, dan tidak memiliki navigation entry sendiri. - Kapan menggunakan nested resource. Relation manager adalah table di samping owner record. Nested resource memiliki list/create/view/edit page sendiri di bawah URL parent. Lihat Nested vs Relation Manager.