Bagian 1 menutup dengan sebuah janji yang belum ditepati: composer.json memetakan namespace App\Util\ ke direktori src/, tetapi src/ masih kosong sehingga tidak ada yang bisa dimuat. Bagian 2 menepati janji itu dengan kelas pertama library kita — App\Util\SlugGenerator yang mengubah judul bebas menjadi slug URL. Kita akan membuktikan kontrak autoloading pada dua level: dulu dengan autoloader kecil buatan sendiri lewat spl_autoload_register(), lalu dengan Composer sungguhan melalui sebuah proyek konsumen yang memasang budi/slugify sebagai dependensi.

Kontrak PSR-4 dalam Praktik #

PSR-4 adalah aturan yang mengikat nama kelas lengkap (fully qualified class name) dengan lokasi berkasnya. Untuk pemetaan App\Util\ → src/ milik kita, aturannya bekerja dalam tiga langkah: buang prefix App\Util\ dari nama kelas, ubah sisa backslash menjadi garis miring, lalu rangkai menjadi src/ + hasil + .php. Karena aturan ini murni manipulasi string, kita bisa melihat langsung bagaimana berbagai nama kelas diterjemahkan menjadi path:

php
<?php

$prefix = 'App\\Util\\';

$kelas = [
    'App\\Util\\SlugGenerator',
    'App\\Util\\Exception\\InvalidInputException',
    'App\\Util\\Formatter\\KodePos',
];

foreach ($kelas as $nama) {
    $relative = str_replace('\\', '/', substr($nama, strlen($prefix)));
    echo str_pad($nama . ' ', 46, '.'), ' src/', $relative, ".php\n";
}

Output yang Dihasilkan:

text
App\Util\SlugGenerator ....................... src/SlugGenerator.php
App\Util\Exception\InvalidInputException ..... src/Exception/InvalidInputException.php
App\Util\Formatter\KodePos ................... src/Formatter/KodePos.php

Nama kelas yang tidak diawali App\Util\ bukan urusan autoloader ini — ia tidak akan pernah menunjuk ke src/ kita. Struktur folder di bawah src/ boleh bertingkat sesuai sub-namespace, seperti baris kedua yang menunjuk ke src/Exception/. Kelas InvalidInputException itu sendiri baru kita tulis pada Bagian 3; barisnya di atas adalah rancangan path-nya, bukan berkas yang sudah ada.

Kelas Pertama: SlugGenerator #

Tiba saatnya menulis kode sesungguhnya. Berikut isi lengkap src/SlugGenerator.php versi dasar — inilah seluruh isi package kita saat ini:

php
<?php

declare(strict_types=1);

namespace App\Util;

/**
 * Pembuat slug URL versi dasar.
 *
 * Slug adalah versi judul yang aman dipakai di URL: huruf kecil,
 * hanya huruf dan angka, dengan tanda hubung sebagai pemisah.
 */
final class SlugGenerator
{
    /**
     * Ubah judul menjadi slug URL.
     *
     * Contoh: "Belajar PHP Dasar" menjadi "belajar-php-dasar".
     */
    public function generate(string $text): string
    {
        // 1. Kecilkan semua huruf, lalu buang spasi di awal dan akhir.
        $slug = strtolower(trim($text));

        // 2. Ganti setiap rentetan karakter non-alfanumerik dengan satu tanda hubung.
        $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';

        // 3. Buang tanda hubung yang menempel di awal dan akhir hasil.
        return trim($slug, '-');
    }
}

