Memuat...
👋 Selamat Pagi!

Kesalahan Umum Struktur Folder Laravel yang Bikin Bingung Tim

Struktur folder Laravel default sering bikin tim development bingung. Pelajari best practice organizing Laravel project dengan Domain-Driven Design untuk kolabo...

Kesalahan Umum Struktur Folder Laravel yang Bikin Bingung Tim

Pernah buka project Laravel warisan dari developer sebelumnya, terus langsung pusing mencari file yang dibutuhkan?

Atau tim kamu sering tanya "Controller ini taruh dimana?" setiap kali bikin fitur baru?

Ini bukan masalah sepele.

Struktur folder yang berantakan adalah pembunuh produktivitas tim yang lambat tapi pasti.

Dalam artikel ini, kita akan bedah kesalahan umum dalam mengorganisir project Laravel dan bagaimana menerapkan best practice yang terbukti bikin kolaborasi tim jauh lebih smooth.

Masalah Struktur Folder Default Laravel untuk Project Besar

Laravel memberikan struktur folder yang elegant untuk project skala kecil.

Tapi begitu aplikasi tumbuh dengan puluhan fitur dan ratusan file, struktur default mulai menunjukkan kelemahan.

Controllers Jadi Tempat Sampah

Di banyak project Laravel, folder app/Http/Controllers berisi puluhan bahkan ratusan controller file.

Tanpa kategorisasi yang jelas, developer harus scroll panjang hanya untuk menemukan file yang tepat.

Belum lagi kalau nama controller tidak konsisten seperti UserController, UsersController, atau UserManagementController.

Models Campur Aduk

Folder app/Models sering jadi tempat semua model dilempar tanpa struktur.

Model untuk fitur invoice, user management, product catalog, dan shipping semua jadi satu.

Padahal setiap domain bisnis seharusnya punya boundary yang jelas.

Services dan Repositories Tidak Ada Rumah

Ketika project mulai kompleks, developer biasanya membuat service layer atau repository pattern.

Tapi kemana menaruh file-file ini?

Ada yang taruh di app/Services, ada yang di app/Repositories, ada juga yang langsung di root app/.

Tanpa standar yang jelas, setiap developer punya interpretasi sendiri.

Routes Membengkak Tanpa Struktur

File routes/web.php dan routes/api.php bisa mencapai ratusan bahkan ribuan baris.

Maintenance jadi nightmare karena harus scroll bolak-balik untuk menemukan route yang tepat.

Grouping route juga sering inconsistent, kadang by prefix, kadang by controller, kadang campur-campur.

Prinsip Domain-Driven Design untuk Organize Laravel Code

Domain-Driven Design (DDD) bukan tentang membuat struktur folder yang rumit.

Ini tentang mengorganisir code berdasarkan domain bisnis, bukan technical layer.

Pisahkan Berdasarkan Business Context

Alih-alih memisahkan by technical layer (controllers, models, services), pisahkan by business domain.

Misalnya untuk aplikasi e-commerce:

app/
├── Domains/
│   ├── Product/
│   │   ├── Models/
│   │   │   ├── Product.php
│   │   │   └── Category.php
│   │   ├── Controllers/
│   │   │   └── ProductController.php
│   │   ├── Services/
│   │   │   └── ProductService.php
│   │   └── Repositories/
│   │       └── ProductRepository.php
│   ├── Order/
│   │   ├── Models/
│   │   │   ├── Order.php
│   │   │   └── OrderItem.php
│   │   ├── Controllers/
│   │   │   └── OrderController.php
│   │   └── Services/
│   │       └── OrderProcessingService.php
│   └── User/
│       ├── Models/
│       │   └── User.php
│       ├── Controllers/
│       │   └── UserController.php
│       └── Services/
│           └── AuthenticationService.php

Dengan struktur ini, semua code yang berhubungan dengan Product ada dalam satu folder.

Developer baru tidak perlu loncat-loncat antar folder untuk memahami satu fitur.

Bounded Context yang Jelas

Setiap domain harus punya boundary yang jelas.

Product domain tidak boleh langsung akses database Order, harus lewat service atau event.

