270 lines
9.7 KiB
Markdown
270 lines
9.7 KiB
Markdown
# Best Practices
|
|
|
|
## PHP
|
|
|
|
### Traits
|
|
- Gabungkan multiple `use` trait menjadi satu baris dengan koma.
|
|
- **Hindari:**
|
|
```php
|
|
use FlashesEntityMessage;
|
|
use ParsesDataTableQuery;
|
|
```
|
|
- **Gunakan:**
|
|
```php
|
|
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
|
|
- Gunakan *anonymous class* yang meng-extend `Migration`.
|
|
- Tulis *return type hint* `: void` secara eksplisit pada method `up()` dan `down()`.
|
|
- **Urutan & Pengelompokan Kolom (Ordering & Grouping):**
|
|
1. **Primary Key**: `$table->id()` ditaruh paling atas.
|
|
2. **Relasi (Foreign Keys / Morphs)**: Ditaruh tepat setelah Primary Key di bagian teratas.
|
|
3. **Kolom Data Utama**: Dikelompokkan secara logis menggunakan spasi (baris kosong) sebagai pemisah antar kelompok data.
|
|
4. **Timestamps & Soft Deletes**: Ditaruh paling bawah.
|
|
- **Gunakan:**
|
|
```php
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
return new class extends Migration
|
|
{
|
|
public function up(): void
|
|
{
|
|
Schema::create('table_name', function (Blueprint $table) {
|
|
$table->id();
|
|
|
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
|
|
|
|
$table->string('name', 100);
|
|
$table->string('description', 255)->nullable();
|
|
|
|
$table->timestamp('created_at')->useCurrent();
|
|
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
|
$table->softDeletes();
|
|
});
|
|
}
|
|
|
|
public function down(): void
|
|
{
|
|
Schema::dropIfExists('table_name');
|
|
}
|
|
};
|
|
```
|
|
|
|
### Definisi Kolom & Tipe Data
|
|
- **ID & Timestamps:**
|
|
- Gunakan `$table->id()` sebagai primary key.
|
|
- Gunakan `timestamp` eksplisit dengan default `useCurrent()` dan `useCurrentOnUpdate()`. **Hindari penggunaan `$table->timestamps()` bawaan Laravel agar format kolom presisi dan konsisten.**
|
|
- Tambahkan `$table->softDeletes()` (atau `deleted_at`) jika model terkait menggunakan trait `SoftDeletes`.
|
|
```php
|
|
$table->timestamp('created_at')->useCurrent();
|
|
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
|
$table->softDeletes();
|
|
```
|
|
- **Kolom Enum:**
|
|
- Hubungkan dengan PHP Backed Enum menggunakan `array_column(EnumName::cases(), 'value')`.
|
|
- **Contoh:**
|
|
```php
|
|
$table->enum('status', array_column(StatusEnum::cases(), 'value'))->default(StatusEnum::PENDING->value);
|
|
```
|
|
|
|
### Relasi & Foreign Key
|
|
- Gunakan `$table->foreignId()`.
|
|
- Tentukan constraint relasi secara eksplisit dengan `constrained()` (dan sebutkan nama tabel target jika nama kolom tidak sesuai dengan nama tabel, misal: `constrained('users')`).
|
|
- Tentukan aksi penghapusan secara eksplisit (`cascadeOnDelete()`, `nullOnDelete()`, atau `restrictOnDelete()`) sesuai kebutuhan integritas data.
|
|
- **Contoh:**
|
|
```php
|
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
|
```
|
|
|
|
### Modifikasi Tabel (Alter Schema)
|
|
- Saat menambahkan kolom baru, gunakan modifier `->after('column_name')` agar tata letak kolom di database terstruktur dengan logis.
|
|
- Jika menambahkan kolom baru yang non-nullable pada tabel yang sudah berisi data, jalankan data migration di dalam method `up()` setelah schema builder:
|
|
```php
|
|
DB::table('table_name')->update(['column_name' => 'default_value']);
|
|
```
|
|
- Di dalam method `down()`, pastikan untuk melepas constraint foreign key terlebih dahulu sebelum menghapus kolom bersangkutan. Gunakan `dropConstrainedForeignId` atau `dropForeign` untuk cara yang aman dan ringkas.
|
|
- **Gunakan:**
|
|
```php
|
|
public function down(): void
|
|
{
|
|
Schema::table('table_name', function (Blueprint $table) {
|
|
$table->dropConstrainedForeignId('foreign_key_id');
|
|
$table->dropColumn(['column_one', 'column_two']);
|
|
});
|
|
}
|
|
```
|
|
|
|
## JavaScript / Vue
|
|
|
|
### Struktur Folder Page-Specific Components
|
|
Komponen yang hanya digunakan oleh satu page tertentu (misalnya modal form, kolom tabel, action button) **harus ditempatkan di folder page-nya langsung** di dalam subfolder sesuai fungsinya, bukan di `resources/js/components/`.
|
|
|
|
Folder `resources/js/components/` hanya untuk komponen yang **reusable** lintas page (seperti UI primitives, DataTable, ConfirmDialog, dll).
|
|
|
|
Subfolder di dalam page folder dikelompokkan berdasarkan fungsi:
|
|
- `form/` — modal form, form fields
|
|
- `table/` — columns definition, data-table-actions, cell renderers
|
|
|
|
- **Hindari:**
|
|
```
|
|
resources/js/components/admin/master/categories/
|
|
CategoryFormModal.vue
|
|
columns.ts
|
|
data-table-actions.vue
|
|
resources/js/pages/admin/master/categories/
|
|
Index.vue
|
|
```
|
|
- **Gunakan:**
|
|
```
|
|
resources/js/pages/admin/master/categories/
|
|
Index.vue
|
|
form/
|
|
CategoryFormModal.vue
|
|
table/
|
|
columns.ts
|
|
data-table-actions.vue
|
|
```
|
|
|
|
### Import Path
|
|
Gunakan relative import (`./`) untuk file yang berada di folder/subfolder yang sama, bukan absolute path `@/...`.
|
|
|
|
- **Hindari:**
|
|
```ts
|
|
import CategoryFormModal from '@/components/admin/master/categories/CategoryFormModal.vue';
|
|
```
|
|
- **Gunakan:**
|
|
```ts
|
|
// Dari Index.vue ke subfolder
|
|
import CategoryFormModal from './form/CategoryFormModal.vue';
|
|
import { createColumns } from './table/columns';
|
|
|
|
// Dari columns.ts ke file satu folder
|
|
import DataTableActions from './data-table-actions.vue';
|
|
```
|
|
|
|
### Pola CRUD (Create/Edit)
|
|
Gunakan **modal** untuk CRUD yang sederhana (sedikit field, tidak ada nested data complex), dan **page terpisah** untuk CRUD yang kompleks.
|
|
|
|
- **Modal** — form sederhana, biasanya 1-3 field, tidak memerlukan layout khusus:
|
|
```
|
|
pages/admin/master/categories/
|
|
Index.vue
|
|
form/
|
|
CategoryFormModal.vue ← modal form
|
|
table/
|
|
columns.ts
|
|
data-table-actions.vue
|
|
```
|
|
Contoh: categories (1 field), suppliers (3 field), customers (3 field)
|
|
|
|
- **Page terpisah** — form kompleks, banyak field, nested data, file upload, atau memerlukan layout khusus:
|
|
```
|
|
pages/admin/master/raw-materials/
|
|
Index.vue
|
|
Create.vue ← halaman tambah
|
|
Edit.vue ← halaman ubah
|
|
form/
|
|
RawMaterialForm.vue ← form component
|
|
table/
|
|
RawMaterialGroupedTable.vue
|
|
data-table-actions.vue
|
|
raw-material-status-toggle.vue
|
|
```
|
|
Contoh: raw-materials (nested prices, multi-image upload, dynamic variant rows)
|