store/BEST_PRACTICE.md

19 KiB

Best Practices

PHP

Traits

  • Gabungkan multiple use trait 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:

    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:
      // 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:
      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: amountamount_formatted, statusstatus_label.
  • 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(), 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:

    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:
      $account->balance = $newBalance;
      $account->save();
      
    • Gunakan:
      $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:
    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.
      $table->timestamp('created_at')->useCurrent();
      $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
      $table->softDeletes();
      
  • Kolom Enum:
    • Hubungkan dengan PHP Backed Enum menggunakan EnumName::values().
    • Contoh:
      $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:
    $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. Gunakan dropConstrainedForeignId atau dropForeign untuk 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']);
          });
      }
      

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, PosCatalogVariantThumbcomponents/catalog/
    • Contoh: OrderPrintButton, ThermalPrinterConnectButtoncomponents/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:
    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.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)