Ini mencegah coupling yang terlalu tight dan memudahkan maintenance jangka panjang.

Shared Kernel untuk Code Bersama

Untuk code yang dipakai banyak domain, buat folder app/Shared/:

app/
├── Shared/
│   ├── Traits/
│   │   └── HasUuid.php
│   ├── Helpers/
│   │   └── MoneyHelper.php
│   └── ValueObjects/
│       └── Money.php

Ini memisahkan dengan jelas mana yang specific untuk satu domain, mana yang shared.

Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika Anda. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.

Cara Struktur Service Layer dan Repository Pattern

Service layer dan repository pattern adalah best practice yang sering disalahgunakan.

Banyak developer yang implement tanpa memahami kapan dan mengapa pola ini digunakan.

Service Layer yang Proper

Service layer bukan tempat menaruh semua business logic secara random.

Ini layer untuk orchestration, koordinasi antar component.

Contoh service yang proper:

namespace App\Domains\Order\Services;

use App\Domains\Order\Models\Order;
use App\Domains\Order\Repositories\OrderRepository;
use App\Domains\Product\Services\ProductService;
use App\Domains\Payment\Services\PaymentGateway;
use Illuminate\Support\Facades\DB;

class OrderProcessingService
{
    public function __construct(
        private OrderRepository $orderRepository,
        private ProductService $productService,
        private PaymentGateway $paymentGateway
    ) {}

    public function processOrder(array $orderData): Order
    {
        return DB::transaction(function () use ($orderData) {
            // Validate product availability
            $this->productService->validateAvailability($orderData['items']);

            // Create order
            $order = $this->orderRepository->create($orderData);

            // Process payment
            $this->paymentGateway->charge($order);

            // Update product stock
            $this->productService->reduceStock($orderData['items']);

            return $order;
        });
    }
}

Service ini mengkoordinasi tiga domain: Order, Product, dan Payment.

Tidak ada logic bisnis kompleks di service, hanya orchestration.

Repository Pattern yang Benar

Repository bukan wrapper untuk Eloquent query.

Ini abstraction layer untuk data access logic yang kompleks.

namespace App\Domains\Order\Repositories;

use App\Domains\Order\Models\Order;
use Illuminate\Database\Eloquent\Collection;

class OrderRepository
{
    public function findPendingOrdersOlderThan(int $hours): Collection
    {
        return Order::where('status', 'pending')
            ->where('created_at', '<', now()->subHours($hours))
            ->with(['items', 'user'])
            ->get();
    }

    public function getTotalRevenueByMonth(int $year): array
    {
        return Order::whereYear('created_at', $year)
            ->where('status', 'completed')
            ->selectRaw('MONTH(created_at) as month, SUM(total) as revenue')
            ->groupBy('month')
            ->get()
            ->pluck('revenue', 'month')
            ->toArray();
    }
}

Repository mengenkapsulasi query kompleks yang specific untuk business requirement.

Kalau query sederhana seperti Order::find($id), langsung pakai Eloquent, tidak perlu repository.

Kapan Tidak Perlu Service atau Repository

Jangan over-engineer.

Untuk CRUD sederhana tanpa business logic kompleks, langsung pakai Controller + Model sudah cukup.

Service dan Repository baru dibutuhkan ketika:

  • Ada orchestration lintas multiple domain
  • Query kompleks yang di-reuse di banyak tempat
  • Business logic yang perlu di-test secara isolated
  • Perlu switch data source (misal dari database ke API external)

Best Practice untuk API dan Web Routes Organization

Routes yang berantakan adalah red flag pertama code quality yang buruk.

File routes yang ratusan baris impossible untuk di-maintain.

Pisahkan Routes by Domain

Alih-alih satu file routes/api.php yang besar, pisahkan by domain:

routes/
├── api/
│   ├── product.php
│   ├── order.php
│   ├── user.php
│   └── payment.php
├── web/
│   ├── admin.php
│   ├── customer.php
│   └── guest.php
└── api.php

