Kita sudah punya seluruh perangkatnya: deklarasi namespace, import, alias, resolusi nama, sampai autoloading Composer yang bekerja otomatis. Namun perangkat tidak menghasilkan desain — dan struktur namespace yang buruk akan terasa semakin menyakitkan seiring proyek membesar, karena nama class adalah kontrak yang dipakai konsumen lintas berkas bahkan lintas paket. Chapter penutup ini membahas sisi desainnya: bagaimana mengelompokkan class, seberapa dalam struktur namespace sebaiknya, dan bagaimana membiarkan proyek berevolusi tanpa merusak orang lain. Prinsipnya sederhana: keputusan struktur yang diambil hari ini menentukan biaya perubahan enam bulan ke depan.

Mengelompokkan Berdasarkan Domain atau Berdasarkan Tipe #

Ada dua gaya pengelompokan yang dominan. Gaya by type mengelompokkan class berdasarkan jenis teknisnya: Controllers/, Models/, Repositories/. Gaya by domain mengelompokkan berdasarkan area bisnis yang dilayaninya: Pesanan/, Pembayaran/, Pelanggan/ — di dalam tiap domain boleh ada controller, model, dan repository sekaligus. Contoh berikut membandingkan jalur berkas yang dihasilkan kedua gaya untuk tiga class yang sama:

php
<?php
function jalurBerdasarTipe(string $kelasTerakhir): string
{
    $tipe = match (true) {
        str_ends_with($kelasTerakhir, 'Controller') => 'Controllers',
        str_ends_with($kelasTerakhir, 'Repository') => 'Repositories',
        default => 'Models',
    };

    return "app/$tipe/$kelasTerakhir.php";
}

function jalurBerdasarDomain(string $domain, string $kelasTerakhir): string
{
    return "src/$domain/$kelasTerakhir.php";
}

$kelasPesanan = ['PesananController', 'Pesanan', 'PesananRepository'];

foreach ($kelasPesanan as $kelas) {
    printf(
        "%-20s tipe: %-40s domain: %s\n",
        $kelas,
        jalurBerdasarTipe($kelas),
        jalurBerdasarDomain('Pesanan', $kelas),
    );
}

Output yang Dihasilkan:

text
PesananController    tipe: app/Controllers/PesananController.php    domain: src/Pesanan/PesananController.php
Pesanan              tipe: app/Models/Pesanan.php                   domain: src/Pesanan/Pesanan.php
PesananRepository    tipe: app/Repositories/PesananRepository.php   domain: src/Pesanan/PesananRepository.php

Perhatikan hasilnya: pada gaya by type, tiga class yang membicarakan satu hal yang sama — pesanan — tersebar ke tiga folder berbeda. Setiap kali fitur pesanan berubah, kita lompat-lompat antar folder. Pada gaya by domain, ketiganya berdampingan dalam src/Pesanan/, dan satu fitur bisnis bisa dibaca utuh dari satu tempat. Inilah alasan gaya by domain populer untuk proyek yang tumbuh besar, sementara gaya by type umum pada proyek kecil yang strukturnya sederhana.

Perbandingan ringkasnya:

AspekBerdasarkan tipeBerdasarkan domain
Strukturapp/Controllers/, app/Models/src/Pesanan/, src/Pembayaran/
Cocok untukproyek kecil, tim satu alurproyek besar, banyak tim/fitur
Mengubah satu fiturmenyentuh banyak folderterkonsentrasi di satu folder
Jebakanfolder raksasa seperti Models/ berisi ratusan class tak berkerabatbutuh disiplin menentukan batas domain

Arsitektur Berlapis Domain-Application-Infrastructure #

Bentuk khusus gaya by domain yang banyak dipakai adalah pemisahan berdasarkan lapisan tanggung jawab. Namespace App\Domain berisi aturan bisnis murni — entitas dan antarmuka kontrak. Namespace App\Application berisi alur kasus pakai yang mengoordinasikan aturan bisnis. Namespace App\Infrastructure berisi detail teknis: akses database, kirim email, baca berkas. Berikut pemetaan lapisan tersebut ke jalur berkas:

php
<?php
$lapisan = [
    'Domain' => ['Pesanan', 'Pelanggan'],
    'Application' => ['BuatPesanan'],
    'Infrastructure' => ['RepositorySqlite'],
];