Mari kita bedah isi berkas ini baris demi baris, karena setiap potongannya adalah keputusan penulis library:

  1. declare(strict_types=1) — memaksa tipe yang ketat untuk berkas ini. Type declaration string $text pada parameter menjadi penjaga pertama: pemanggil tidak bisa menyelundupkan tipe yang tidak sesuai.
  2. namespace App\Util; — mendeklarasikan bahwa semua simbol di berkas ini hidup di namespace App\Util; inilah sisi kiri kontrak PSR-4.
  3. final class SlugGenerator — kelas ini tertutup untuk pewarisan. Library kecil jarang dirancang untuk di-extend; final mencegah pengguna membungkusnya dengan subclass yang perilakunya menyimpang, dan memberi kita kebebasan mengubah isi tanpa takut merusak anak kelas orang lain.
  4. generate(string $text): string — satu-satunya method publik. Menerima judul apa pun, mengembalikan slug; string pada parameter dan return menjaga kontrak pada level bahasa.
  5. Tiga langkah di dalam generate() — strtolower(trim($text)) merapikan huruf dan tepi; preg_replace('/[^a-z0-9]+/', ...) mengganti setiap rentetan karakter di luar huruf kecil dan angka (spasi, titik dua, tanda seru, simbol &) menjadi satu tanda hubung; trim($slug, '-') membuang tanda hubung yang menempel di ujung. Pola ?? '' setelah preg_replace() adalah pengaman: pada kondisi PCRE yang ekstrem fungsi ini bisa mengembalikan null, dan library tidak boleh diam-diam mengubah null menjadi slug.
  6. Berkas berisi kelas saja, tanpa efek samping — tidak ada echo, tidak ada kode yang berjalan saat berkas dimuat. Inilah syarat berkas yang di-autoload: ia hanya mendeklarasikan simbol, sesuai semangat PSR-1.

Menguji Tanpa Autoloader #

Karena berkasnya murni deklarasi kelas, cara paling sederhana memakainya adalah require manual lalu mengacu kelas dengan nama lengkapnya:

php
<?php

require __DIR__ . '/src/SlugGenerator.php';

$generator = new App\Util\SlugGenerator();

$judul = [
    'Belajar PHP Dasar',
    '  Rilis  v2.0: Perbaikan Bug!  ',
    'Tips & Trik -- Untuk Pemula',
];

foreach ($judul as $teks) {
    echo str_pad(json_encode($teks), 34), ' -> ';
    echo $generator->generate($teks), "\n";
}

Output yang Dihasilkan:

text
"Belajar PHP Dasar"                -> belajar-php-dasar
"  Rilis  v2.0: Perbaikan Bug!  "  -> rilis-v2-0-perbaikan-bug
"Tips & Trik -- Untuk Pemula"      -> tips-trik-untuk-pemula

Ketiga hasilnya benar: spasi ganda dan tanda baca runtuh menjadi satu tanda hubung, huruf besar menjadi kecil, dan tepi yang bersih tidak menyisakan tanda hubung. Namun versi dasar ini punya batas yang perlu kita akui secara jujur. Fungsi strtolower() hanya mengenal huruf ASCII — karakter beraksen tidak ikut dikecilkan dan langsung menjadi korban regex:

php
<?php

require __DIR__ . '/src/SlugGenerator.php';

$generator = new App\Util\SlugGenerator();

echo 'Résumé Terbaru 2026 -> ', $generator->generate('Résumé Terbaru 2026'), "\n";

Output yang Dihasilkan:

text
Résumé Terbaru 2026 -> r-sum-terbaru-2026

Huruf É dan é tidak dikenal [a-z0-9], sehingga keduanya digantikan tanda hubung dan judul beraksen menghasilkan slug yang hancur. Masalah ini kita selesaikan pada Bagian 3 dengan fungsi mb_* dan transliterasi; untuk sekarang, kita fokus pada satu hal yang lebih mendasar: bagaimana berkas kelas ini dimuat tanpa require manual.

Autoloader Buatan Sendiri #

Satu require untuk satu kelas masih terasa wajar — tapi bayangkan library dengan 50 kelas, dan setiap program pemakai harus tahu urutan require yang benar. PHP menyelesaikannya dengan autoload stack: saat interpreter menemukan nama kelas yang belum dimuat, ia memanggil satu per satu callback yang terdaftar di spl_autoload_register() sampai ada yang berhasil memuat kelasnya. Kita bisa menulis autoloader yang mengikuti PSR-4 sendiri, lengkap dengan jejak eksekusinya:

php
<?php

spl_autoload_register(static function (string $class): void {
    echo "[autoload] diminta: $class\n";

    $prefix = 'App\\Util\\';

    if (!str_starts_with($class, $prefix)) {
        return;
    }

    $relative = str_replace('\\', '/', substr($class, strlen($prefix)));
    $file = __DIR__ . '/src/' . $relative . '.php';

    if (is_file($file)) {
        echo "[autoload] memuat berkas: src/$relative.php\n";
        require $file;
    }
});

