527 lines
19 KiB
Markdown
527 lines
19 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. **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<T>`, `SelectOption`, `EnumOption`.
|
|
- Hindari duplikasi type di page; import dari `@/types/*`.
|
|
|
|
### Form Request dan View Label
|
|
|
|
- Label validasi harus **sama persis** dengan `<FieldLabel>` 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)
|