Soft Deleted Relations
Relasi yang record-nya masuk trash alih-alih langsung dihancurkan membutuhkan tiga hal sekaligus: filter yang dapat menampilkan deleted record, Action untuk mengembalikan atau menghapusnya secara permanen, dan lookup yang tetap dapat melihat trashed record tersebut. protected static bool $softDeletes = true menyatakan bahwa soft-delete behavior bermakna untuk manager ini; filter dan Action tetap harus Anda deklarasikan. Gunakan fitur ini ketika related model memakai Illuminate\Database\Eloquent\SoftDeletes dan relation memang dimaksudkan untuk mengekspos status deleted tersebut.
Manager dengan soft delete
<?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\Actions\Relations\ForceDeleteAction;
use PandaPanel\Actions\Relations\ForceDeleteBulkAction;
use PandaPanel\Actions\Relations\RestoreAction;
use PandaPanel\Actions\Relations\RestoreBulkAction;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\RelationManager;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Filters\TrashedFilter;
use PandaPanel\Tables\TableSchema;
final class TasksRelationManager extends RelationManager
{
protected static string $relationship = 'tasks';
protected static ?string $recordTitleAttribute = 'name';
protected static bool $softDeletes = true;
public static function table(TableSchema $table, Model $owner): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
])
->filters([
TrashedFilter::make('trashed'),
])
->recordActions([
EditRelatedAction::make(ProjectResource::class, self::class, $owner),
RestoreAction::make(self::class, $owner),
ForceDeleteAction::make(self::class, $owner),
DeleteRelatedAction::make(self::class, $owner),
])
->bulkActions([
RestoreBulkAction::make(self::class, $owner),
ForceDeleteBulkAction::make(self::class, $owner),
]);
}
public static function form(FormSchema $schema, Model $owner): FormSchema
{
return $schema->schema([
TextInput::make('name')->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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Deklarasi
protected static bool $softDeletes = false;Fitur ini dideklarasikan, bukan dideteksi otomatis. Related model yang menggunakan SoftDeletes untuk kebutuhan lain tidak seharusnya tiba-tiba mendapatkan filter yang manager tidak pernah dimaksudkan untuk menampilkan.
public static function usesSoftDeletes(Model $owner): bool;Dua syarat harus sama-sama benar: property manager dan related model benar-benar menggunakan trait:
TasksRelationManager::usesSoftDeletes($project); // true when Task uses SoftDeletesusesSoftDeletes() digunakan bulk restore dan bulk force-delete Action untuk visibility. Karena itu $softDeletes = true pada model yang tidak memiliki trait menghasilkan tidak adanya bulk button, bukan button yang baru crash setelah diklik.
Filter-lah yang membuat deleted record terlihat
use PandaPanel\Tables\Filters\TrashedFilter;
->filters([TrashedFilter::make('trashed')])2
3
| Constant | Value | Query |
|---|---|---|
TrashedFilter::WITHOUT | without | Scope withoutTrashed Eloquent tetap aktif |
TrashedFilter::WITH | with | Menghapus SoftDeletingScope |
TrashedFilter::ONLY | only | Menghapus scope lalu whereNotNull(deleted_at) |
Default state adalah without, sehingga table yang mendeklarasikan filter tetap menyembunyikan deleted row sampai user memilih untuk melihatnya. Pada relation table, state berada pada namespace relation:
/admin/projects/7?relations[tasks][filters][trashed]=onlyTiga value tersebut adalah seluruh vocabulary yang valid. sanitize() menolak value lain sehingga request tidak dapat memperlebar query menggunakan value yang tidak pernah didefinisikan filter.
Tanpa filter, RestoreAction dan ForceDeleteAction tidak pernah memiliki record tempat keduanya dapat muncul. Keduanya hidden pada record live; table yang tidak pernah menampilkan trashed row tidak pernah menampilkan kedua button tersebut. Generator menambahkan filter bersama Action untuk alasan ini.
RestoreAction
use PandaPanel\Actions\Relations\RestoreAction;
RestoreAction::make(string $manager, Model $owner): Action;2
3
| Properti | Nilai |
|---|---|
| Name | restore |
| Label | Restore |
| Icon | rotate-ccw |
| Variant | ActionVariant::Ghost |
| Success message | Record restored. |
| Terlihat ketika | record sedang trashed |
| Diotorisasi oleh | RelationManager::canRestore($owner, $record) → restore pada related record |
POST /{panel}/relations/action
{ "resource": "projects", "record": 7, "relation": "tasks", "action": "restore", "related": 12 }2
Action disembunyikan untuk live record, sehingga sebuah row menampilkan restore atau delete sesuai state-nya, bukan keduanya sekaligus.
ForceDeleteAction
use PandaPanel\Actions\Relations\ForceDeleteAction;
ForceDeleteAction::make(string $manager, Model $owner): Action;2
3
| Properti | Nilai |
|---|---|
| Name | forceDelete |
| Label | Delete permanently |
| Icon | trash-2 |
| Variant | ActionVariant::Destructive |
| Confirmation | "Delete this record permanently?" — "This cannot be undone and the record cannot be restored afterwards." |
| Success message | Record permanently deleted. |
| Terlihat ketika | record sedang trashed |
| Diotorisasi oleh | RelationManager::canForceDelete($owner, $record) → forceDelete pada related record |
Action hanya ditawarkan pada record yang sudah deleted. Force-delete terhadap record live akan melewati tahap recoverable yang justru merupakan alasan model menggunakan soft delete.
Bulk variants
use PandaPanel\Actions\Relations\ForceDeleteBulkAction;
use PandaPanel\Actions\Relations\RestoreBulkAction;
RestoreBulkAction::make(string $manager, Model $owner): Action;
ForceDeleteBulkAction::make(string $manager, Model $owner): Action;2
3
4
5
RestoreBulkAction | ForceDeleteBulkAction | |
|---|---|---|
| Name | restore | forceDelete |
| Label | Restore selected | Delete selected permanently |
| Variant | ActionVariant::Outline | ActionVariant::Destructive |
| Confirmation | none | "Delete the selected records permanently?" |
| Terlihat ketika | usesSoftDeletes($owner) | usesSoftDeletes($owner) |
| Ability per record | canRestore | canForceDelete |
Keduanya meng-authorize seluruh record sebelum menulis satu pun dan menjalankan set dalam satu DB::transaction():
POST /{panel}/relations/bulk
{ "resource": "projects", "record": 7, "relation": "tasks", "action": "restore", "records": [12, 13] }2
Selection yang memiliki satu record terlarang tidak me-restore apa pun dan menghasilkan 403, bukan me-restore record yang diizinkan lalu gagal di tengah.
Me-resolve trashed record
public static function resolveRecord(Model $owner, int|string $key): ?Model;Pada manager yang mendukung soft delete, lookup melepas SoftDeletingScope. Restore adalah operation terhadap sesuatu yang disembunyikan default scope; lookup yang tidak dapat melihat trashed record tidak akan pernah dapat mengembalikannya.
$task->delete();
TasksRelationManager::resolveRecord($project, $task->getKey()); // the trashed Task2
3
Operation apa yang diizinkan terhadap record tersebut tetap merupakan pertanyaan authorization milik Action masing-masing. Endpoint dapat berhasil me-resolve trashed record tetapi ability masih dapat menolaknya. RestoreAction juga tetap hidden untuk record yang tidak trashed.
TrashedRecord
Pertanyaan soft-delete digunakan melalui satu helper yang sama oleh Resource Action dan relation Action:
use PandaPanel\Support\TrashedRecord;
TrashedRecord::supports($record); // bool — does this model answer `trashed()` at all
TrashedRecord::isTrashed($record); // bool — false for a model without the trait
TrashedRecord::restore($record); // no-op unless the record is trashed and can restore
TrashedRecord::forceDelete($record); // always safe: on a plain model it is delete()2
3
4
5
6
Check dilakukan per method, bukan satu class_uses_recursive, karena model dapat mengimplementasikan soft deletion dengan caranya sendiri. Menanyakan "apakah model dapat menjawab operasi ini?" lebih akurat daripada "apakah model memakai implementasi tertentu?". Inilah yang membuat manager dengan $softDeletes = true pada model tanpa trait menghasilkan Action yang tidak pernah muncul, bukan Action yang 500 saat diklik.
Hal yang perlu diperhatikan
- Filter tidak ditambahkan otomatis.
$softDeletes = truemembuat soft-delete Action bermakna;TrashedFilter::make('trashed')yang membuat trashed record dapat dilihat. Deklarasikan keduanya atau restore Action tidak pernah muncul. $softDeletessaja belum cukup.usesSoftDeletes()juga memeriksa traitSoftDeletespada related model dan bulk Action hidden bila syarat tersebut gagal.DeleteRelatedActionmelakukan soft delete ketika model mendukungnya. Handler memanggil$record->delete(), sehingga pada soft-deleting model record masuk trash, bukan dihancurkan. Inilah alasan force-delete Action tersedia.- Restore bersifat idempotent. POST restore terhadap live record sukses tetapi tidak mengubah apa pun; handler hanya memanggil restore bila record memang trashed.
- Trashed row tidak masuk
attachableOptions(). Option query menggunakan plainnewQuery()pada related model sehingga default scope berlaku. Trashed record tidak dapat di-attach kembali sebelum direstore. - Soft delete owner adalah pertanyaan terpisah. Apakah owner yang trashed dapat dijangkau ditentukan melalui
Resource::query()danResource::$softDeletes. Lihat Resource soft deletes.