echo "Sebelum baris ini, berkas SlugGenerator.php belum dimuat.\n";

$generator = new App\Util\SlugGenerator();
echo $generator->generate('Autoloading PSR-4'), "\n";

Output yang Dihasilkan:

text
Sebelum baris ini, berkas SlugGenerator.php belum dimuat.
[autoload] diminta: App\Util\SlugGenerator
[autoload] memuat berkas: src/SlugGenerator.php
autoloading-psr-4

Urutan baris pada output adalah cerita utamanya: baris “Sebelum…” tercetak lebih dulu, membuktikan berkas kelas memang belum dimuat. Baru ketika new App\Util\SlugGenerator() dievaluasi, interpreter tidak menemukan kelas itu, memanggil callback kita, dan callback-nya menerjemahkan nama kelas menjadi path dengan aturan yang sama seperti demo di awal bagian. Callback yang baik punya tiga sifat yang terlihat di sini: ia memeriksa prefix sebelum bertindak (kelas milik vendor lain bukan urusannya), memeriksa keberadaan berkas dengan is_file() sebelum require (berkas yang hilang tidak menjadi fatal error di dalam autoloader), dan mendaftar tanpa melempar apa pun saat pemuatan gagal — cukup diam, mungkin loader lain di stack bisa menangani.

Autoloader inilah yang digenerate Composer secara otomatis dari composer.json kita — dengan satu perbedaan penting: versi Composer sudah dioptimasi, mendukung banyak prefix sekaligus, dan tidak mencetak apa pun.

Sesi Nyata: Package dalam Proyek Konsumen #

Pengujian paling jujur terjadi ketika package dipakai oleh proyek lain, persis seperti pengguna sungguhan. Kita raket package lengkap di luar berkas materi — salin composer.json dari Bagian 1 dan src/SlugGenerator.php dari bagian ini:

bash
$ mkdir -p /tmp/slugify-dev/src
$ cp composer.json /tmp/slugify-dev/                    # dari bundle Bagian 1
$ cp src/SlugGenerator.php /tmp/slugify-dev/src/        # dari bundle Bagian 2

Lalu kita buat proyek konsumen di direktori terpisah. Kuncinya ada pada bagian repositories: secara bawaan Composer hanya mencari package di Packagist, tetapi path repository memberitahunya untuk mencari budi/slugify di direktori lokal:

json
{
    "name": "budi/konsumen-slugify",
    "description": "Proyek contoh yang memakai budi/slugify dari path repository.",
    "repositories": [
        {
            "type": "path",
            "url": "/tmp/slugify-dev"
        }
    ],
    "require": {
        "budi/slugify": "@dev"
    }
}

Simpan sebagai composer.json di /tmp/konsumen-slugify/, lalu jalankan pemasangan:

bash
$ cd /tmp/konsumen-slugify
$ composer install
text
Composer could not detect the root package (budi/konsumen-slugify) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version
No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking budi/slugify (dev-main fb3e343)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing budi/slugify (dev-main fb3e343): Symlinking from /tmp/slugify-dev
Generating autoload files

Dua detail pada sesi ini menjelaskan cara kerja path repository. Penanda versi @dev diperlukan karena package lokal kita belum punya git tag, sehingga Composer menganggapnya sebagai versi cabang pengembangan dev-main. Lalu baris Symlinking from /tmp/slugify-dev menunjukkan bahwa Composer tidak menyalin berkas, melainkan membuat tautan simbolik: vendor/budi/slugify menunjuk balik ke /tmp/slugify-dev, sehingga setiap perubahan pada package langsung terlihat oleh konsumen tanpa pemasangan ulang.

Sekarang bukti akhirnya — program konsumen yang memakai kelas kita tanpa satu baris require untuk berkas kelas:

php
<?php

require __DIR__ . '/vendor/autoload.php';

use App\Util\SlugGenerator;

$generator = new SlugGenerator();

echo $generator->generate('Panduan Lengkap PHP 8'), "\n";
echo $generator->generate('Migrasi dari Laravel 10 ke 11'), "\n";

Simpan sebagai demo.php di /tmp/konsumen-slugify/ dan jalankan:

bash
$ php demo.php
text
panduan-lengkap-php-8
migrasi-dari-laravel-10-ke-11

