Sepuluh proyek sebelumnya berakhir sebagai aplikasi: program yang dipanggil pengguna akhir lewat browser atau terminal. Proyek penutup ini membalik posisi kita menjadi penulis library — kode yang dipanggil oleh kode developer lain lewat composer require. Selama enam bagian kita membangun budi/slugify, package pembuat slug URL yang siap dipublikasikan ke Packagist. Chapter Membuat Library PHP
sudah memperkenalkan bentuk paling ringkasnya; proyek ini menempuh jalur produksi penuh: struktur package, autoloading sungguhan, public API yang tervalidasi, test suite, static analysis, hingga rilis. Bagian 1 dimulai dari satu berkas yang paling menentukan: composer.json.
Dari Aplikasi ke Package #
Perbedaan library dan aplikasi bukan soal ukuran kode, melainkan siapa pemanggilnya. Aplikasi punya titik masuk (index.php, skrip CLI) dan berjalan atas namanya sendiri; library tidak punya titik masuk sama sekali — berkas-berkasnya dimuat oleh autoloader milik proyek lain. Konsekuensinya menjalar ke struktur direktori. Inilah rancangan lengkap package kita di akhir proyek:
slugify/
├── composer.json identitas package + kontrak autoloading (Bagian 1)
├── src/
│ ├── SlugGenerator.php kelas utama namespace App\Util (Bagian 2)
│ └── Exception/
│ └── InvalidInputException.php exception domain (Bagian 3)
├── tests/
│ └── SlugGeneratorTest.php test suite PHPUnit (Bagian 4)
├── phpstan.neon konfigurasi static analysis (Bagian 5)
└── README.md, CHANGELOG.md dokumentasi untuk calon pengguna (Bagian 6)Tidak ada public/, tidak ada router, tidak ada loop request. Yang tersisa hanyalah src/ berisi kelas-kelas plus berkas-berkas pendukung proses rilis. Struktur mungil ini adalah seluruh isi package — dan setiap itemnya punya alasan keberadaan yang akan kita bangun satu per satu.
Anatomi composer.json #
Berkas composer.json adalah kartu identitas sekaligus kontrak package: nama, deskripsi, lisensi, batas versi PHP, dependensi, dan aturan autoloading semuanya dideklarasikan di sini. Berikut isi lengkapnya untuk budi/slugify versi kerangka — berkas yang sama tersedia di bagian bawah halaman ini:
{
"name": "budi/slugify",
"description": "Generate URL-friendly slugs from any title: configurable separator, lowercase mode, and length limit.",
"type": "library",
"license": "MIT",
"keywords": ["slug", "url", "seo"],
"authors": [
{
"name": "Budi",
"email": "[email protected]"
}
],
"require": {
"php": "^8.1"
},
"autoload": {
"psr-4": {
"App\\Util\\": "src/"
}
}
}Tabel berikut memetakan peran setiap field pada keputusan kita:
| Field | Nilai kita | Peran |
|---|---|---|
name | budi/slugify | identitas unik vendor/nama di ekosistem Composer |
description | satu kalimat bahasa Inggris | tampil di hasil pencarian Packagist |
type | library | paket berupa pustaka, bukan proyek yang dijalankan |
license | MIT | izin pemakaian bagi pengguna package |
keywords | slug, url, seo | tag pencarian di Packagist |
authors | Budi | pencipta dan kontak |
require | php: ^8.1 | batas versi PHP dan daftar dependensi runtime |
autoload | psr-4 App\Util\ → src/ | kontrak pemetaan namespace ke direktori |
Dua keputusan pada tabel ini layak diperhatikan. Pertama, description sengaja ditulis dalam bahasa Inggris karena Packagist adalah katalog global — calon pengguna mencari dengan kata kunci Inggris, dan deskripsi adalah hal pertama yang mereka baca. Kedua, license wajib diisi jujur sejak awal: package tanpa lisensi berarti secara hukum orang lain tidak berhak memakainya. MIT adalah pilihan umum untuk library utilitas karena ringkas dan permissive.
Karena composer.json hanyalah JSON, kita bisa memeriksanya dengan alat bawaan PHP sendiri. PHP 8.3 memperkenalkan json_validate() yang memeriksa sintaks tanpa membangun struktur data — pas untuk validasi cepat:
<?php
$raw = file_get_contents(__DIR__ . '/composer.json');
if (!json_validate($raw)) {
echo "composer.json TIDAK valid: ", json_last_error_msg(), "\n";
exit(1);
}
$manifest = json_decode($raw, true);
foreach (['name', 'description', 'type', 'license', 'require', 'autoload'] as $field) {
echo str_pad($field, 13), ": ", array_key_exists($field, $manifest) ? "ada" : "HILANG", "\n";
}
echo "\n";
echo "nama : ", $manifest['name'], "\n";
echo "php : ", $manifest['require']['php'], "\n";
echo "psr-4 : App\\Util\\ => ", $manifest['autoload']['psr-4']['App\\Util\\'], "\n";Output yang Dihasilkan:
name : ada
description : ada
type : ada
license : ada
require : ada
autoload : ada
nama : budi/slugify
php : ^8.1
psr-4 : App\Util\ => src/Enam field kunci terbaca lengkap dari berkas sungguhan — bukan sekadar klaim di tabel. Pemeriksaan seperti ini adalah bentuk paling sederhana dari kebiasaan yang akan kita rawan sepanjang proyek: setiap klaim tentang package harus bisa diverifikasi oleh mesin.
Aturan Penamaan vendor/package #
Nama package adalah alamat permanennya di ekosistem. Formatnya selalu vendor/nama: bagian vendor adalah identitas Anda sebagai penerbit (biasanya sama dengan username GitHub atau organisasi), bagian kedua adalah nama proyek. Keduanya wajib huruf kecil, boleh memakai titik, garis bawah, dan tanda hubung, tetapi tidak boleh diawali atau diakhiri tanda baca — dan harus dipisahkan tepat satu garis miring.
Aturan itu bisa kita uji langsung terhadap beberapa kandidat nama:
<?php
$kandidat = [
'budi/slugify',
'Budi/Slugify',
'budi',
'budi/slugify/extra',
'budi/slugify-php',
'budi/slugify-php-',
];
foreach ($kandidat as $nama) {
$valid = preg_match('/^[a-z0-9]([a-z0-9_.-]*[a-z0-9])?\/[a-z0-9]([a-z0-9_.-]*[a-z0-9])?$/', $nama) === 1;
echo ($valid ? 'OK ' : 'TOLAK '), $nama, "\n";
}Output yang Dihasilkan:
OK budi/slugify
TOLAK Budi/Slugify
TOLAK budi
TOLAK budi/slugify/extra
OK budi/slugify-php
TOLAK budi/slugify-php-Nama pakai huruf besar ditolak, nama tanpa garis miring ditolak, dan segmen berlebih juga ditolak. Nama dipilih sekali dan dipakai selamanya: mengganti nama setelah publikasi berarti memaksa seluruh pengguna mengubah require mereka. Karena itu budi/slugify kita kunci sejak berkas pertama.
Nol Dependensi Runtime #
Perhatikan isi require kita: hanya "php": "^8.1" dan tidak ada satu pun package. Keputusan tanpa dependensi ini disengaja, dan alasannya berkaitan langsung dengan posisi kita sebagai pustaka. Setiap dependensi sebuah library otomatis menjadi dependensi setiap aplikasi yang memasangnya — pengguna mewarisi seluruh pohon dependensi kita beserta risikonya. Package utilitas kecil yang menyeret framework raksasa akan ditolak calon pengguna sebelum dibaca lebih jauh.
Membuat slug hanya butuh kemampuan yang sudah ada di PHP sendiri: manipulasi string dan regular expression. Ketika tugas bisa diselesaikan murni dengan PHP standar, dependensi terbaik adalah nol. Sebaliknya, dependensi mulai masuk akal ketika menulis ulang lebih mahal daripada memeliharanya — misalnya parser kompleks atau klien HTTP. Bahkan demikian, aturan praktisnya tetap sama: dependensi library harus sebanyak mungkin di require-dev, bukan require. PHPUnit yang akan kita pakai di Bagian 4 hidup di sana karena hanya dibutuhkan saat menguji, bukan saat pengguna memanggil generate().
Batas "php": "^8.1" sendiri menggunakan operator caret yang maknanya bisa kita buktikan dengan version_compare():
<?php
// ^8.1 artinya: >= 8.1.0 dan < 9.0.0
$versiPhp = ['8.1.0', '8.1.29', '8.2.0', '8.4.23', '9.0.0'];
foreach ($versiPhp as $v) {
$boleh = version_compare($v, '8.1.0', '>=') && version_compare($v, '9.0.0', '<');
echo str_pad($v, 10), $boleh ? 'lolos ^8.1' : 'ditolak ^8.1', "\n";
}Output yang Dihasilkan:
8.1.0 lolos ^8.1
8.1.29 lolos ^8.1
8.2.0 lolos ^8.1
8.4.23 lolos ^8.1
9.0.0 ditolak ^8.1Versi 8.1.0 sampai 8.4.23 lolos, 9.0.0 ditolak. Menargetkan 8.1 adalah kompromi yang disadari: kita memakai fitur yang hadir di PHP 8.1 seperti constructor promotion, tetapi menolak fitur 8.2 ke atas supaya jangkauan pengguna seluas mungkin — keputusan yang dibahas tuntas pada chapter Version Constraints .
Mulai dari Versi 0.1.0 #
Package membutuhkan nomor versi sejak rilis pertama, dan konvensinya adalah semantic versioning: tiga angka mayor.minor.patch dengan makna yang disepakati.
| Naiknya angka | Makna untuk pengguna |
|---|---|
patch (0.1.0 → 0.1.1) | perbaikan bug, perilaku tidak berubah |
minor (0.1.0 → 0.2.0) | fitur baru, kode lama tetap jalan |
mayor (0.1.0 → 1.0.0) | perubahan yang bisa merusak kode pengguna |
Angka mayor 0 punya makna khusus: package masih tahap pengembangan, API boleh berubah kapan saja, dan belum ada komitmen backward compatibility. Itulah sebabnya kita mulai dari 0.1.0, bukan 0.0.1 maupun 1.0.0 — 0.1.0 menyampaikan “sudah terpakai dan berguna, tetapi belum stabil”, sementara 1.0.0 adalah janji stabilitas yang belum sanggup kita bayar di awal proyek. Nomor versi sendiri tidak ditulis di composer.json: Packagist membacanya dari git tag, seperti akan kita praktikkan pada Bagian 6.
Memeriksa Package dengan Composer #
Teori cukup; sekarang kita minta Composer sendiri menilai kerangka package ini. Perintah composer validate memeriksa keabsahan composer.json dari sisi schema sekaligus praktik yang disarankan:
$ cd slugify
$ composer validate --no-check-publish./composer.json is validLulus tanpa keluhan. Selanjutnya, karena direktori vendor/ tidak boleh mengotori berkas materi ini, sesi pemasangan berikutnya kita jalankan pada salinan di /tmp — cara kerja yang sama dengan yang akan Anda lakukan di mesin sendiri:
$ mkdir -p /tmp/slugify && cp composer.json /tmp/slugify/
$ cd /tmp/slugify
$ composer installComposer could not detect the root package (budi/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
Nothing to modify in lock file
Writing lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Generating autoload filesTiga hal terbaca dari sesi itu. Pertama, Nothing to install — bukti nyata bahwa dependensi runtime kita memang nol. Kedua, Composer menolak menebak versi (could not detect the root package version) karena belum ada git tag, sehingga ia memakai 1.0.0 sebagai pengganti sementara di lingkungan lokal. Ketiga, baris Generating autoload files tetap berjalan meski tak ada package — autoloader kita sendiri sedang dibangun. Isi vendor/ membuktikannya:
$ ls vendor vendor/composervendor:
composer
vendor/composer:
ClassLoader.php
InstalledVersions.php
LICENSE
autoload_classmap.php
autoload_namespaces.php
autoload_psr4.php
autoload_real.php
autoload_static.php
installed.json
installed.php
platform_check.phpDi antara berkas-berkas itu ada platform_check.php yang berjaga di baris paling depan autoloader: ia melempar error sebelum satu kelas pun dimuat bila PHP berjalan di bawah 8.1 — batas "php": "^8.1" kita diterjemahkan Composer menjadi pemeriksaan runtime sungguhan. Berkas autoload_psr4.php yang memuat pemetaan App\Util\ akan kita bedah isinya pada Bagian 2, saat kelas pertama akhirnya ada untuk di-autoload.
Apa yang Berubah #
Setelah Bagian 1, package budi/slugify berdiri sebagai kerangka:
composer.json— identitas lengkap: namabudi/slugify, tipelibrary, lisensi MIT, deskripsi bahasa Inggris, dependensi runtime nol, dan pemetaan PSR-4App\Util\kesrc/.- Keputusan yang terkunci: aturan penamaan vendor/package, batas
"php": "^8.1", dan nomor versi awal0.1.0(ditulis sebagai git tag, bukan field dicomposer.json). - Belum ada
src/— pemetaan PSR-4 dicomposer.jsonmasih menunjuk direktori yang kosong.
Kontrak autoloading sudah dideklarasikan tetapi belum ada yang menepatinya. Bagian 2 menulis kelas App\Util\SlugGenerator pertama, membuktikan pemetaan App\Util\ → src/ bekerja — dulu dengan autoloader manual spl_autoload_register(), lalu dengan Composer sungguhan lewat sebuah proyek konsumen.
Cara Menjalankan #
Dari direktori bagian-1-struktur-package/ pada bundle halaman ini:
# 1. Validasi kartu identitas package
composer validate --no-check-publish
# 2. Sesi pemasangan di /tmp (vendor/ tidak mengotori materi)
mkdir -p /tmp/slugify && cp composer.json /tmp/slugify/
cd /tmp/slugify && composer install
# 3. Atau periksa field-nya dengan PHP murni
php -r 'var_dump(json_validate(file_get_contents("composer.json")));'Berkas composer.json yang dipakai seluruh sesi di atas tersedia di bagian bawah halaman. Sebagai rujukan resmi: schema lengkap composer.json
untuk setiap field, artikel versi Composer
untuk operator caret, serta dokumentasi json_validate()
yang kita pakai memeriksa berkasnya. Dengan kerangka berdiri, kita lanjut ke Bagian 2 untuk mengisi src/ dengan kelas pertama dan membuat autoloading itu benar-benar bekerja.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
composer.json
Unduh berkas{
"name": "budi/slugify",
"description": "Generate URL-friendly slugs from any title: configurable separator, lowercase mode, and length limit.",
"type": "library",
"license": "MIT",
"keywords": ["slug", "url", "seo"],
"authors": [
{
"name": "Budi",
"email": "[email protected]"
}
],
"require": {
"php": "^8.1"
},
"autoload": {
"psr-4": {
"App\\Util\\": "src/"
}
}
}