6
Siklus Soft Delete dan Restore
Model yang memakai trait SoftDeletes mendapat dua tahap tambahan pada siklus hidup record: deleting mengisi deleted_by_id sekaligus updated_by_id, dan restoring mengosongkan deleted_by_id lalu mengisi ulang updated_by_id. Kedua branch hanya berjalan ketika usingSoftDeletes() bernilai true (src/Observers/UserstampsObserver.php:42-591, src/Observers/UserstampsObserver.php:61-782). Observer sendiri dipasang lewat event booted di trait, sehingga model uji seperti Product yang memakai HasUserstamps dan SoftDeletes langsung ikut (src/Concerns/HasUserstamps.php:12-173, tests/Models/Product.php:9-194).
Kapan branch soft delete dijalankan
Penjaga branch adalah usingSoftDeletes(), yang hanya memeriksa apakah trait SoftDeletes ada di rantai class_uses_recursive() milik model (src/Concerns/HasUserstamps.php:64-675). Artinya keputusan diambil dari sifat kelas, bukan dari jenis operasi yang sedang berjalan. Nama kolom yang dipakai bukan string tetap, melainkan hasil config('userstamps.deleted_by_column') dan config('userstamps.updated_by_column') yang ditambah akhiran _id atau _type (config/userstamps.php:110-1206).
Transisi status dan kolom yang berubah
stateDiagram-v2
[*] --> active: create
active --> active: delete()
active --> trashed: delete()
trashed --> active: restore()
note right of active
deleted_by_id = null
updated_by_id = user id
end note
note right of trashed
deleted_by_id = user id
updated_by_id = user id
end note
classDef ok fill:#2d7d46,color:#fff
classDef store fill:#336791,color:#fff
classDef danger fill:#dc2626,color:#fff
class active ok
class trashed store
%% fig: Siklus status record ber-Userstamps dengan SoftDeletes
%% desc: delete() memindahkan record ke status trashed sambil mengisi deleted_by_id, restore() mengembalikannya ke active dan mengosongkan kolom itu; updated_by_id selalu diisi user aktif.Cabang deleting menulis deleted_by_id (dan deleted_by_type bila is_using_morph aktif) tanpa syarat, lalu menimpa updated_by_id dengan user yang sama (src/Observers/UserstampsObserver.php:42-591). Cabang restoring melakukan kebalikannya: deleted_by_id dan deleted_by_type diset null, sedangkan updated_by_id tetap diisi user aktif (src/Observers/UserstampsObserver.php:61-782). Kolom deleted_at tidak disentuh observer sama sekali; kolom itu diurus oleh trait SoftDeletes milik Laravel.
| Operasi | Event | deleted_by_id | updated_by_id | Catatan |
|---|---|---|---|---|
delete() pada model ber-SoftDeletes | deleting | diisi user aktif | diisi user aktif | nilai lama selalu ditimpa, tanpa pemeriksaan isClean() |
restore() | restoring | diset null | diisi user aktif | deleted_by_type juga null saat morph aktif |
save() biasa | updating | tidak disentuh | diisi hanya bila kolom masih clean | kolom yang sudah diubah manual tidak ditimpa (src/Observers/UserstampsObserver.php:30-407) |
| tanpa user terautentikasi | deleting atau restoring | null | null | getUserPrimaryValueAndClass() mengembalikan id dan type bernilai null (src/Observers/UserstampsObserver.php:91-1078) |
Penyimpanan di dalam event
Kedua branch tidak mengandalkan save() milik pemanggil. Setelah atribut diisi, observer memanggil saveWithoutEventDispatching(): dispatcher event dilepas, $model->save() dijalankan, lalu dispatcher dipasang kembali (src/Observers/UserstampsObserver.php:80-899). Karena dispatcher sedang dilepas, event updating tidak terpicu dan branch updating tidak menimpa nilai yang baru saja ditulis. Efek sampingnya, pada deleting ada satu UPDATE stempel yang dijalankan sebelum Laravel menulis deleted_at.
⚠️
deletingdanrestoringmenimpaupdated_by_idsecara langsung, sedangkan branchupdatingmemakaiisClean()sebagai penjaga (src/Observers/UserstampsObserver.php:30-407, src/Observers/UserstampsObserver.php:42-591). Nilaiupdated_by_idyang sengaja diubah sebelumdelete()akan hilang.
⚠️ Tanpa user terautentikasi,
getUserPrimaryValueAndClass()mengembalikan['id' => null, 'type' => null](src/Observers/UserstampsObserver.php:91-1078).delete()ataurestore()dari console, queue, atau seeder karena itu mengosongkandeleted_by_idmaupunupdated_by_idpada record tersebut.
Kolom nullable dan relasi setelah restore
Kolom deleted_by_id didaftarkan oleh macro softUserstamps() melalui addUserIdColumn(), yang selalu memanggil ->nullable() pada setiap tipe ID; saat morph aktif ditambah kolom deleted_by_type bertipe string dan nullable (src/Database/Schema/Macros/UserstampsMacro.php:72-8510). Nullable itu yang membuat restoring bisa mengosongkan kolom tanpa melanggar constraint.
Membaca kembali pelaku penghapusan memakai relasi deletedBy, yang berbentuk morphTo atau belongsTo sesuai is_using_morph, dan dibungkus withTrashed() bila userstamps.with_trashed bernilai true (src/Concerns/HasUserstamps.php:49-6211). Detail bentuk relasinya dibahas di Trait HasUserstamps dan Relasi User.
Bukti di rangkaian test
testUserstampsBehavior() menjalankan tiga aktor berurutan pada satu Product: John membuat, Jane menghapus, Alice memulihkan (tests/Feature/UserstampsBehaviorTest.php:24-3912).
| Aktor | Aksi | Ekspektasi |
|---|---|---|
| John | Product::create() | createdBy dan updatedBy terisi John, deletedBy bernilai null |
| Jane | $product->delete() | deletedBy dan updatedBy terisi Jane |
| Alice | $product->restore() | deletedBy kembali null, updatedBy terisi Alice |
Setelah restore, test mengubah userstamps.with_trashed menjadi true, menghapus user Alice, lalu memastikan updatedBy masih mengembalikan Alice (tests/Feature/UserstampsBehaviorTest.php:41-4713, config/userstamps.php:110-1206). Karena penghapusan user di situ berupa soft delete, baris user tetap ada dan relasi tetap dapat dibaca selama with_trashed aktif.