Tidak ada require __DIR__ . '/src/SlugGenerator.php' — satu-satunya yang dimuat adalah vendor/autoload.php, dan dari situlah nama App\Util\SlugGenerator diterjemahkan. Bagaimana? Dengan melihat apa yang Composer tulis di dalam vendor/composer/autoload_psr4.php milik si konsumen:

php
<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'App\\Util\\' => array($vendorDir . '/budi/slugify/src'),
);

Pemetaannya persis deklarasi composer.json kita, tetapi jalurnya kini menunjuk ke dalam direktori vendor/ milik konsumen — lokasi tempat budi/slugify terpasang. Pada sesi Bagian 1 yang memasang package sebagai root, baris yang sama menunjuk ke $baseDir . '/src' di root proyek. Kontrak "App\\Util\\": "src/" kita ternyata relatif: Composer menerjemahkannya terhadap lokasi package, bukan lokasi siapa pun yang memakainya.

Apa yang Berubah #

Dibandingkan kerangka Bagian 1 yang hanya berisi composer.json:

  • src/SlugGenerator.php — kelas pertama library: App\Util\SlugGenerator::generate(string $text): string, huruf kecil ASCII, penggantian non-alfanumerik, dan perapian tanda hubung. Kelasnya final, berkasnya murni deklarasi tanpa efek samping.
  • Kontrak PSR-4 teruji dua arah — autoloader manual membuktikan mekanismenya, proyek konsumen di /tmp membuktikan paketnya benar-benar terpasang dan kelasnya termuat lewat vendor/autoload.php.
  • Batas versi dasar tercatat jujur: strtolower() merusak judul beraksen — utang yang dilunasi pada Bagian 3.

Yang belum ada: konfigurasi (separator masih hardcode -), batas panjang, dan validasi input. Bagian 3 mengubah generate() sederhana ini menjadi public API yang layak dipakai orang lain — tanpa merusak perilaku yang sudah terbukti hari ini.

Cara Menjalankan #

Dari direktori bagian-2-namespace-dan-autoloading/ pada bundle halaman ini:

bash
# 1. Demo manual — require langsung tanpa autoloader
php -r 'require "src/SlugGenerator.php"; echo (new App\Util\SlugGenerator())->generate("Belajar PHP Dasar"), "\n";'

# 2. Sesi proyek konsumen (ikuti langkah pada bagian di atas)
mkdir -p /tmp/slugify-dev/src
cp ../bagian-1-struktur-package/composer.json /tmp/slugify-dev/
cp src/SlugGenerator.php /tmp/slugify-dev/src/
mkdir /tmp/konsumen-slugify && cd /tmp/konsumen-slugify
# ... lalu tulis composer.json konsumen seperti di atas, lalu:
composer install && php demo.php

Berkas src/SlugGenerator.php yang dipakai seluruh sesi tersedia di bagian bawah halaman. Sebagai rujukan resmi: standar PSR-4 untuk aturan pemetaannya, dokumentasi spl_autoload_register() untuk autoload stack, dan dasar penggunaan Composer untuk vendor/autoload.php. Dengan kelas yang hidup dan termuat, kita lanjut ke Bagian 3: merancang public API yang bisa dikonfigurasi, sadar unicode, dan menolak input yang tidak sah.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

src/SlugGenerator.php

Unduh berkas
php
<?php

declare(strict_types=1);

namespace App\Util;

/**
 * Pembuat slug URL versi dasar.
 *
 * Slug adalah versi judul yang aman dipakai di URL: huruf kecil,
 * hanya huruf dan angka, dengan tanda hubung sebagai pemisah.
 */
final class SlugGenerator
{
    /**
     * Ubah judul menjadi slug URL.
     *
     * Contoh: "Belajar PHP Dasar" menjadi "belajar-php-dasar".
     */
    public function generate(string $text): string
    {
        // 1. Kecilkan semua huruf, lalu buang spasi di awal dan akhir.
        $slug = strtolower(trim($text));

        // 2. Ganti setiap rentetan karakter non-alfanumerik dengan satu tanda hubung.
        $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';

        // 3. Buang tanda hubung yang menempel di awal dan akhir hasil.
        return trim($slug, '-');
    }
}