Di routes/api.php, register semua route files:

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::middleware('auth:api')->group(function () {
        require __DIR__ . '/api/product.php';
        require __DIR__ . '/api/order.php';
        require __DIR__ . '/api/user.php';
    });

    Route::middleware('guest')->group(function () {
        require __DIR__ . '/api/auth.php';
    });
});

Setiap domain file berisi routes specific untuk domain itu:

// routes/api/order.php
use App\Domains\Order\Controllers\OrderController;

Route::prefix('orders')->name('orders.')->group(function () {
    Route::get('/', [OrderController::class, 'index'])->name('index');
    Route::post('/', [OrderController::class, 'store'])->name('store');
    Route::get('/{order}', [OrderController::class, 'show'])->name('show');
    Route::put('/{order}', [OrderController::class, 'update'])->name('update');
    Route::delete('/{order}', [OrderController::class, 'destroy'])->name('destroy');
    
    Route::post('/{order}/cancel', [OrderController::class, 'cancel'])->name('cancel');
    Route::post('/{order}/refund', [OrderController::class, 'refund'])->name('refund');
});

Route Model Binding yang Proper

Gunakan route model binding untuk clean controller code:

// app/Domains/Order/Models/Order.php
public function resolveRouteBinding($value, $field = null)
{
    return $this->where('uuid', $value)
        ->orWhere('id', $value)
        ->firstOrFail();
}

Dengan ini, controller bisa langsung terima model instance:

public function show(Order $order)
{
    return response()->json($order);
}

Tidak perlu manual Order::findOrFail($id) di setiap method.

API Versioning yang Sustainable

Untuk API yang akan berkembang, implement versioning sejak awal:

routes/
├── api/
│   ├── v1/
│   │   ├── product.php
│   │   └── order.php
│   └── v2/
│       ├── product.php
│       └── order.php

Controller juga ikut versioning:

app/
├── Domains/
│   └── Product/
│       └── Controllers/
│           ├── V1/
│           │   └── ProductController.php
│           └── V2/
│               └── ProductController.php

Ini memudahkan maintain multiple API version tanpa breaking changes untuk client lama.

Refactoring Struktur Project Laravel yang Sudah Jalan

Refactoring project besar yang sudah production adalah proses yang tricky.

Tidak bisa asal ubah struktur karena bisa break aplikasi.

Mulai dari Domain Terkecil

Pilih satu domain yang paling isolated untuk refactor pertama.

Misalnya Product domain yang tidak terlalu banyak dependency ke domain lain.

Buat struktur baru di app/Domains/Product/ tanpa hapus yang lama dulu.

Pindahkan File Secara Bertahap

Jangan pindahkan semua file sekaligus.

Pindahkan satu per satu, test, commit, baru lanjut yang berikutnya.

# Step 1: Buat struktur domain baru
mkdir -p app/Domains/Product/{Models,Controllers,Services,Repositories}

# Step 2: Copy file (jangan move dulu)
cp app/Models/Product.php app/Domains/Product/Models/
cp app/Http/Controllers/ProductController.php app/Domains/Product/Controllers/

# Step 3: Update namespace di file yang baru
# Step 4: Update references di file lain
# Step 5: Test thoroughly
# Step 6: Commit
# Step 7: Hapus file lama setelah yakin tidak ada yang break

Update Namespace dan Autoload

Setelah pindah file, update composer.json untuk autoload namespace baru:

{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "App\\Domains\\": "app/Domains/"
        }
    }
}

Jangan lupa run composer dump-autoload setiap kali update autoload config.

Buat Alias untuk Backward Compatibility

Untuk mencegah break existing code, buat class alias:

// app/Models/Product.php (file lama)
namespace App\Models;

class_alias(
    \App\Domains\Product\Models\Product::class,
    __NAMESPACE__ . '\Product'
);

Dengan ini, code lama yang masih import App\Models\Product tetap jalan.

Write Tests Sebelum Refactor

Ini non-negotiable.

Kalau project belum ada test, minimal buat integration test untuk flow critical:

