Antrean SPL pada chapter sebelumnya menyelesaikan masalah banyak autoloader dalam satu proyek, tetapi belum menyelesaikan masalah yang lebih besar: bagaimana pihak-pihak berbeda — framework, paket, dan aplikasi — sepakat tentang di mana sebuah class seharusnya berada? Tanpa kesepakatan, setiap paket membawa autoloader buatannya sendiri dan struktur foldernya saling tidak kompatibel. PSR-4, standar yang diterbitkan PHP-FIG (PHP Framework Interop Group), menjawabnya dengan satu aturan pemetaan sederhana dari namespace ke jalur berkas. Pada chapter ini kita bedah aturannya, lalu membangun autoloader PSR-4 minimal dengan tangan kita sendiri.

Masalah yang Ingin Diselesaikan PSR-4 #

Bayangkan Anda memasang tiga paket dari tiga vendor berbeda. Paket pertama menyimpan class di folder lib/, paket kedua meniru struktur Java dengan src/main/php/, paket ketiga menormalkan semua nama berkas ke huruf kecil. Masing-masing mendaftarkan autoloader sendiri ke antrean SPL — berfungsi, tetapi boros: setiap pemanggilan class yang gagal menembus seluruh antrean, dan setiap paket harus menjaga konfigurasi pemetaannya sendiri.

PSR-4 membalik pendekatannya: daripada setiap paket membawa mesin pencari sendiri, cukup ada satu autoloader bersama yang mengikuti konvensi penamaan seragam. Kesepakatan itu disebut autoloading standard: pemilik paket cukup mendeklarasikan “prefix namespace Toko\ saya berada di direktori src/”, dan autoloader standar siapa pun — termasuk milik Composer — langsung tahu cara menemukan semua class paket tersebut.

Aturan Pemetaan PSR-4 #

Inti PSR-4 dapat dirangkum dalam tiga kalimat. Sebuah class dipetakan lewat sepasang prefix namespace dan direktori dasar. Bagian nama class setelah prefix disebut nama class relatif, dan setiap separator \ di dalamnya menjadi separator direktori /. Nama class terakhir menjadi nama berkas dengan akhiran .php, dengan kapitalisasi yang harus persis sama.

Untuk class Acme\Log\FileWriter dengan prefix Acme\ yang dipetakan ke src/, pemetaannya terurai sebagai berikut:

BagianNilai
Prefix namespaceAcme\
Direktori dasarsrc/
Nama class lengkapAcme\Log\FileWriter
Nama class relatifLog\FileWriter
Jalur berkas akhirsrc/Log/FileWriter.php

Aturan yang sama berlaku berapa pun dalamnya subnamespace. Fungsi kecil berikut merangkum seluruh spesifikasi tersebut dalam empat baris:

php
<?php
function jalurPsr4(string $prefix, string $direktoriDasar, string $kelasLengkap): string
{
    $kelasRelatif = substr($kelasLengkap, strlen($prefix));

    return $direktoriDasar . '/' . str_replace('\\', '/', $kelasRelatif) . '.php';
}

echo jalurPsr4('Acme\\', 'src', 'Acme\\Log\\FileWriter'), "\n";
echo jalurPsr4('Acme\\', 'src', 'Acme\\Log\\Writer\\FileWriter'), "\n";
echo jalurPsr4('App\\', 'app', 'App\\Model\\Pengguna'), "\n";

Output yang Dihasilkan:

text
src/Log/FileWriter.php
src/Log/Writer/FileWriter.php
app/Model/Pengguna.php

Baris substr($kelasLengkap, strlen($prefix)) membuang prefix dan menyisakan nama class relatif. Baris str_replace('\\', '/', ...) mengubah setiap separator namespace menjadi separator direktori, lalu akhiran .php dilekatkan. Perhatikan bahwa nama berkas selalu sama dengan nama class terakhir — FileWriter.php, bukan filewriter.php atau file_writer.php.

Dengan aturan ini, struktur proyek kecil berikut sepenuhnya sah menurut PSR-4 asalkan prefix Toko\ dipetakan ke src/:

text
toko-proyek/
├── composer.json
└── src/
    ├── Kasir/
    │   └── Transaksi.php   -> class Toko\Kasir\Transaksi
    └── Laporan/
        └── Bulanan.php     -> class Toko\Laporan\Bulanan

Kapitalisasi Harus Persis Sama #

