2.2
Trait HasUserstamps dan Relasi User
Trait HasUserstamps adalah satu-satunya titik masuk paket di sisi model. Trait ini melakukan dua hal: mendaftarkan UserstampsObserver pada event booted, dan menyediakan tiga relasi baca ke baris user yang tercatat di kolom userstamp. Pada model uji Product, trait itu dipasang bersama SoftDeletes, sehingga createdBy, updatedBy, dan deletedBy semuanya punya sumber data — tests/Models/Product.php:9-191.
use TwintyLabs\Userstamps\Concerns\HasUserstamps;
class Product extends Model
{
use HasUserstamps;
use SoftDeletes;
}
Isi trait itu sendiri hanya 57 baris: satu hook boot, tiga method relasi, dan satu helper statis — src/Concerns/HasUserstamps.php:10-172.
Registrasi observer lewat event booted
Eloquent memanggil bootHasUserstamps() sekali saat model di-boot. Method itu tidak langsung mengobservasi model, melainkan mendaftarkan closure pada event booted, dan di dalam closure itulah static::observe(UserstampsObserver::class) baru dijalankan — src/Concerns/HasUserstamps.php:12-173.
sequenceDiagram
participant Model as Model Product
participant Trait as HasUserstamps
participant Obs as UserstampsObserver
Model->>Trait: bootHasUserstamps
Trait->>Model: registerModelEvent booted
Model-->>Trait: event booted dipicu
Trait->>Obs: static::observe
Obs-->>Model: creating / updating / deleting / restoring
%% fig: Urutan registrasi observer dari hook boot trait
%% desc: Trait menunda pemanggilan observe sampai event booted, sehingga observer terpasang setelah model selesai di-boot.Penundaan ini menghindari boot bersarang: observe() ikut memicu boot model, sehingga memanggilnya langsung di dalam bootHasUserstamps() berisiko rekursi. Berkas test menutup kasus tersebut dengan memastikan new Product tidak melempar LogicException — tests/Feature/TraitHasUserstampsTest.php:5-74. Perilaku observer setelah terpasang dijelaskan di Observer dan Siklus Event Model.
Tiga relasi userstamp
createdBy(), updatedBy(), dan deletedBy() bertipe kembalian MorphTo|BelongsTo. Tipe konkretnya ditentukan saat pemanggilan, bukan saat kompilasi, karena bergantung pada nilai config userstamps.is_using_morph — src/Concerns/HasUserstamps.php:19-325, src/Concerns/HasUserstamps.php:34-476, src/Concerns/HasUserstamps.php:49-627.
| Method | Tipe nilai kembali | Nama relasi morphTo | Kolom belongsTo |
|---|---|---|---|
createdBy() | MorphTo|BelongsTo | createdBy + created_by_type + created_by_id | created_by_id |
updatedBy() | MorphTo|BelongsTo | updatedBy + updated_by_type + updated_by_id | updated_by_id |
deletedBy() | MorphTo|BelongsTo | deletedBy + deleted_by_type + deleted_by_id | deleted_by_id |
Nama relasi pada cabang morphTo diambil dari __FUNCTION__, sedangkan kolom _type dan _id diberikan eksplisit sebagai argumen kedua dan ketiga, jadi Eloquent tidak perlu menebak sufiks kolom — src/Concerns/HasUserstamps.php:228, src/Concerns/HasUserstamps.php:379, src/Concerns/HasUserstamps.php:5210.
Config yang dibaca ketiga method tersebut:
| Config key | Nilai bawaan | Perannya di trait |
|---|---|---|
is_using_morph | false | memilih cabang morphTo atau belongsTo — config/userstamps.php:1811 |
users_model | User::class | argumen pertama belongsTo — config/userstamps.php:4812 |
users_table_id_column_name | id | kolom owner pada belongsTo — config/userstamps.php:60-7113 |
created_by_column, updated_by_column, deleted_by_column | created_by, updated_by, deleted_by | prefiks nama kolom _id dan _type — config/userstamps.php:84-11014 |
with_trashed | false | menentukan pemanggilan withTrashed() — config/userstamps.php:12015 |
graph TD
A["createdBy / updatedBy / deletedBy"] --> B{"is_using_morph"}
B -->|true| C["morphTo: nama method + kolom _type + kolom _id"]
B -->|false| D["belongsTo: users_model + kolom _id + users_table_id_column_name"]
C --> E{"with_trashed"}
D --> E
E -->|true| F["relation->withTrashed()"]
E -->|false| G["relation"]
style A fill:#f59e0b,color:#000
style B fill:#f59e0b,color:#000
style E fill:#f59e0b,color:#000
style C fill:#8b5cf6,color:#fff
style D fill:#336791,color:#fff
style F fill:#2d7d46,color:#fff
style G fill:#2d7d46,color:#fff
%% fig: Percabangan di dalam setiap method relasi
%% desc: Setiap method memilih morphTo atau belongsTo berdasarkan config, lalu membungkus hasilnya dengan withTrashed bila diminta.Kardinalitas relasi ke tabel users
Kolom userstamp dibuat nullable dan, pada mode non-morph, memakai foreign key nullOnDelete ke tabel users — src/Database/Schema/Macros/UserstampsMacro.php:42-4716. Karena itu relasinya bersifat opsional: satu baris model menunjuk ke nol atau satu baris users, dan satu baris users bisa dirujuk banyak baris model. Model uji membangun tabelnya lewat $table->userstamps() dan $table->softUserstamps() — tests/Feature/UserstampsBehaviorTest.php:153-16017.
erDiagram
PRODUCT }o--o| USERS : "created_by_id"
PRODUCT }o--o| USERS : "updated_by_id"
PRODUCT }o--o| USERS : "deleted_by_id"
PRODUCT {
bigint id PK
varchar name
bigint created_by_id FK
bigint updated_by_id FK
bigint deleted_by_id FK
timestamp deleted_at
}
USERS {
bigint id PK
varchar name
varchar email
timestamp deleted_at
}
%% fig: Kardinalitas userstamp dari model ke tabel users
%% desc: Tiga foreign key nullable pada products menunjuk ke satu baris users; kolomnya nullOnDelete sehingga relasi selalu opsional.Nama tabel users ditentukan userstamps.users_table dan kolom primary key-nya users_table_id_column_name — config/userstamps.php:3318, config/userstamps.php:7119. Pada sisi model user, kolom itu harus benar-benar ada. UserWithId adalah model uji untuk skenario ini: Authenticatable dengan SoftDeletes dan $fillable = ['name', 'email'] — tests/Models/UserWithId.php:8-1320. Test mengarahkan users_model ke class tersebut sebelum skenario non-morph berjalan — tests/Feature/UserstampsBehaviorTest.php:141-14321.
Jebakan yang mudah terlewat
⚠️
is_using_morphwajib ditetapkan sebelum migration dijalankan, karena nilai itu menentukan apakah kolom_typeikut dibuat — config/userstamps.php:13-1822. Mengubahnya setelah tabel terbentuk membuat relasimorphTomenunjuk kolom yang tidak ada.
Pada mode morph, trait memakai created_by_type, updated_by_type, dan deleted_by_type berisi string nullable yang ditambahkan macro — src/Database/Schema/Macros/UserstampsMacro.php:58-6023, src/Database/Schema/Macros/UserstampsMacro.php:79-8124. Detail kolom dan tipe ID dibahas di Mode Morph dan Tipe Kolom ID, sedangkan pembentukan kolomnya di Lapisan Schema dan Macro Blueprint.
⚠️
deletedBy()membacadeleted_by_id, kolom yang hanya dibuat olehsoftUserstamps()— src/Database/Schema/Macros/UserstampsMacro.php:72-8525. Model tanpa macro itu akan gagal saat relasideletedBydipakai; daftar kolom lengkap ada di Migrasi dan Kolom Userstamps.
with_trashed pada query relasi
Setiap method menutup dengan pemeriksaan yang sama: bila userstamps.with_trashed bernilai true, relasi yang sudah terbentuk dibungkus withTrashed(), selain itu dikembalikan apa adanya — src/Concerns/HasUserstamps.php:3126, src/Concerns/HasUserstamps.php:4627, src/Concerns/HasUserstamps.php:6128. Flag ini hanya berpengaruh kalau model user memakai SoftDeletes.
Efeknya terlihat di test: setelah Config::set('userstamps.with_trashed', true), user yang sudah dihapus tetap terbaca lewat $product->updatedBy — tests/Feature/UserstampsBehaviorTest.php:42-4729. Nilai kolom userstamp sendiri tidak diubah oleh flag ini; yang berubah hanya cakupan query relasi.
usingSoftDeletes sebagai pemeriksa trait
Trait juga menyediakan helper statis usingSoftDeletes() yang mengembalikan true bila SoftDeletes::class ada di class_uses_recursive(static::class) — src/Concerns/HasUserstamps.php:64-6730. Helper ini yang dipanggil observer pada event deleting untuk memutuskan apakah cap deleted_by_id perlu diisi — src/Observers/UserstampsObserver.php:4431.
Konsekuensinya, memasang HasUserstamps tanpa SoftDeletes membuat deletedBy selalu null, meskipun createdBy dan updatedBy tetap terisi — bandingkan tests/Feature/UserstampsBehaviorTest.php:18-2232 dengan tests/Feature/UserstampsBehaviorTest.php:29-3133. Pakai HasUserstamps bersama SoftDeletes bila cap hapus dibutuhkan; siklusnya dirinci di Siklus Soft Delete dan Restore.