From 0986ec780ba701a12fadc834ad4186460e1eb984 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 22 Jun 2026 11:26:47 +0700 Subject: [PATCH] refactor: standardize model structure, normalize bidirectional relations, and update documentation. --- BEST_PRACTICE.md | 89 ++++++++++++++++++++++++++++++++ app/Models/Attendance.php | 6 +++ app/Models/CashTransaction.php | 6 +++ app/Models/Cutting.php | 14 ++--- app/Models/CuttingMaterial.php | 10 ++-- app/Models/CuttingResult.php | 10 ++-- app/Models/Employee.php | 48 ++++++++--------- app/Models/Order.php | 10 ++-- app/Models/OrderItem.php | 10 ++-- app/Models/PayrollAdjustment.php | 10 ++-- app/Models/PayrollPeriod.php | 3 +- app/Models/ProductVariant.php | 10 ++-- app/Models/RawMaterialPrice.php | 5 ++ app/Models/User.php | 78 +++++++++++++++++++++++----- 14 files changed, 233 insertions(+), 76 deletions(-) diff --git a/BEST_PRACTICE.md b/BEST_PRACTICE.md index b47215b..a2d23c3 100644 --- a/BEST_PRACTICE.md +++ b/BEST_PRACTICE.md @@ -14,6 +14,95 @@ ### Traits use FlashesEntityMessage, ParsesDataTableQuery; ``` +### Model Eloquent + +- **Urutan Penulisan di dalam Model Class:** + Setiap model harus disusun mengikuti urutan struktur berikut dari atas ke bawah: + 1. **Use Trait** (misal: `use HasFactory, SoftDeletes;`). + 2. **Casting** (metode `casts()`). + 3. **Relasi (Relationships)** (diurutkan secara alfabetis berdasarkan nama method relasi). + 4. **Attribute (Accessors / Appends)** (misal format harga, format tanggal, dll. Harus dimasukkan dalam array `$appends` di atas class). + 5. **Scope** (metode local scope dengan PHP attribute `#[Scope]`). + 6. **Method Lainnya** (helper method, logic bisnis, dll.). + +- **Hubungan Timbal Balik (2-Way Relations):** + - Pastikan setiap relasi ditulis secara 2 arah (bi-directional). + - Jika suatu model memiliki `belongsTo` ke model lain, pastikan model lain tersebut juga mendefinisikan relasi kebalikannya (`hasMany` atau `hasOne`). + +- **Casting & Formatting Attribute:** + - Lakukan casting secara tepat pada kolom yang memerlukan tipe data khusus (seperti Boolean, Integer, Datetime, atau Enum). + - Jika suatu kolom perlu diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal), buatlah accessor menggunakan class `Attribute` (Eloquent Attribute) dan tambahkan nama atribut tersebut ke properti `$appends` model. + +- **Query Scopes:** + - Jika model memiliki opsi/status tertentu, buatlah local scope agar kueri dapat digunakan kembali (reusable). Gunakan PHP attribute `#[Scope]` di atas metode scope. + +- **Contoh:** + ```php + namespace App\Models; + + use App\Enums\OrderStatus; + use Illuminate\Database\Eloquent\Attributes\Appends; + use Illuminate\Database\Eloquent\Attributes\Guarded; + use Illuminate\Database\Eloquent\Attributes\Scope; + use Illuminate\Database\Eloquent\Builder; + use Illuminate\Database\Eloquent\Casts\Attribute; + use Illuminate\Database\Eloquent\Factories\HasFactory; + use Illuminate\Database\Eloquent\Model; + use Illuminate\Database\Eloquent\Relations\BelongsTo; + use Illuminate\Database\Eloquent\Relations\HasMany; + use Illuminate\Database\Eloquent\SoftDeletes; + + #[Guarded(['id'])] + #[Appends(['amount_formatted', 'status_label'])] + class Order extends Model + { + // 1. Use Trait + use HasFactory, SoftDeletes; + + // 2. Casting + protected function casts(): array + { + return [ + 'status' => OrderStatus::class, + 'amount' => 'integer', + ]; + } + + // 3. Relasi (Urut Abjad) + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function items(): HasMany + { + return $this->hasMany(OrderItem::class); + } + + // 4. Attribute + public function amountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'), + ); + } + + public function statusLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->status->label(), + ); + } + + // 5. Scope + #[Scope] + public function pending(Builder $query): void + { + $query->where('status', OrderStatus::PENDING->value); + } + } + ``` + ## Database & Migrasi ### Struktur File Migrasi diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index 990147a..415f7ec 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Spatie\MediaLibrary\HasMedia; #[Guarded(['id'])] @@ -47,6 +48,11 @@ public function employee(): BelongsTo return $this->belongsTo(Employee::class); } + public function payrollAdjustments(): HasMany + { + return $this->hasMany(PayrollAdjustment::class); + } + public function attendanceDateFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 9264008..10ec5e5 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\MediaLibrary\HasMedia; @@ -48,6 +49,11 @@ public function createdBy(): BelongsTo return $this->belongsTo(User::class, 'created_by_id'); } + public function order(): HasOne + { + return $this->hasOne(Order::class); + } + public function reference(): MorphTo { return $this->morphTo(); diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php index 235e60c..639a72e 100644 --- a/app/Models/Cutting.php +++ b/app/Models/Cutting.php @@ -39,24 +39,24 @@ public function createdBy(): BelongsTo return $this->belongsTo(User::class, 'created_by_id'); } - public function submittedBy(): BelongsTo - { - return $this->belongsTo(User::class, 'submitted_by_id'); - } - public function materials(): HasMany { return $this->hasMany(CuttingMaterial::class); } + public function resultPrices(): HasMany + { + return $this->hasMany(CuttingResultPrice::class); + } + public function results(): HasMany { return $this->hasMany(CuttingResult::class); } - public function resultPrices(): HasMany + public function submittedBy(): BelongsTo { - return $this->hasMany(CuttingResultPrice::class); + return $this->belongsTo(User::class, 'submitted_by_id'); } public function createdAtFormatted(): Attribute diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index 9531357..1e08a42 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -31,11 +31,6 @@ protected function casts(): array ]; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - public function cutting(): BelongsTo { return $this->belongsTo(Cutting::class); @@ -46,6 +41,11 @@ public function rawMaterialPrice(): BelongsTo return $this->belongsTo(RawMaterialPrice::class); } + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + public function materialUsageFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/CuttingResult.php b/app/Models/CuttingResult.php index d8eadcb..d5bfc18 100644 --- a/app/Models/CuttingResult.php +++ b/app/Models/CuttingResult.php @@ -23,11 +23,6 @@ protected function casts(): array ]; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - public function cutting(): BelongsTo { return $this->belongsTo(Cutting::class); @@ -37,4 +32,9 @@ public function productVariant(): BelongsTo { return $this->belongsTo(ProductVariant::class); } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Models/Employee.php b/app/Models/Employee.php index 497be73..d741008 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -32,30 +32,6 @@ protected function casts(): array ]; } - #[Scope] - public function contract(Builder $query): void - { - $query->where('employment_status', EmploymentStatus::CONTRACT->value); - } - - #[Scope] - public function fullTime(Builder $query): void - { - $query->where('employment_status', EmploymentStatus::FULL_TIME->value); - } - - #[Scope] - public function partTime(Builder $query): void - { - $query->where('employment_status', EmploymentStatus::PART_TIME->value); - } - - #[Scope] - public function temporary(Builder $query): void - { - $query->where('employment_status', EmploymentStatus::TEMPORARY->value); - } - public function advances(): HasMany { return $this->hasMany(EmployeeAdvance::class); @@ -115,4 +91,28 @@ public function resignDateFormatted(): Attribute get: fn () => $this->resign_date ? Carbon::parse($this->resign_date)->translatedFormat('l, d F Y') : null, ); } + + #[Scope] + public function contract(Builder $query): void + { + $query->where('employment_status', EmploymentStatus::CONTRACT->value); + } + + #[Scope] + public function fullTime(Builder $query): void + { + $query->where('employment_status', EmploymentStatus::FULL_TIME->value); + } + + #[Scope] + public function partTime(Builder $query): void + { + $query->where('employment_status', EmploymentStatus::PART_TIME->value); + } + + #[Scope] + public function temporary(Builder $query): void + { + $query->where('employment_status', EmploymentStatus::TEMPORARY->value); + } } diff --git a/app/Models/Order.php b/app/Models/Order.php index f42c5a5..8a42534 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -58,11 +58,6 @@ public function createdBy(): BelongsTo return $this->belongsTo(User::class, 'created_by_id'); } - public function marketing(): BelongsTo - { - return $this->belongsTo(User::class, 'marketing_id'); - } - public function customer(): BelongsTo { return $this->belongsTo(Customer::class); @@ -73,6 +68,11 @@ public function items(): HasMany return $this->hasMany(OrderItem::class); } + public function marketing(): BelongsTo + { + return $this->belongsTo(User::class, 'marketing_id'); + } + public function channelLabel(): Attribute { return Attribute::make( diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index 16e8620..fd033fb 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -33,11 +33,6 @@ protected function casts(): array ]; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - public function order(): BelongsTo { return $this->belongsTo(Order::class); @@ -48,6 +43,11 @@ public function productVariant(): BelongsTo return $this->belongsTo(ProductVariant::class); } + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + public function quantityFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/PayrollAdjustment.php b/app/Models/PayrollAdjustment.php index 27c8373..c7c7856 100644 --- a/app/Models/PayrollAdjustment.php +++ b/app/Models/PayrollAdjustment.php @@ -28,6 +28,11 @@ protected function casts(): array ]; } + public function attendance(): BelongsTo + { + return $this->belongsTo(Attendance::class); + } + public function createdBy(): BelongsTo { return $this->belongsTo(User::class, 'created_by_id'); @@ -38,11 +43,6 @@ public function payroll(): BelongsTo return $this->belongsTo(Payroll::class); } - public function attendance(): BelongsTo - { - return $this->belongsTo(Attendance::class); - } - public function amountFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/PayrollPeriod.php b/app/Models/PayrollPeriod.php index 7ee0eb1..f1b9bda 100644 --- a/app/Models/PayrollPeriod.php +++ b/app/Models/PayrollPeriod.php @@ -12,12 +12,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] #[Appends(['period_label', 'status_label', 'closed_at_formatted'])] class PayrollPeriod extends Model { - use HasFactory, InteractsWithActivityLog; + use HasFactory, InteractsWithActivityLog, SoftDeletes; protected function casts(): array { diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php index 7d34270..a5fda91 100644 --- a/app/Models/ProductVariant.php +++ b/app/Models/ProductVariant.php @@ -27,6 +27,11 @@ protected function casts(): array ]; } + public function cuttingResultPrices(): HasMany + { + return $this->hasMany(CuttingResultPrice::class); + } + public function cuttingResults(): HasMany { return $this->hasMany(CuttingResult::class); @@ -37,11 +42,6 @@ public function orderItems(): HasMany return $this->hasMany(OrderItem::class); } - public function cuttingResultPrices(): HasMany - { - return $this->hasMany(CuttingResultPrice::class); - } - public function prices(): HasMany { return $this->hasMany(ProductPrice::class, 'variant_id'); diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index d669cac..1d8ef94 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -33,6 +33,11 @@ public function cuttingMaterials(): HasMany return $this->hasMany(CuttingMaterial::class); } + public function purchaseItems(): HasMany + { + return $this->hasMany(PurchaseItem::class); + } + public function rawMaterial(): BelongsTo { return $this->belongsTo(RawMaterial::class); diff --git a/app/Models/User.php b/app/Models/User.php index f541571..d624ad0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -35,23 +35,36 @@ protected function casts(): array ]; } - #[Scope] - public function active(Builder $query): void - { - $query->where('is_active', true); - } - - #[Scope] - public function inactive(Builder $query): void - { - $query->where('is_active', false); - } - public function cashTransactions(): HasMany { return $this->hasMany(CashTransaction::class, 'created_by_id'); } + public function closedPayrollPeriods(): HasMany + { + return $this->hasMany(PayrollPeriod::class, 'closed_by_id'); + } + + public function createdCuttings(): HasMany + { + return $this->hasMany(Cutting::class, 'created_by_id'); + } + + public function createdPayrollAdjustments(): HasMany + { + return $this->hasMany(PayrollAdjustment::class, 'created_by_id'); + } + + public function cuttingMaterials(): HasMany + { + return $this->hasMany(CuttingMaterial::class); + } + + public function cuttingResults(): HasMany + { + return $this->hasMany(CuttingResult::class); + } + public function employee(): HasOne { return $this->hasOne(Employee::class); @@ -62,24 +75,49 @@ public function expenses(): HasMany return $this->hasMany(Expense::class, 'created_by_id'); } + public function marketingOrders(): HasMany + { + return $this->hasMany(Order::class, 'marketing_id'); + } + + public function orderItems(): HasMany + { + return $this->hasMany(OrderItem::class); + } + public function orders(): HasMany { return $this->hasMany(Order::class, 'created_by_id'); } + public function paidPayrolls(): HasMany + { + return $this->hasMany(Payroll::class, 'paid_by_id'); + } + public function profile(): HasOne { return $this->hasOne(UserProfile::class); } + public function purchases(): HasMany + { + return $this->hasMany(Purchase::class, 'created_by_id'); + } + public function pushSubscriptions(): HasMany { return $this->hasMany(PushSubscription::class); } - public function purchases(): HasMany + public function submittedCuttings(): HasMany { - return $this->hasMany(Purchase::class, 'created_by_id'); + return $this->hasMany(Cutting::class, 'submitted_by_id'); + } + + public function verifiedLeaveRequests(): HasMany + { + return $this->hasMany(LeaveRequest::class, 'verified_by_id'); } public function roleLabel(): Attribute @@ -98,4 +136,16 @@ public function roleLabel(): Attribute }, ); } + + #[Scope] + public function active(Builder $query): void + { + $query->where('is_active', true); + } + + #[Scope] + public function inactive(Builder $query): void + { + $query->where('is_active', false); + } }