Spesifikasi PSR-4 menegaskan bahwa nama berkas harus cocok dengan nama class secara case-sensitive — meniru perilaku class PHP sendiri yang peka kapitalisasi pada nama berkas di sistem operasi Linux. Konsekuensinya mudah diuji:

php
<?php
$direktori = sys_get_temp_dir() . '/psr4-huruf-' . getmypid();
mkdir($direktori, 0777, true);
file_put_contents($direktori . '/Transaksi.php', "<?php\n\nclass Transaksi {}\n");

$namaKelas = 'transaksi'; // kapitalisasi salah

var_dump(is_file($direktori . '/' . $namaKelas . '.php'));
var_dump(is_file($direktori . '/Transaksi.php'));

unlink($direktori . '/Transaksi.php');
rmdir($direktori);

Output yang Dihasilkan:

text
bool(false)
bool(true)

Berkas Transaksi.php ada, tetapi pencarian dengan nama transaksi.php mengembalikan bool(false) pada sistem berkas Linux yang sensitif kapitalisasi.

Membangun Autoloader PSR-4 Minimal #

Sekarang kita rakit seluruh konsep menjadi autoloader PSR-4 yang benar-benar berfungsi. Autoloader ini menyimpan banyak pasangan prefix-direktori, memeriksa prefix yang paling panjang lebih dulu, dan memuat berkas pertama yang cocok. Seperti contoh sebelumnya, skrip berikut membuat berkas class sementara di direktori temporer lalu membersihkannya sendiri:

php
<?php
class AutoloaderPsr4
{
    /** @var array<string, list<string>> peta prefix namespace ke direktori dasar */
    private array $peta = [];

    public function daftarkan(string $prefix, string $direktoriDasar): void
    {
        $prefix = trim($prefix, '\\') . '\\';
        $this->peta[$prefix][] = rtrim($direktoriDasar, '/');

        uksort($this->peta, fn (string $a, string $b): int => strlen($b) <=> strlen($a));
    }

    public function muat(string $kelasLengkap): void
    {
        foreach ($this->peta as $prefix => $daftarDirektori) {
            if (!str_starts_with($kelasLengkap, $prefix)) {
                continue;
            }

            $kelasRelatif = substr($kelasLengkap, strlen($prefix));
            foreach ($daftarDirektori as $direktori) {
                $berkas = $direktori . '/'
                    . str_replace('\\', '/', $kelasRelatif) . '.php';
                if (is_file($berkas)) {
                    require $berkas;
                    return;
                }
            }
        }
    }
}

$akar = sys_get_temp_dir() . '/psr4-demo-' . getmypid();
mkdir($akar . '/modul-kasir', 0777, true);
mkdir($akar . '/src/Laporan', 0777, true);

file_put_contents($akar . '/modul-kasir/Transaksi.php', <<<'PHP'
<?php

namespace Toko\Kasir;

class Transaksi
{
    public const SUMBER = 'modul-kasir';
}
PHP);
file_put_contents($akar . '/src/Laporan/Bulanan.php', <<<'PHP'
<?php

namespace Toko\Laporan;

class Bulanan
{
    public const SUMBER = 'src';
}
PHP);

$autoloader = new AutoloaderPsr4();
$autoloader->daftarkan('Toko\\Kasir\\', $akar . '/modul-kasir');
$autoloader->daftarkan('Toko\\', $akar . '/src');
spl_autoload_register([$autoloader, 'muat']);

$transaksi = new \Toko\Kasir\Transaksi();
echo $transaksi::class, ' <- ', $transaksi::SUMBER, "\n";

$laporan = new \Toko\Laporan\Bulanan();
echo $laporan::class, ' <- ', $laporan::SUMBER, "\n";

unlink($akar . '/modul-kasir/Transaksi.php');
unlink($akar . '/src/Laporan/Bulanan.php');
rmdir($akar . '/modul-kasir');
rmdir($akar . '/src/Laporan');
rmdir($akar . '/src');
rmdir($akar);

Output yang Dihasilkan:

text
Toko\Kasir\Transaksi <- modul-kasir
Toko\Laporan\Bulanan <- src