foreach ($lapisan as $namaLapisan => $daftarKelas) {
    foreach ($daftarKelas as $kelas) {
        $fqcn = "App\\$namaLapisan\\$kelas";
        $jalur = 'src/' . $namaLapisan . '/' . $kelas . '.php';
        printf("%-35s -> %s\n", $fqcn, $jalur);
    }
}

Output yang Dihasilkan:

text
App\Domain\Pesanan                  -> src/Domain/Pesanan.php
App\Domain\Pelanggan                -> src/Domain/Pelanggan.php
App\Application\BuatPesanan         -> src/Application/BuatPesanan.php
App\Infrastructure\RepositorySqlite -> src/Infrastructure/RepositorySqlite.php

Hasilnya memperlihatkan pola yang rapi: namespace dan direktori saling mencerminkan persis seperti tuntutan PSR-4, dan setiap class punya tempat yang jelas. Diagram berikut merangkum struktur lengkapnya:

Ketergantungan mengalir satu arah: Application memakai Domain, Infrastructure mengimplementasikan kontrak dari Domain. Detail lengkap pola arsitektur seperti ini kita bahas tuntas pada Section Design Patterns dan Prinsip — di sini cukup pahami bahwa pemilihan namespace adalah alat utamanya.

Datar atau Dalam #

Seberapa dalam struktur namespace sebaiknya? Aturan praktisnya: gunakan kedalaman secukupnya untuk menghilangkan bentrok nama, jangan lebih. Prefix vendor seperti App\ mencegah bentrok dengan paket luar; satu tingkat domain seperti App\Pembayaran\ mencegah bentrok antar fitur internal. Di bawah itu, tambahkan tingkat baru hanya bila sebuah area benar-benar punya sub-area yang padat class.

Struktur terlalu dalam menimbulkan biaya keterbacaan: import use App\Domain\Pesanan\Nilai\Total\Nominal; membuat pembaca menebak-nebak lokasi berkasnya. Struktur terlalu datar menimbulkan folder raksasa yang tak lagi bisa dipindai. Kedalaman dua sampai empat tingkat biasanya merupakan titik seimbang untuk proyek menengah.

Namespace sebagai Kontrak Publik #

Nama yang kita publikasikan menjadi API: konsumen menuliskannya di use mereka, dan setiap perubahan nama menuntut mereka ikut berubah. Karena itu, bagian yang paling layak distabilkan adalah antarmuka (interface) — kontrak perilaku yang implementasinya boleh datang dari mana saja. Contoh berikut memperlihatkan pola tersebut (dipadatkan dalam satu berkas agar mudah dibaca; pada proyek nyata, pisahkan tiap class ke berkasnya masing-masing):

php
<?php
namespace App\Domain;

interface PenyimpananPesanan
{
    public function simpan(string $nomor, array $data): bool;
}

namespace App\Infrastructure;

use App\Domain\PenyimpananPesanan;

class PenyimpananSqlite implements PenyimpananPesanan
{
    public function simpan(string $nomor, array $data): bool
    {
        // detail SQL tidak relevan untuk contoh ini
        return true;
    }
}

namespace App\Application;

use App\Domain\PenyimpananPesanan;
use App\Infrastructure\PenyimpananSqlite;

function prosesPesanan(PenyimpananPesanan $penyimpanan, string $nomor): string
{
    $berhasil = $penyimpanan->simpan($nomor, ['status' => 'baru']);

    return $berhasil ? "pesanan $nomor tersimpan" : "pesanan $nomor gagal";
}

echo prosesPesanan(new PenyimpananSqlite(), 'PSN-001'), "\n";

Output yang Dihasilkan:

text
pesanan PSN-001 tersimpan

Fungsi prosesPesanan() hanya mengenal nama App\Domain\PenyimpananPesanan — nama yang stabil karena hanya mendeklarasikan kontrak, bukan detail. Implementasi PenyimpananSqlite bisa dipindah, diganti versi MySQL, atau diberi alias, selama kontrak simpan() tetap sama. Semantik interface dibahas mendalam pada chapter interfaces ; di sini fokus kita adalah peran namanya sebagai jangkar stabilitas.

Evolusi Tanpa Merusak Konsumen #