public function test_user_can_create_order()
{
    $user = User::factory()->create();
    $product = Product::factory()->create(['stock' => 10]);

    $response = $this->actingAs($user)
        ->postJson('/api/orders', [
            'items' => [
                ['product_id' => $product->id, 'quantity' => 2]
            ]
        ]);

    $response->assertStatus(201);
    $this->assertDatabaseHas('orders', [
        'user_id' => $user->id
    ]);
}

Test ini memastikan refactoring tidak break flow utama aplikasi.

Dokumentasikan Convention Baru

Buat dokumentasi internal tentang struktur folder baru dan convention yang harus diikuti.

Contoh di docs/CODE_STRUCTURE.md:

# Code Structure Convention

## Domain Organization

All business logic organized by domain in `app/Domains/`:

- Each domain has its own Models, Controllers, Services, Repositories
- Cross-domain communication through Services or Events
- Shared code goes to `app/Shared/`

## Naming Convention

- Controllers: `{Domain}Controller` (e.g., ProductController)
- Services: `{Purpose}Service` (e.g., OrderProcessingService)
- Repositories: `{Model}Repository` (e.g., ProductRepository)

## When to Create New Domain

Create new domain when:
- Feature has its own database tables
- Feature has distinct business rules
- Feature can operate independently

Share dokumentasi ini dengan seluruh tim dan enforce during code review.

Migration Checklist

Buat checklist untuk setiap domain yang akan di-refactor:

  • Buat struktur folder domain baru
  • Copy Models ke domain folder
  • Copy Controllers ke domain folder
  • Buat Services untuk business logic
  • Buat Repositories untuk complex queries
  • Update routes file
  • Update namespace di semua file
  • Update import di file yang reference domain ini
  • Run all tests
  • Deploy to staging
  • Monitor for errors
  • Hapus file lama setelah 1 sprint tanpa issue

Checklist ini memastikan tidak ada step yang terlewat.

Tips Maintenance Struktur Folder Jangka Panjang

Struktur folder yang bagus hari ini bisa jadi berantakan besok kalau tidak ada discipline.

Code Review Fokus ke Structure

Saat code review, jangan hanya review logic, tapi juga structure.

Reject PR yang taruh file di tempat yang salah atau tidak ikuti convention.

Automated Linting untuk Structure

Gunakan tools seperti PHP_CodeSniffer atau PHPStan untuk enforce structure rules:

// phpstan.neon
parameters:
    level: 8
    paths:
        - app
    excludePaths:
        - app/Domains/*/Migrations
    ignoreErrors:
        - '#Call to an undefined method App\\Domains\\.+\\Models\\.+::.+\(\)#'

Refactor Sprint Rutin

Alokasikan waktu khusus setiap quarter untuk refactoring.

Tidak perlu banyak, 1-2 hari cukup untuk cleanup code smell dan improve structure.

Onboarding Document untuk Developer Baru

Update onboarding document setiap kali ada perubahan structure convention.

Developer baru harus paham structure sejak hari pertama, bukan trial and error.

Kesimpulan

Struktur folder yang baik bukan tentang ikut trend atau pamer arsitektur yang kompleks.

Ini tentang memudahkan tim untuk navigate codebase, mengurangi cognitive load, dan accelerate development velocity.

Mulai dengan refactor domain terkecil, buat convention yang jelas, dan enforce lewat code review.

Project Laravel kamu tidak harus perfect dari hari pertama.

Yang penting ada commitment untuk continuous improvement dan team discipline untuk maintain structure quality.

Selamat refactoring!

Ajie Kusumadhany
Written by

Ajie Kusumadhany

Founder & Lead Developer KerjaKode. Berpengalaman dalam pengembangan web modern dengan Laravel, React.js, Vue.js, dan teknologi terkini. Passionate tentang coding, teknologi, dan berbagi pengetahuan melalui artikel.

Promo Spesial Hari Ini!

10% DISKON

Promo berakhir dalam:

00 Jam
:
00 Menit
:
00 Detik
Klaim Promo Sekarang!

*Promo berlaku untuk order hari ini

0
User Online
Halo! 👋
Kerjakode Support Online
×

👋 Hai! Pilih layanan yang kamu butuhkan:

Chat WhatsApp Sekarang