Mari kita bedah komponen-komponennya:

  1. daftarkan() (baris 7-13): menormalkan prefix agar selalu diakhiri \, lalu menyimpan direktori dasarnya. Satu prefix boleh punya beberapa direktori dasar — semua dicoba bergantian.
  2. uksort(...) (baris 11): mengurutkan peta dari prefix terpanjang ke terpendek. Dengan begitu pemetaan khusus seperti Toko\Kasir\ selalu diperiksa sebelum pemetaan umum Toko\, meniru perilaku Composer.
  3. str_starts_with() (baris 18): memeriksa apakah prefix cocok dengan awal nama class. Jika tidak, continue melanjutkan ke prefix berikutnya — autoloader tidak boleh error saat menolak class milik orang lain.
  4. Penggabungan jalur (baris 22-23): identik dengan fungsi jalurPsr4() di awal chapter — potong prefix, ganti separator, tempel .php.
  5. spl_autoload_register([$autoloader, 'muat']) (baris 51): mendaftarkan metode muat sebagai callable array, persis bentuk yang kita pelajari di chapter SPL.
  6. Hasil eksekusi: Toko\Kasir\Transaksi termuat dari modul-kasir (prefix panjang menang), sedangkan Toko\Laporan\Bulanan tidak cocok dengan prefix Toko\Kasir\ dan turun ke prefix Toko\ yang memuatnya dari src.

Ketika Prefix Tidak Cocok #

Kekuatan antrean SPL baru terasa ketika beberapa autoloader yang saling tidak mengenal bekerja bersama. Autoloader yang prefix-nya tidak cocok wajib menolak dengan diam — tidak melempar error, tidak memuat berkas yang salah — sehingga antrean bisa melanjutkan ke autoloader berikutnya:

php
<?php
$jejak = [];

$akar = sys_get_temp_dir() . '/psr4-antrean-' . getmypid();
mkdir($akar . '/plugin', 0777, true);
file_put_contents($akar . '/plugin/Siaran.php', <<<'PHP'
<?php

namespace Plugin;

class Siaran
{
    public static function jalankan(): string
    {
        return 'siaran berjalan';
    }
}
PHP);

$loaderToko = function (string $kelas) use (&$jejak): void {
    $jejak[] = "loader Toko menolak: $kelas";
};

$loaderPlugin = function (string $kelas) use (&$jejak, $akar): void {
    $jejak[] = "loader Plugin memeriksa: $kelas";
    if (str_starts_with($kelas, 'Plugin\\')) {
        $berkas = $akar . '/plugin/' . substr($kelas, strlen('Plugin\\')) . '.php';
        if (is_file($berkas)) {
            require $berkas;
        }
    }
};

spl_autoload_register($loaderToko);
spl_autoload_register($loaderPlugin);

echo \Plugin\Siaran::jalankan(), "\n";
echo implode("\n", $jejak), "\n";

unlink($akar . '/plugin/Siaran.php');
rmdir($akar . '/plugin');
rmdir($akar);

Output yang Dihasilkan:

text
siaran berjalan
loader Toko menolak: Plugin\Siaran
loader Plugin memeriksa: Plugin\Siaran

Jejak menunjukkan urutan kejadian yang sehat: loader Toko menerima nama Plugin\Siaran lalu menolaknya karena tidak cocok dengan prefix-nya, kemudian loader Plugin memeriksa, cocok, dan memuat berkasnya. Method statis jalankan() baru bisa dipanggil setelah baris require berhasil — itulah tanda class benar-benar terdefinisi.

Composer Menggunakan PSR-4 #

Autoloader minimal kita hanya sekitar dua puluh baris, tetapi versi produksi harus menangani lebih banyak kasus: cache jalur, classmap teroptimasi, cek keamanan, hingga integrasi dengan ribuan paket. Composer, manajer paket standar de-facto PHP, mengimplementasikan PSR-4 sebagai strategi autoload utamanya — dan sebagian besar proyek PHP modern tidak pernah menulis autoloader manual karena cukup memakai milik Composer. Cara mendeklarasikan prefix PSR-4 di composer.json dan apa yang terjadi di balik vendor/autoload.php adalah topik chapter berikutnya.

Ringkasan #

PSR-4 mendefinisikan kontrak pemetaan yang membuat namespace dan struktur direktori saling mencerminkan: prefix dipetakan ke direktori dasar, separator namespace menjadi separator direktori, dan nama berkas wajib sama persis dengan nama class termasuk kapitalisasinya. Autoloader yang mengikuti kontrak ini bekerja berdampingan dengan autoloader lain lewat antrean SPL — menolak diam saat prefix tidak cocok. Dengan spesifikasi ini di tangan, kita siap memasuki praktik industri: mendeklarasikan pemetaan PSR-4 di Composer dan membiarkan vendor/autoload.php mengurus sisanya.

Referensi #