Lompat ke konten
dokumentasi.id
Commit 159d6ac
v1
Star1

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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-19.

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-17.

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-17.

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.
Fig. 1: Urutan registrasi observer dari hook boot traitTrait 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-7. 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-32, src/Concerns/HasUserstamps.php:34-47, src/Concerns/HasUserstamps.php:49-62.

MethodTipe nilai kembaliNama relasi morphToKolom belongsTo
createdBy()MorphTo|BelongsTocreatedBy + created_by_type + created_by_idcreated_by_id
updatedBy()MorphTo|BelongsToupdatedBy + updated_by_type + updated_by_idupdated_by_id
deletedBy()MorphTo|BelongsTodeletedBy + deleted_by_type + deleted_by_iddeleted_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:22, src/Concerns/HasUserstamps.php:37, src/Concerns/HasUserstamps.php:52.

Config yang dibaca ketiga method tersebut:

Config keyNilai bawaanPerannya di trait
is_using_morphfalsememilih cabang morphTo atau belongsTo — config/userstamps.php:18
users_modelUser::classargumen pertama belongsTo — config/userstamps.php:48
users_table_id_column_nameidkolom owner pada belongsTo — config/userstamps.php:60-71
created_by_column, updated_by_column, deleted_by_columncreated_by, updated_by, deleted_byprefiks nama kolom _id dan _type — config/userstamps.php:84-110
with_trashedfalsemenentukan pemanggilan withTrashed() — config/userstamps.php:120
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.
Fig. 2: Percabangan di dalam setiap method relasiSetiap 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-47. 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-160.

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.
Fig. 3: Kardinalitas userstamp dari model ke tabel usersTiga 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:33, config/userstamps.php:71. 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-13. Test mengarahkan users_model ke class tersebut sebelum skenario non-morph berjalan — tests/Feature/UserstampsBehaviorTest.php:141-143.

Jebakan yang mudah terlewat

⚠️ is_using_morph wajib ditetapkan sebelum migration dijalankan, karena nilai itu menentukan apakah kolom _type ikut dibuat — config/userstamps.php:13-18. Mengubahnya setelah tabel terbentuk membuat relasi morphTo menunjuk 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-60, src/Database/Schema/Macros/UserstampsMacro.php:79-81. Detail kolom dan tipe ID dibahas di Mode Morph dan Tipe Kolom ID, sedangkan pembentukan kolomnya di Lapisan Schema dan Macro Blueprint.

⚠️ deletedBy() membaca deleted_by_id, kolom yang hanya dibuat oleh softUserstamps() — src/Database/Schema/Macros/UserstampsMacro.php:72-85. Model tanpa macro itu akan gagal saat relasi deletedBy dipakai; 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:31, src/Concerns/HasUserstamps.php:46, src/Concerns/HasUserstamps.php:61. 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-47. 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-67. Helper ini yang dipanggil observer pada event deleting untuk memutuskan apakah cap deleted_by_id perlu diisi — src/Observers/UserstampsObserver.php:44.

Konsekuensinya, memasang HasUserstamps tanpa SoftDeletes membuat deletedBy selalu null, meskipun createdBy dan updatedBy tetap terisi — bandingkan tests/Feature/UserstampsBehaviorTest.php:18-22 dengan tests/Feature/UserstampsBehaviorTest.php:29-31. Pakai HasUserstamps bersama SoftDeletes bila cap hapus dibutuhkan; siklusnya dirinci di Siklus Soft Delete dan Restore.