Bagian 4 menutup dengan suite 30 test yang hijau — tetapi test hanya membuktikan jalur yang benar-benar dijalankan. Library dipegang ke standar yang lebih tinggi daripada aplikasi: pengguna memanggil kode kita dari konteks yang tidak pernah kita bayangkan, lewat kombinasi opsi yang tidak pernah kita coba, di versi PHP yang tidak pernah kita sentuh. Pada Bagian 5 kita mengundang pemeriksa yang tidak menjalankan kode sama sekali: PHPStan di level maksimal, yang membaca seluruh jalur eksekusi sekaligus — termasuk jalur yang tidak pernah terjadi pada mesin kita. Hasil akhirnya satu berkas konfigurasi kecil, phpstan.neon, yang mengunci janji kualitas package selamanya.
Mengapa Level Maksimal untuk Library #
PHPStan punya sepuluh level yang menumpuk dari paling longgar ke paling ketat: level rendah menangkap kesalahan klasik, level tinggi meminta bukti untuk setiap nilai yang mengalir. Untuk aplikasi, naik bertahap dari level 5 adalah strategi yang wajar — strategi beserta konsep baseline untuk kode warisan sudah kita bahas di chapter Baseline dan Analysis Levels
. Library tidak punya kemewahan itu. Setiap tanda tangan publik adalah kontrak yang dibaca IDE pengguna; setiap lubang null, false, atau mixed yang lolos akan menjadi TypeError di kode orang lain, bukan di kode kita.
Peta kasar pita levelnya sebagai berikut:
| Pita level | Contoh yang ditangkap |
|---|---|
| 0–3 | kelas, fungsi, atau method tidak dikenal; jumlah argumen salah |
| 4–5 | tipe return dan tipe argumen yang tidak cocok dengan deklarasi |
| 6 | typehint hilang, misalnya array tanpa tipe nilai |
| 7–8 | nilai null/false menyusup ke parameter non-nullable |
| 9–10 | nilai mixed dipakai tanpa diperiksa; kondisi yang selalu benar |
Aplikasi bisa berkompromi dengan level 5 karena pemanggilnya kita sendiri. Library dipakai pihak lain, jadi kita pilih level max sejak awal — lebih mudah menjaga kode tetap bersih dari hari pertama daripada menaikkan level sambil menambal puluhan temuan lama.
Sebelum memasang alat apa pun, mari audit dulu dengan yang sudah ada di tangan: Reflection. Kode berikut memeriksa tanda tangan seluruh method publik SlugGenerator dari bundle Bagian 3:
<?php
require __DIR__ . '/../bagian-3-public-api-dan-validation/src/SlugGenerator.php';
require __DIR__ . '/../bagian-3-public-api-dan-validation/src/Exception/InvalidInputException.php';
use App\Util\SlugGenerator;
foreach (['generate', 'withSeparator', 'withMaxLength', 'underscore'] as $nama) {
$refleksi = new ReflectionMethod(SlugGenerator::class, $nama);
$param = [];
foreach ($refleksi->getParameters() as $p) {
$param[] = $p->getName() . ': ' . $p->getType();
}
printf(
"%-14s (%s): %s\n",
$nama,
implode(', ', $param),
$refleksi->getReturnType()
);
}Output yang Dihasilkan:
generate (text: string): string
withSeparator (separator: string): self
withMaxLength (maxLength: ?int): self
underscore (): selfEmpat method publik, empat tanda tangan bertipe penuh — parameter maupun return tidak ada yang kosong. Ini bukan kebetulan: tipe sudah menjadi keputusan rancangan sejak Bagian 3, sebelum PHPStan masuk. Audit manual seperti ini memuaskan, tetapi ia tidak melihat ke dalam method — dan di sanalah letak pekerjaan static analysis.
Sesi Nyata: Memasang dan Menjalankan PHPStan #
Sesi berjalan pada rakitan /tmp/slugify-sa yang sudah dikenal dari Bagian 4: composer.json kerangka Bagian 1 plus src/ dari Bagian 3. PHPStan adalah alat pengembang, jadi tempatnya require-dev — tidak pernah ikut terpasang di mesin pengguna library:
$ composer require --dev phpstan/phpstanComposer could not detect the root package (budi/slugify) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version
./composer.json has been updated
Composer could not detect the root package (budi/slugify) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version
Running composer update phpstan/phpstan
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
- Locking phpstan/phpstan (2.3.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing phpstan/phpstan (2.3.1): Extracting archive
Generating autoload files
1 package you are using is looking for funding.
Use the `composer fund` command to find out more!
No security vulnerability advisories found.
Using version ^2.3 for phpstan/phpstanPHPStan 2.3.1 terpasang sebagai satu-satunya package baru — peringatan versi root muncul dua kali karena mode --working-dir memuat ulang manifest setelah mengubahnya, dan akan lenyap begitu kita menandai git tag di Bagian 6. Sekarang momen kebenaran — analisis pertama, langsung di level maksimal:
$ php vendor/bin/phpstan analyse src -l max --no-progress
[OK] No errors
exit code: 0Nol error, pada percobaan pertama, di level tertinggi. Tidak ada satu baris kode pun yang perlu diketatkan — disiplin mengetik di Bagian 3 terbayar penuh. Yang diperiksa bukan sekadar tanda tangan: PHPStan membuktikan properti $separator selalu terinisialisasi sebelum dibaca, bahwa hasil iconv() yang bisa false sudah disaring is_string(), bahwa preg_replace() yang bisa null sudah ditangkap ?? '', dan bahwa strrpos() yang bisa false tidak pernah mengalir ke substr() tanpa pemeriksaan.
Konfigurasi phpstan.neon #
Mengetik -l max setiap kali menjalankan analisis itu rapuh — lupa satu huruf l, dan analisis turun ke level 0 tanpa peringatan. Konfigurasinya kita kunci ke berkas phpstan.neon di akar package:
parameters:
level: max
paths:
- src
phpVersion: 80100Tiga baris parameter ini memuat tiga keputusan. level: max mengunci ketatness — sekarang vendor/bin/phpstan analyse tanpa argumen apa pun berjalan di level tertinggi. paths menentukan direktori yang dianalisis; di proyek nyata kita tambahkan tests setelah PHPUnit terpasang. Terakhir, phpVersion: 80100 memerintahkan PHPStan menilai kode seolah berjalan di PHP 8.1 — versi minimum janji "php": "^8.1" kita. Baris ini penting karena PHPStan versi 2.x sendiri butuh PHP 8.1+, padahal versi runtime pengembang bisa lebih baru; tanpa baris ini, analisis mengikuti versi PHP yang menjalankannya. Bukti bahwa baris itu bekerja:
<?php
declare(strict_types=1);
namespace App\Util;
final class CekVersi
{
/**
* @param array<int, int> $data
*/
public static function pertama(array $data): ?int
{
return array_find($data, static fn (int $n): bool => $n > 0);
}
}Fungsi array_find() baru hadir di PHP 8.4. Analisis berkas kecil ini dengan phpVersion: 80100 dan phpVersion: 80400 menghasilkan dua nasib yang berbeda:
$ php ../slugify-sa/vendor/bin/phpstan analyse src -l 9 --no-progress -c neon-81.neon
------ ----------------------------------------------------------------------
Line CekVersi.php
------ ----------------------------------------------------------------------
14 Function array_find not found.
🪪 function.notFound
💡 Learn more at https://phpstan.org/user-guide/discovering-symbols
------ ----------------------------------------------------------------------
[ERROR] Found 1 error
exit code: 1$ php ../slugify-sa/vendor/bin/phpstan analyse src -l 9 --no-progress -c neon-84.neon
[OK] No errors
exit code: 0Di bawah kacamata 8.1, array_find tidak dikenal — tepat seperti yang akan dialami pengguna library kita yang masih memakai PHP 8.1. Menjalankan analisis pada versi termuda yang kita janjikan adalah satu-satunya cara memaksa diri jujur terhadap batas require.
Satu penyesuaian tata letak perlu didokumentasikan. Berkas phpstan.neon yang tersedia di bawah halaman ini memakai paths: ../bagian-3-public-api-dan-validation/src — pada bundle materi, kode library hidup di bundle kembar Bagian 3, jadi konfigurasi menunjuk ke sana agar tetap jujur terhadap berkas yang benar-benar dianalisis. Di checkout proyek nyata seperti sesi /tmp kita, nilainya src (dan tests) persis seperti blok di atas.
Setelah konfigurasi berdiri, kita lengkapi sesinya: salin tests/ dan phpunit.xml.dist dari bundle Bagian 4, pasang phpunit/phpunit:^12.5 plus ekstensi phpstan/phpstan-phpunit (ekstensi ini yang mengajari PHPStan memeriksa keterkaitan data provider dengan tanda tangan test-nya), lalu tambahkan tests ke paths dan dua baris includes untuk ekstensi tersebut. Hasil akhirnya:
$ php vendor/bin/phpstan analyse --no-progressNote: Using configuration file /tmp/slugify-sa/phpstan.neon.
[OK] No errors
exit code: 0Baris Note mengonfirmasi konfigurasi terbaca, dan level max kini mencakup src maupun tests — 30 test dari Bagian 4 ikut diverifikasi tipenya, tetap tanpa satu pun temuan. Untuk memastikan tidak ada yang bergeser, suite dijalankan sekali lagi lewat pemasangan vendor yang baru ini: OK (30 tests, 45 assertions) — angka yang sama persis dengan sesi Bagian 4.
Melihat yang Ditangkap: Regresi yang Disengaja #
Nol error itu jujur, tetapi tidak mendidik — kita belum melihat PHPStan menolak apa pun. Maka kita suntikkan regresi: salinan SlugGenerator yang sengaja dikerjakan secara cerobek, disimpan sebagai SlugGeneratorBuruk.php di /tmp/slugify-demo/src/. Berkas ini lolos php -l dan, pada input yang “normal”, hasilnya pun tampak benar — persis jenis kode yang mudah lolos review:
<?php
declare(strict_types=1);
namespace App\Util;
final class SlugGeneratorBuruk
{
private const ACCENT_MARKS = [
'`' => '',
"'" => '',
'^' => '',
'"' => '',
'~' => '',
];
private string $separator;
private ?int $maxLength;
private bool $lowercase;
public function __construct(
string $separator = '-',
?int $maxLength = null,
bool $lowercase = true,
) {
$this->seperator = $separator;
$this->maxLength = $maxLength;
$this->lowercase = $lowercase;
}
public function generate($text)
{
$slug = trim($text);
if ($slug === '') {
return '';
}
$slug = $this->lowercase
? mb_strtolower($slug, 'UTF-8')
: $slug;
$slug = strtr(
@iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $slug),
self::ACCENT_MARKS
);
$slug = preg_replace('/[^A-Za-z0-9]+/', $this->separator, $slug);
$slug = trim($slug, $this->separator);
if ($this->maxLength !== null && strlen($slug) > $this->MaxLength) {
$slug = $this->truncate($slug, $this->maxLength);
}
return $slug;
}
private function truncate(string $slug, int $maxLength): string
{
$slug = substr($slug, 0, $maxLength);
$posisi = strrpos($slug, $this->separator);
if ($posisi > 0) {
$slug = substr($slug, 0, $posisi);
}
return $slug;
}
}Enam kesalahan berbeda terpendam di dalamnya: salah ketik $this->seperator di constructor, salah ketik $this->MaxLength di baris pemeriksaan panjang, parameter $text tanpa tipe, method generate() tanpa return type, hasil iconv() yang bisa false langsung diberikan ke strtr(), dan hasil preg_replace() yang bisa null langsung diberikan ke trim(). Level 5 — standar nyaman banyak proyek — hanya menangkap tiga:
$ php ../slugify-sa/vendor/bin/phpstan analyse src -l 5 --no-progress ------ -----------------------------------------------------------------------
Line SlugGeneratorBuruk.php
------ -----------------------------------------------------------------------
17 Property App\Util\SlugGeneratorBuruk::$separator is never written,
only read.
🪪 property.onlyRead
💡 See:
https://phpstan.org/developing-extensions/always-read-written-propert
ies
28 Access to an undefined property
App\Util\SlugGeneratorBuruk::$seperator.
🪪 property.notFound
💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-und
efined-property
54 Access to an undefined property
App\Util\SlugGeneratorBuruk::$MaxLength.
🪪 property.notFound
💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-und
efined-property
------ -----------------------------------------------------------------------
[ERROR] Found 3 errors
exit code: 1Naik ke level max, jumlahnya membengkak menjadi delapan — lima temuan baru semuanya soal tipe:
$ php ../slugify-sa/vendor/bin/phpstan analyse src -l max --no-progress ------ -----------------------------------------------------------------------
Line SlugGeneratorBuruk.php
------ -----------------------------------------------------------------------
17 Property App\Util\SlugGeneratorBuruk::$separator is never written,
only read.
🪪 property.onlyRead
💡 See:
https://phpstan.org/developing-extensions/always-read-written-propert
ies
28 Access to an undefined property
App\Util\SlugGeneratorBuruk::$seperator.
🪪 property.notFound
💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-und
efined-property
33 Method App\Util\SlugGeneratorBuruk::generate() has no return type
specified.
🪪 missingType.return
33 Method App\Util\SlugGeneratorBuruk::generate() has parameter $text
with no type specified.
🪪 missingType.parameter
35 Parameter #1 $string of function trim expects string, mixed given.
🪪 argument.type
46 Parameter #1 $str of function strtr expects string, string|false
given.
🪪 argument.type
52 Parameter #1 $string of function trim expects string, string|null
given.
🪪 argument.type
54 Access to an undefined property
App\Util\SlugGeneratorBuruk::$MaxLength.
🪪 property.notFound
💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-und
efined-property
------ -----------------------------------------------------------------------
[ERROR] Found 8 errors
exit code: 1Bandingkan dua laporan itu per pita: level 5 melihat properti (baris 17, 28, 54), level max juga melihat tanda tangan (baris 33), nilai mixed yang mengalir (baris 35), dan false/null yang menyusup (baris 46 dan 52). Setiap temuan membawa pengenal seperti argument.type — pengenal ini bisa dicari langsung di dokumentasi PHPStan, menjadikan pesan error menjadi pintu belajar, bukan sekadar gangguan. Perhatikan juga bahwa suite test Bagian 4 bisa saja tetap hijau untuk kelas ini — test menguji perilaku yang terjadi, sedangkan string|false dari iconv() tidak pernah terjadi pada build mesin buku (byte tidak valid justru dibuang diam-diam). PHPStan membacanya dari tanda tangan fungsi, bukan dari keberuntungan lingkungan.
Memperbaiki Temuan #
Perbaikan untuk kedelapan temuan itu jatuh ke lima lokasi, dan setiap lokasi punya padanan persis di kode asli Bagian 3 — itulah bukti terkuat bahwa disiplin mengetik bukan formalitas:
| Temuan | Perbaikan | Padanan di src/SlugGenerator.php Bagian 3 |
|---|---|---|
$seperator dan $MaxLength tidak dikenal | perbaiki salah ketik | penamaan konsisten $separator, $maxLength |
generate() tanpa tipe parameter/return | generate(string $text): string | tanda tangan bertipe penuh |
trim() menerima mixed | mengikuti tipe parameter | sama — hilang otomatis setelah parameter bertipe |
strtr() menerima string|false | simpan hasil iconv(), saring dengan is_string() | blok if (is_string($ascii)) pada langkah 2 generate() |
trim() menerima string|null | tambahkan ?? '' setelah preg_replace() | langkah 4 generate() |
Dua penjaga terakhir layak dilihat utuh karena keduanya adalah jantung pembelajaran bagian ini. Fungsi berikut memuat pipeline yang sama dalam bentuk murni — persis seperti alur di dalam generate() setelah perbaikan:
<?php
// Pipeline slug versi terkunci tipe — bentuk fungsi murni supaya bisa
// dijalankan sendiri; di kelas asli blok ini hidup di dalam generate().
function pipelineSlug(string $text, string $separator): string
{
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
if (!is_string($ascii)) {
return '';
}
$ascii = strtr($ascii, ['`' => '', "'" => '', '^' => '', '"' => '', '~' => '']);
$slug = preg_replace('/[^A-Za-z0-9]+/', $separator, $ascii) ?? '';
return trim($slug, $separator);
}
echo pipelineSlug(mb_strtolower('Résumé Terbaru 2026', 'UTF-8'), '-'), "\n";
echo pipelineSlug('Framework Laravel Untuk Pemula', '_'), "\n";Output yang Dihasilkan:
resume-terbaru-2026
Framework_Laravel_Untuk_PemulaSetelah kelima lokasi diperbaiki pada berkas SlugGeneratorBuruk.php, analisis diulang dan hasilnya menutup lengkungannya:
$ php ../slugify-sa/vendor/bin/phpstan analyse src -l max --no-progress
[OK] No errors
exit code: 0Delapan error menjadi nol, dan perilaku runtime pun tetap utuh — php -r pada kelas yang telah diperbaiki menghasilkan framework untuk judul panjang dengan maxLength: 15 dan resume-terbaru untuk judul beraksen. Type system tidak mengubah perilaku; type system hanya membuat perbedaan antara “berjalan di mesin saya” dan “terbukti benar oleh tanda tangan” hilang.
Integrasi ke CI #
Semua pemeriksaan ini kehilangan nilainya bila hanya dijalankan saat ingat. Gerbang terakhir adalah menjadikannya otomatis pada setiap push — dan di sinilah exit code yang kita lihat tadi bekerja: PHPStan keluar dengan 1 saat ada temuan, PHPUnit keluar dengan 1 saat ada kegagalan, sehingga pipeline cukup memeriksa exit code. Konfigurasi GitHub Actions berikut memasang dependensi lalu menjalankan kedua pemeriksaan berurutan:
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- run: composer install --prefer-dist
- run: vendor/bin/phpstan analyse
- run: vendor/bin/phpunitEnam langkahnya membaca lurus: checkout kode, siapkan PHP (kita pilih 8.1 — versi minimum yang kita janjikan, selaras dengan phpVersion di phpstan.neon), pasang dependensi, lalu dua gerbang kualitas. Bila salah satu langkah gagal, pipeline berhenti dan pull request ditandai merah sebelum sempat menyentuh main. Dua perintah terakhir itu akan kita pangkas menjadi composer phpstan dan composer test pada Bagian 6, saat composer scripts mengambil alih pengetikan jalurnya. Pembahasan mendalam tentang merangkai pipeline — termasuk matriks versi PHP — ada di chapter Continuous Integration
.
Apa yang Berubah #
Dibandingkan Bagian 4 yang mengunci perilaku lewat test:
phpstan.neonbaru — levelmax, pathssrc(dantestsdi checkout nyata),phpVersion: 80100; di bundle halaman ini paths menunjuk bundle kembar Bagian 3.- Hasil analisis nyata: 0 error di level max untuk
src/maupuntests/, tanpa satu baris kode yang perlu diubah — disiplin tipe Bagian 3 terbukti sejak rancangan. - Bukti kerja PHPStan lewat regresi disengaja: 3 error di level 5, 8 error di level max, seluruhnya dipetakan ke penjaga yang sudah ada di kode asli, lalu 0 error setelah perbaikan.
- Suite PHPUnit dijalankan ulang pada pemasangan vendor yang baru: tetap
OK (30 tests, 45 assertions)— kode tidak berubah, keyakinan bertambah.
Library sekarang dijaga dua lapis: test membekukan perilaku, static analysis membekukan tipe. Yang tersisa untuk menjadi package sungguhan hanyalah urusan pengiriman: manifest final, nomor versi, dan pintu masuk ke katalog dunia — semuanya di Bagian 6.
Cara Menjalankan #
Dari direktori bagian-5-static-analysis-dan-code-quality/ pada bundle halaman ini:
# 1. Rakit sesi nyata di /tmp (vendor/ tidak mengotori materi)
mkdir -p /tmp/slugify-sa
cp ../bagian-1-struktur-package/composer.json /tmp/slugify-sa/
cp -r ../bagian-3-public-api-dan-validation/src /tmp/slugify-sa/
cd /tmp/slugify-sa
composer require --dev phpstan/phpstan
# 2. Analisis pertama — langsung level max
php vendor/bin/phpstan analyse src -l max --no-progress
# 3. Tulis phpstan.neon (lihat blok konfigurasi di atas), lalu:
php vendor/bin/phpstan analyse --no-progress
# 4. Demo regresi disengaja
mkdir -p /tmp/slugify-demo/src
# ... salin SlugGeneratorBuruk.php dari halaman ini, lalu:
php /tmp/slugify-sa/vendor/bin/phpstan analyse /tmp/slugify-demo/src -l max --no-progressBerkas phpstan.neon yang dipakai seluruh sesi tersedia di bagian bawah halaman. Sebagai rujukan resmi: mulai cepat PHPStan
untuk perintah dasarnya, referensi konfigurasi PHPStan
untuk setiap kunci phpstan.neon termasuk phpVersion, serta dokumentasi type declarations
untuk dasar tanda tangan yang kita pertahankan sepanjang proyek. Dengan gerbang kualitas berdiri, kita menuju garis akhir: Bagian 6 merampungkan manifest, menstempel versi lewat git tag, dan mengirim budi/slugify ke Packagist.