16 KiB
Best Practices
PHP
Traits
- Gabungkan multiple
usetrait menjadi satu baris dengan koma. - Hindari:
use FlashesEntityMessage; use ParsesDataTableQuery; - Gunakan:
use FlashesEntityMessage, ParsesDataTableQuery;
Model Eloquent
-
Urutan Penulisan di dalam Model Class: Setiap model harus disusun mengikuti urutan struktur berikut dari atas ke bawah:
- Use Trait (misal:
use HasFactory, SoftDeletes;). - Casting (metode
casts()). - Scope (metode local scope dengan PHP attribute
#[Scope]). - Attribute (Accessors / Appends - misal format harga, format tanggal, dll. Harus dimasukkan dalam array
$appendsdi atas class). - Method Lainnya (helper method, logic bisnis, dll.).
- Relasi (Relationships) (diurutkan secara alfabetis berdasarkan nama method relasi).
- Use Trait (misal:
-
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_activedanstatus:// 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
belongsToke model lain, pastikan model lain tersebut juga mendefinisikan relasi kebalikannya (hasManyatauhasOne).
-
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:
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.
- Semua kolom yang memiliki opsi/status tertentu (seperti
-
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
$appendsdi atas class. - Contoh:
amount→amount_formatted,status→status_label.
- Kolom yang akan diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal) wajib menggunakan accessor dengan class
-
Contoh Lengkap:
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(), atauresponse()->json().
-
Urutan Method:
indexcreatestoreshow(jika ada)editupdatedestroy- 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(), atauAuth::user().
- Selalu ambil user dari
-
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
FlashesEntityMessageuntuk 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.).
- Gunakan helper dari trait
-
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 StockRetailController.php StokOpnameController.php ← modul dengan 1 controller tetap di level parent OwnerVerificationController.php
-
Contoh:
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'); } }
Database & Migrasi
Struktur File Migrasi
- Gunakan anonymous class yang meng-extend
Migration. - Tulis return type hint
: voidsecara eksplisit pada methodup()dandown(). - Urutan & Pengelompokan Kolom (Ordering & Grouping):
- Primary Key:
$table->id()ditaruh paling atas. - Relasi (Foreign Keys / Morphs): Ditaruh tepat setelah Primary Key di bagian teratas.
- Kolom Data Utama: Dikelompokkan secara logis menggunakan spasi (baris kosong) sebagai pemisah antar kelompok data.
- Timestamps & Soft Deletes: Ditaruh paling bawah.
- Primary Key:
- Gunakan:
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
timestampeksplisit dengan defaultuseCurrent()danuseCurrentOnUpdate(). Hindari penggunaan$table->timestamps()bawaan Laravel agar format kolom presisi dan konsisten. - Tambahkan
$table->softDeletes()(ataudeleted_at) jika model terkait menggunakan traitSoftDeletes.$table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); $table->softDeletes();
- Gunakan
- Kolom Enum:
- Hubungkan dengan PHP Backed Enum menggunakan
EnumName::values(). - Contoh:
$table->enum('status', StatusEnum::values())->default(StatusEnum::PENDING->value);
- Hubungkan dengan PHP Backed Enum menggunakan
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(), ataurestrictOnDelete()) sesuai kebutuhan integritas data. - Contoh:
$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: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. GunakandropConstrainedForeignIdataudropForeignuntuk cara yang aman dan ringkas.- Gunakan:
public function down(): void { Schema::table('table_name', function (Blueprint $table) { $table->dropConstrainedForeignId('foreign_key_id'); $table->dropColumn(['column_one', 'column_two']); }); }
- Gunakan:
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:
import CategoryFormModal from '@/components/admin/master/categories/CategoryFormModal.vue'; - Gunakan:
// 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.vueContoh: 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.vueContoh: raw-materials (nested prices, multi-image upload, dynamic variant rows)