# 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. **Scope** (metode local scope dengan PHP attribute `#[Scope]`). 4. **Attribute** (Accessors / Appends - misal format harga, format tanggal, dll. Harus dimasukkan dalam array `$appends` di atas class). 5. **Method Lainnya** (helper method, logic bisnis, dll.). 6. **Relasi (Relationships)** (diurutkan secara alfabetis berdasarkan nama method relasi). - **Pengelompokan Scope:** - Scope dikelompokkan berdasarkan kolom yang di-query. - Urutkan berdasarkan nama kolom (abjad), lalu di dalam setiap Column Group, urutkan lagi berdasarkan nama method (abjad). - **Contoh:** Jika ada kolom `is_active` dan `status`: ```php // Column group: is_active (first alphabetical) #[Scope] public function active(Builder $query): void { $query->where('is_active', true); } #[Scope] public function inactive(Builder $query): void { $query->where('is_active', false); } // Column group: status (second alphabetical) #[Scope] public function completed(Builder $query): void { $query->where('status', OrderStatus::COMPLETED); } #[Scope] public function pending(Builder $query): void { $query->where('status', OrderStatus::PENDING); } ``` - **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:** - Lakukan casting secara tepat pada kolom yang memerlukan tipe data khusus. - Tipe data yang perlu di-cast: `enum`, `boolean`, `integer`, `decimal`, `date`, `datetime`, `array`, `json`, dll. - **Jangan** cast kolom relasi (foreign key). - **Contoh:** ```php protected function casts(): array { return [ 'status' => OrderStatus::class, // enum 'is_active' => 'boolean', // boolean 'amount' => 'integer', // integer 'price' => 'decimal:2', // decimal 'birth_date' => 'date', // date 'created_at' => 'datetime', // datetime 'settings' => 'array', // array/json ]; } ``` - **Scope untuk Kolom Opsi:** - Semua kolom yang memiliki opsi/status tertentu (seperti `status`, `is_active`, `type`, dll.) **wajib** memiliki scope. - Gunakan PHP attribute `#[Scope]` di atas metode scope. - **Attribute untuk Format:** - Kolom yang akan diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal) **wajib** menggunakan accessor dengan class `Attribute`. - Nama atribut harus dimasukkan dalam properti `$appends` di atas class. - **Contoh:** `amount` → `amount_formatted`, `status` → `status_label`. - **Contoh Lengkap:** ```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. Scope (grouped by column, then alphabetical) #[Scope] public function completed(Builder $query): void { $query->where('status', OrderStatus::COMPLETED); } #[Scope] public function pending(Builder $query): void { $query->where('status', OrderStatus::PENDING); } // 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. Method Lainnya public static function mediaModuleName(): string { return 'order'; } // 6. Relation public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function items(): HasMany { return $this->hasMany(OrderItem::class); } } ``` ### Controller - **Tugas Controller:** - Controller hanya menangani **request dan response**. - Semua logika bisnis (validasi aturan bisnis, query kompleks, transformasi data, authorization bisnis) **wajib** dipindahkan ke **Service**. - Controller memanggil service, lalu mengembalikan `Inertia::render()`, `redirect()`, atau `response()->json()`. - **Urutan Method:** 1. `index` 2. `create` 3. `store` 4. `show` (jika ada) 5. `edit` 6. `update` 7. `destroy` 8. Method tambahan (custom action) ditempatkan **di bawah** `destroy`. - **Form Request:** - **Selalu** gunakan Form Request untuk validasi, sekecil apapun validasinya. - Authorization di Form Request menggunakan `$this->user()`, bukan helper global. - **Authenticated User:** - **Selalu** ambil user dari `$request->user()` pada method controller. - **Jangan** gunakan `request()->user()`, `auth()->user()`, atau `Auth::user()`. - **Docblock:** - **Jangan** gunakan PHPDoc/docblock pada controller. - **Penamaan:** - Variable, method, file, dan elemen sistem lainnya **wajib** menggunakan **Bahasa Inggris**. - Teks yang ditampilkan ke user (flash message, label UI) menggunakan **Bahasa Indonesia**. - Kecuali nama modul yang memang sudah Bahasa Inggris (misal: `customer`, `cutting`, `order`). - **Flash Message:** - Gunakan helper dari trait `FlashesEntityMessage` untuk pesan standar CRUD: - `flashCreated($entity)` → "{entity} berhasil ditambahkan." - `flashUpdated($entity)` → "{entity} berhasil diperbarui." - `flashDeleted($entity)` → "{entity} berhasil dihapus." - `flashStatusUpdated($entity)` → "Status {entity} berhasil diperbarui." - Gunakan `flashSuccess($message)` **hanya** untuk pesan custom (verifikasi owner, approve/reject, transisi status, dll.). - **Struktur Folder:** - Jika satu modul memiliki **lebih dari satu controller**, kelompokkan dalam subfolder modul. - **Contoh:** ``` app/Http/Controllers/Admin/Manage/ Cutting/ CuttingController.php CuttingDraftItemController.php Order/ OrderController.php OrderDraftItemController.php Purchase/ PurchaseController.php PurchaseDraftItemController.php Stock/ StockController.php RetailStockController.php StokOpnameController.php ← modul dengan 1 controller tetap di level parent OwnerVerificationController.php ``` - **Contoh:** ```php namespace App\Http\Controllers\Admin\Manage\Order; use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Manage\OrderRequest; use App\Models\Order; use App\Services\Manage\OrderService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; class OrderController extends Controller { use FlashesEntityMessage, ParsesDataTableQuery; public function __construct( private readonly OrderService $orderService, ) {} public function index(Request $request): Response { $tableQuery = $this->parseDataTableQuery($request); return Inertia::render('admin/manage/orders/Index', [ 'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()), 'filters' => $this->dataTableFilters($tableQuery), ]); } public function store(OrderRequest $request): RedirectResponse { $this->orderService->create($request->validated(), $request->user()); $this->flashCreated('Pesanan'); return redirect()->route('admin.manage.orders.index'); } public function destroy(Order $order): RedirectResponse { $this->orderService->delete($order); $this->flashDeleted('Pesanan'); return redirect()->route('admin.manage.orders.index'); } } ``` ### Service - **Tugas Service:** - Service menampung **semua logika bisnis** (query kompleks, transformasi data, transaksi, authorization bisnis). - Controller hanya memanggil service dan mengembalikan response. - **Urutan Method:** 1. `paginateForIndex` / method index/list 2. `findForEdit` / `findForShow` (jika ada) 3. `create` / `store` 4. `update` 5. `delete` / `destroy` 6. Method tambahan (custom action) 7. Method `private` di paling bawah - **Docblock & Comment:** - **Jangan** gunakan PHPDoc/docblock pada service. - **Jangan** gunakan inline comment tipe `@var Model $variable`. - **Pagination:** - Default pagination untuk data table: **25** (`->paginate(25)`). - **Update Model:** - Gunakan `$model->update([...])`, **bukan** assign property lalu `$model->save()`. - **Hindari:** ```php $account->balance = $newBalance; $account->save(); ``` - **Gunakan:** ```php $account->update(['balance' => $newBalance]); ``` - **Logic di Model/Enum, Bukan Wrapper di Service:** - Method yang hanya meneruskan ke model/enum **wajib** ditempatkan di model/enum, bukan di service. - **Contoh:** `isEditable()`, `transitionStatusMessage()`, `ensureEditable()` → enum/model. - Service memanggil langsung: `$order->status->isEditable()`, `$status->transitionStatusMessage()`, `$order->ensureEditable()`. - **Penamaan:** - Variable, method, file **Bahasa Inggris**. - Pesan error/flash ke user **Bahasa Indonesia**. ## 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 `EnumName::values()`. - **Contoh:** ```php $table->enum('status', StatusEnum::values())->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 ### Komponen Reusable vs Page-Specific - `resources/js/components/` → komponen **reusable** lintas page (UI primitives, catalog POS, printer, display formatter, dll.). - `resources/js/pages/{module}/form/` dan `table/` → komponen **khusus page** (modal form, columns, actions). - Jika komponen dipakai **lebih dari satu modul/page**, pindahkan ke `resources/js/components/{domain}/`. - Contoh: `PosCatalogCard`, `PosCatalogVariantThumb` → `components/catalog/` - Contoh: `OrderPrintButton`, `ThermalPrinterConnectButton` → `components/order/` ### Format Data dan Attribute Model - Untuk **tampilan read-only**, gunakan field `*_formatted` dari API/model accessor. - **Jangan** format manual di view jika accessor model sudah tersedia (misal: `stock_formatted`, `price_formatted`). - Format **live/calc** di form (cart total, input rupiah) boleh pakai `formatRupiah()` / `RupiahInput`. - Komponen display: `RupiahText` (`amount` + optional `formatted` prop). ### Types - Type domain di `resources/js/types/{domain}.ts`. - Type shared di `resources/js/types/common.ts`: `Paginated`, `SelectOption`, `EnumOption`. - Hindari duplikasi type di page; import dari `@/types/*`. ### Form Request dan View Label - Label validasi harus **sama persis** dengan `` di view. - Gunakan `attributes()` pada Form Request (locale `id` → `:Attribute wajib diisi.`). - Contoh: view `Nama Produk` → `'name' => 'Nama Produk'`. - Pesan custom untuk rule nested/business logic gunakan `messages()`. ### Pemisahan File - Pecah file besar berdasarkan **fungsi**: catalog panel, cart panel, metadata form, composable logic. - Target ideal: **< 300 baris** per komponen. ### 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)