Lalu apa yang terjadi bila kita tetap harus memindahkan atau mengganti nama class? Konsumen lama yang belum diperbarui akan langsung tersengat. Contoh berikut mensimulasikan refactor: class PengirimEmail dipindah dari App\Service ke App\Infrastructure, tetapi satu konsumen masih memanggil nama lamanya:

php
<?php
namespace App\Infrastructure;

class PengirimEmail
{
    public function kirim(string $tujuan): string
    {
        return "email terkirim ke $tujuan";
    }
}

namespace AplikasiKonsumen;

echo "Konsumen lama memanggil nama class sebelum refactor...\n";
$namaSebelumRefactor = \App\Service\PengirimEmail::class;
new $namaSebelumRefactor();

Output yang Dihasilkan:

text
Konsumen lama memanggil nama class sebelum refactor...

Fatal error: Uncaught Error: Class "App\Service\PengirimEmail" not found in /home/z/my-project/php-course/content/php/namespaces-dan-autoloading/merancang-namespace-project/.verify_tmp.php:16
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/namespaces-dan-autoloading/merancang-namespace-project/.verify_tmp.php on line 16

Konsumen lama menampilkan pesannya, lalu tersengat Fatal error: Class "App\Service\PengirimEmail" not found — nama lama sudah tidak dikenal siapa pun, termasuk autoloader. Inilah yang disebut breaking change: perubahan internal kita menjadi error fatal di kode orang lain.

PHP menyediakan satu alat bantu untuk masa transisi: class_alias() mendaftarkan nama lama sebagai alias dari nama baru, sehingga kedua nama hidup berdampingan:

php
<?php
namespace App\Infrastructure;

class PengirimEmail
{
    public function kirim(string $tujuan): string
    {
        return "email terkirim ke $tujuan";
    }
}

namespace AplikasiKonsumen;

use App\Infrastructure\PengirimEmail;

// Jembatan masa transisi: nama lama tetap hidup sebagai alias
class_alias(PengirimEmail::class, 'App\Service\PengirimEmail');

$konsumenLama = new \App\Service\PengirimEmail();
echo $konsumenLama->kirim('[email protected]'), "\n";

$konsumenBaru = new PengirimEmail();
echo $konsumenBaru->kirim('[email protected]'), "\n";

Output yang Dihasilkan:

text
email terkirim ke [email protected]
email terkirim ke [email protected]

Kedua pemanggilan berhasil: konsumen lama tetap memakai App\Service\PengirimEmail yang kini hanyalah alias, sementara konsumen baru memanggil nama aslinya. Pasang alias pada satu-dua versi rilis, umumkan bahwa nama lama akan dihapus, lalu lepas alias tersebut di rilis mayor berikutnya.

Tabel Keputusan #

Rangkuman keputusan desain pada chapter ini dalam satu tabel:

KeputusanPilih bilaHindari bila
Kelompok per domain (src/Pesanan/)fitur bisnis jelas dan padat classproyek main-main beberapa berkas saja
Kelompok per tipe (app/Models/)proyek kecil, struktur datar sudah cukupjumlah class per folder melewati belasan
Lapisan Domain-Application-Infrastructureaturan bisnis perlu terpisah dari detail teknisaplikasi CRUD sederhana tanpa logika berarti
Kedalaman 2-4 tingkatcukup memisahkan vendor dan domainpenamaan murni gaya-gayaan tanpa pemisah nyata
Interface sebagai kontrak publikbanyak implementasi, konsumen lintas paketantarmuka hanya punya satu implementasi abadi
class_alias() untuk transisikonsumen lama butuh waktu bermigrasisolusi permanen — alias pun harus dihapus suatu hari

Ringkasan #

Merancang namespace adalah keputusan arsitektur: kelompokkan berdasarkan domain agar fitur bisnis utuh di satu tempat, jaga kedalaman secukupnya, patuhi satu class satu berkas, dan perlakukan nama publik sebagai kontrak yang mahal diubah. Ketika evolusi tak terhindarkan, gunakan interface sebagai jangkar stabilitas dan class_alias() sebagai jembatan masa transisi. Dengan fondasi namespaces dan autoloading yang tuntas ini, kita siap melangkah ke Section Modern PHP — mulai dari fitur yang mengubah cara kita memanggil fungsi: named arguments.

Referensi #