Struktur bawaan Laravel mengelompokkan kode berdasarkan jenis-nya: semua controller di app/Http/Controllers, semua model di app/Models, semua action di app/Actions. Pada aplikasi kecil pengelompokan ini ringan dan mudah diikuti. Masalah muncul ketika satu fitur — katakanlah blog — menyebar ke lima folder berbeda, dan fitur tagihan menyebar ke lima folder yang sama: mengubah satu fitur berarti membuka lima direktori sekaligus. Domain organization membalik sudut pandangnya — mengelompokkan kode berdasarkan fitur, bukan jenis, sehingga semua yang milik blog hidup di satu tempat: app/Blog/. Chapter ini melakukan reorganisasi sungguhan di aplikasi sandbox kita dan membuktikan test tetap hijau sebelum, selama, dan sesudah pemindahan.
Struktur Default: Berdasarkan Layer #
Berikut isi penuh folder app/ pada skeleton Laravel 13 yang baru diinstal:
app/
├── Http/Controllers/Controller.php
├── Models/User.php
└── Providers/AppServiceProvider.phpTiga berkas — itulah seluruhnya. Sejak Laravel 11, skeleton sengaja datar: folder app/ hampir kosong, dan folder seperti app/Http/Middleware atau app/Console/Commands baru dibuat saat ada perintah make:* yang membutuhkannya. Kedataran ini kabar baik untuk domain organization: tidak banyak struktur bawaan yang harus dilawan. Namun perintah make:model tetap menaruh hasilnya di app/Models dan make:controller di app/Http/Controllers — konvensi layer tetap menjadi default yang aktif.
Alternatif: Berdasarkan Domain #
Prinsipnya sederhana: satu folder per domain fitur, dan di dalamnya kode diatur menurut jenis. Berikut struktur aplikasi sandbox kita setelah domain Blog dan Billing dipisahkan:
app/
├── Actions/PlaceOrder.php
├── Billing/
│ ├── Actions/IssueInvoice.php
│ └── Models/Invoice.php
├── Blog/
│ ├── Actions/PublishPost.php
│ ├── Exceptions/PostSudahTerbit.php
│ ├── Http/Controllers/PostController.php
│ └── Models/Post.php
├── Console/Commands/BuatPesananCommand.php
├── Exceptions/KeranjangKosong.php
├── Http/Controllers/{Controller,OrderController}.php
├── Jobs/BuatPesananJob.php
├── Models/{Order,OrderItem,User}.php
└── Providers/AppServiceProvider.phpSemua kode milik blog kini berada dalam satu folder app/Blog/ — action, controller, model, sampai exception khususnya. Perhatikan bahwa struktur ini inkremental: PlaceOrder masih tinggal di app/Actions gaya lama, User tetap di app/Models karena dipakai lintas domain, dan keduanya sama sekali tidak mengganggu. Reorganisasi tidak harus selesai sekaligus.
Melakukan Reorganisasi #
Pemindahan satu kelas terdiri dari dua langkah: memindahkan berkas, lalu mengubah namespace agar sesuai lokasi barunya. PublishPost pindah ke app/Blog/Actions/PublishPost.php dengan namespace baru App\Blog\Actions:
<?php
namespace App\Blog\Actions;
use App\Blog\Exceptions\PostSudahTerbit;
use App\Blog\Models\Post;
class PublishPost
{
public function __invoke(Post $post): Post
{
if ($post->published_at !== null) {
throw new PostSudahTerbit($post->slug);
}
$post->update(['published_at' => now()]);
return $post;
}
}Tidak ada satu baris pun di composer.json yang perlu diubah. Mapping PSR-4 bawaan tetap App\ menunjuk app/, sehingga subfolder otomatis menjadi subnamespace — app/Blog/Models/Post.php adalah App\Blog\Models\Post tanpa konfigurasi tambahan.
Route ikut dipisah ke routes/blog.php, lalu didaftarkan lewat parameter then pada withRouting() di bootstrap/app.php:
<?php
// bootstrap/app.php — bagian withRouting
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware('web')
->group(base_path('routes/blog.php'));
},
)route:list memastikan route tetap terdaftar dengan controller di namespace barunya:
Output yang Dihasilkan:
$ php artisan route:list --path=posts
POST posts/{post}/publish .................. posts.publish › App\Blog\Http\Controllers\PostController@publish
Showing [1] routesPerintah make:* juga perlu diberi namespace lengkap. Tanpa prefiks App\, Laravel menempelkan lokasi default sehingga hasilnya salah:
Output yang Dihasilkan:
$ php artisan make:model 'Billing\Models\Invoice'
INFO Model [app/Models/Billing/Models/Invoice.php] created successfully.
$ php artisan make:model 'App\Billing\Models\Invoice'
INFO Model [app/Billing/Models/Invoice.php] created successfully.Invokasi kedua yang benar: sebut kelas lengkap dari App\ agar berkas mendarat di dalam domain. Keseluruhan reorganisasi di atas ditutup dengan satu kali php artisan test:
Output yang Dihasilkan:
PASS Tests\Feature\BillingDomainTest
✓ issue invoice membuat tagihan pesanan 0.15s
PASS Tests\Feature\BlogDomainTest
✓ post bisa terbit lewat route 0.02s
✓ post kedua kalinya ditolak 0.02s
PASS Tests\Feature\PlaceOrderActionTest
✓ action bisa dites langsung tanpa http 0.01s
Tests: 10 passed (20 assertions)
Duration: 0.34sApa yang Pindah, Apa yang Tinggal #
| Kode | Keputusan |
|---|---|
| Controller, model, action, exception milik satu fitur | Pindah ke folder domainnya |
Model lintas domain (User), base Controller, provider | Tetap di lokasi bersama |
| Migrasi database | Tetap terpusat di database/migrations — urutan kronologis lintas domain harus tetap satu alur |
| Route | Boleh dipecah per domain (routes/blog.php, routes/billing.php) |
| View Blade | Opsional: resources/views/blog/ agar template juga mengelompok |
Alasan baris migrasi layak dijelaskan: migration berjalan berdasarkan urutan waktu, dan dua domain yang sama-sama menyentuh tabel yang sama akan saling bergantung urutannya. Menyatukan semua migrasi di satu folder menjaga sejarah schema tetap linear dan mudah dibaca.
Kapan Pindah, Kapan Bertahan #
Domain organization membayar dirinya ketika aplikasi punya sekitar sepuluh model atau lebih dan tim terbagi menurut fitur — satu orang memelihara blog, orang lain memelihara tagihan. Setiap tim menggarap foldernya sendiri, konflik merge menurun, dan keputusan bisa dibaca dari struktur. Di bawah ambang tersebut, struktur default lebih murah: folder app/Models berisi 15 berkas masih lebih mudah dinavigasi daripada 15 folder domain yang masing-masing berisi satu model.
Ada pula jalan tengah untuk klaster yang baru membesar: tetap berada di struktur layer, tetapi kelompokkan per domain di dalamnya — app/Services/Blog/, app/Services/Billing/. Biarkan klaster itu pindah menjadi domain penuh ketika anggotanya benar-benar banyak. Panduan resmi mengenai struktur bawaan tetap jadi rujukan di dokumentasi structure
.
Struktur folder adalah wadah; isinya tetap kode yang harus lari di production. Wadah berikutnya yang kita rapikan bukan folder, melainkan sesuatu yang lebih kecil namun lebih sering disentuh: berkas konfigurasi dan environment.