Lingkungan PHP di dunia nyata jarang seragam. Server produksi sebuah perusahaan bisa masih PHP 8.1, shared hosting pelanggan tertinggal di 8.2, sementara mesin pengembang Anda menjalankan 8.4 — dan pustaka yang Anda tulis harus tetap benar di ketiganya. Chapter ini membahas perangkat yang dimiliki PHP untuk mengelola keragaman tersebut: pemeriksaan versi runtime, pola polyfill, deteksi fitur, matriks uji di CI, serta persyaratan platform di Composer. Tujuan akhirnya satu: kode gagal lebih dini dan lebih jelas ketika lingkungan tidak memenuhi syarat, alih-alih salah perilaku secara diam-diam.

Memeriksa Versi Runtime dengan PHP_VERSION_ID #

PHP menyediakan konstanta PHP_VERSION_ID berupa bilangan bulat yang mengodekan versi: mayor dikali 10.000, ditambah minor dikali 100, ditambah nomor rilis. PHP 8.4.23 misalnya menjadi 80423. Angka tunggal ini jauh lebih mudah dibandingkan daripada mengurai string PHP_VERSION, dan sangat cocok untuk gerbang fitur sederhana.

php
<?php
printf("PHP_VERSION_ID: %d\n", PHP_VERSION_ID);

if (PHP_VERSION_ID >= 80100) {
    echo "Enums, readonly property, dan never tersedia.\n";
}

if (PHP_VERSION_ID < 80300) {
    echo "json_validate() belum ada — siapkan polyfill.\n";
} else {
    echo "json_validate() tersedia.\n";
}

echo "Versi: " . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "." . PHP_RELEASE_VERSION . "\n";

Output yang Dihasilkan:

text
PHP_VERSION_ID: 80423
Enums, readonly property, dan never tersedia.
json_validate() tersedia.
Versi: 8.4.23

Mari kita bedah baris-barisnya:

  1. Baris 2 — PHP_VERSION_ID dicetak apa adanya: 80423 untuk PHP 8.4.23.
  2. Baris 4 — ambang 80100 mewakili PHP 8.1.0; karena 80423 lebih besar, blok fitur 8.1 dianggap aman dipakai.
  3. Baris 8 — 80300 mewakili PHP 8.3.0; pada runtime 8.4 kondisi bernilai salah sehingga cabang else yang berjalan. Pada PHP 8.2, cabang if-lah yang akan memberi peringatan dini.
  4. Baris 14 — konstanta PHP_MAJOR_VERSION, PHP_MINOR_VERSION, dan PHP_RELEASE_VERSION menyediakan potongan versi secara terpisah untuk kebutuhan tampilan atau log.

Membandingkan Versi dengan version_compare #

PHP_VERSION_ID memadai untuk ambang sederhana, tetapi kebutuhan seperti rentang versi atau penanganan akhiran rc, beta, dan dev lebih pas ditangani version_compare(). Fungsi ini memahami semantik nomor versi, bukan sekadar urutan alfabetis.

php
<?php
var_dump(version_compare(PHP_VERSION, "8.1.0", ">="));
var_dump(version_compare("8.4.23", "8.5", "<"));
var_dump(version_compare("8.10", "8.9", ">"));
var_dump("8.10" > "8.9");

Output yang Dihasilkan:

text
bool(true)
bool(true)
bool(true)
bool(false)

Empat pembandingan tersebut memperlihatkan cara kerjanya:

  1. Baris 2 — runtime 8.4.23 dibandingkan dengan ambang 8.1.0; hasil true karena setiap segmen dibaca sebagai angka.
  2. Baris 3 — "8.4.23" dianggap lebih rendah dari "8.5"; versi 8.5 memang lebih tinggi dari 8.4 sekalipun teksnya lebih pendek.
  3. Baris 4 — version_compare("8.10", "8.9", ">") menghasilkan true karena segmen 10 lebih besar dari 9.
  4. Baris 5 — perbandingan string biasa "8.10" > "8.9" menghasilkan false karena karakter 1 lebih kecil daripada 9 pada urutan byte — inilah jebakan yang harus dihindari.

Pola Polyfill dengan function_exists #

Polyfill adalah implementasi buatan sendiri (userland) untuk fungsi yang belum tersedia di versi PHP lama, dipasang hanya bila diperlukan. Polanya satu baris: bungkus definisi fungsi dengan function_exists() sehingga fungsi native tetap menang begitu runtime yang lebih baru menyediakannya. Pendekatan ini dipopulerkan paket seperti symfony/polyfill.

php
<?php
// Polyfill untuk PHP < 8.0: hanya dipasang bila fungsi belum ada
if (!function_exists("str_contains")) {
    function str_contains(string $haystack, string $needle): bool
    {
        return $needle === "" || strpos($haystack, $needle) !== false;
    }
}

var_dump(str_contains("Belajar PHP modern", "PHP"));

Output yang Dihasilkan:

text
bool(true)

Pada PHP 8.4, fungsi native str_contains sudah ada sehingga guard bernilai salah dan definisi fallback diabaikan — versi native yang melayani panggilan. Pada PHP 7.4, fungsi tersebut belum ada; guard memasang implementasi berbasis strpos sehingga kode pemanggil tetap berjalan tanpa perubahan.

Lalu bagaimana membuktikan cabang fallback benar-benar berfungsi tanpa PHP lama? Kita bisa meminjam perilaku resolusi nama dalam namespace: PHP selalu mencari fungsi di namespace saat ini lebih dulu sebelum jatuh ke fungsi global.

php
<?php
namespace Toko\Util;

// Pada PHP 8.4 fungsi global mb_trim sudah ada, tetapi di dalam
// namespace ini nama "Toko\Util\mb_trim" belum terdefinisi, sehingga
// guard polyfill mengambil cabang fallback — persis seperti di PHP 8.3.
if (!function_exists(__NAMESPACE__ . "\\mb_trim")) {
    function mb_trim(string $teks): string
    {
        return preg_replace('/^\s+|\s+$/u', '', $teks);
    }
}

var_dump(mb_trim("  Kopi Gayo  "));

Output yang Dihasilkan:

text
string(9) "Kopi Gayo"

Apa yang terjadi pada berkas tersebut:

  1. Guard memeriksa nama lengkap — function_exists(__NAMESPACE__ . "\\mb_trim") menanyakan Toko\Util\mb_trim, yang memang belum terdefinisi, walau fungsi global mb_trim telah tersedia sejak PHP 8.4.
  2. Fallback terpasang — definisi polyfill di dalam namespace dieksekusi, meniru perilaku fungsi global.
  3. Pemanggilan menembus versi lokal — mb_trim(...) tanpa awalan \ menemukan versi namespace lebih dulu, sehingga tubuh polyfill benar-benar berjalan dan hasilnya identik dengan versi native.

Deteksi Fitur Lebih Andal daripada Deteksi Versi #

Pemeriksaan nomor versi mengandung asumsi tersembunyi: fitur tertentu persis datang pada versi tertentu. Kenyataannya lebih berantakan — distribusi Linux kerap mem-backport perbaikan, build kustom bisa kehilangan ekstensi, dan begitu polyfill dimuat, fungsi tersedia terlepas dari versi. Deteksi fitur mengajukan pertanyaan yang sebenarnya: apakah yang saya butuhkan tersedia di lingkungan ini?

php
<?php
// Deteksi fitur, bukan deteksi versi
$fitur = [
    "json_validate (PHP 8.3)" => function_exists("json_validate"),
    "mb_trim (PHP 8.4)" => function_exists("mb_trim"),
    "ekstensi mbstring" => extension_loaded("mbstring"),
];

foreach ($fitur as $nama => $ada) {
    echo $nama . ": " . ($ada ? "ada" : "tidak ada") . "\n";
}

Output yang Dihasilkan:

text
json_validate (PHP 8.3): ada
mb_trim (PHP 8.4): ada
ekstensi mbstring: ada

Tiga pemeriksaan pada contoh tersebut menggunakan dua fungsi yang berbeda:

  1. function_exists() — bertanya langsung apakah sebuah fungsi bisa dipanggil; hasilnya otomatis benar baik fungsi berasal dari native maupun polyfill yang kebetulan sudah dimuat.
  2. extension_loaded() — memeriksa ketersediaan ekstensi; mb_trim bermakna hanya bila mbstring aktif, sehingga pemeriksaan ini melengkapinya.
  3. Pola gabungan — dictionary fitur memudahkan penambahan pemeriksaan baru dan pembacaan hasilnya dalam satu tampilan. Pemetaan fitur ke versi mana pun asalnya bisa Anda lacak di chapter Modern PHP Version Features .

Matriks Uji di CI #

Semua pemeriksaan di atas tidak berarti bila rangkaian uji Anda hanya pernah berjalan di satu versi. Integrasi berkelanjutan (CI) mengatasi hal ini dengan matriks: rangkaian uji yang sama diulang pada beberapa versi PHP, sehingga kode yang tidak sengaja memakai fitur 8.4 akan gagal pada job PHP 8.2 sebelum menyentuh produksi.

yaml
jobs:
  uji:
    strategy:
      matrix:
        php: ["8.1", "8.2", "8.3", "8.4"]
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
      - run: composer install
      - run: vendor/bin/phpunit

Konfigurasi di atas adalah pola GitHub Actions: kunci strategy.matrix.php menduplikasi seluruh job untuk setiap nilai versi, action setup-php memasang versi yang diminta, lalu PHPUnit berjalan identik di setiap salinan. Kegagalan ditandai per versi sehingga Anda langsung tahu batas bawah kompatibilitas yang sebenarnya. Praktik pengujian itu sendiri dibahas tuntas pada Section Testing dan Quality Assurance .

Persyaratan Platform di Composer #

Untuk pustaka yang dibagikan, cara paling awal menolak lingkungan yang tidak cocok adalah lewat composer.json: kunci require.php dan ext-* diperiksa Composer setiap kali dependensi dipasang atau diperbarui.

json
{
    "name": "toko/util",
    "require": {
        "php": ">=8.2",
        "ext-mbstring": "*"
    }
}

Deklarasi tersebut menyatakan dua hal: pustaka membutuhkan PHP 8.2 ke atas, dan ekstensi mbstring versi berapa pun. Perintah composer check-platform-reqs membandingkan lingkungan saat ini dengan seluruh persyaratan yang terpasang — berguna sebagai pemeriksaan terakhir sebelum deploy. Ketika tim Anda sengaja mengunci versi target agar berkas composer.lock deterministik, konfigurasi platform.php membuat solver dependensi berpura-pura berjalan pada versi tersebut.

bash
composer check-platform-reqs
composer config platform.php 8.2.99

Pengelolaan dependensi secara keseluruhan — versi semantik, constraint, dan lock file — dibahas tuntas pada Section Composer dan Dependency Management .

Merujuk Panduan Upgrade Resmi #

Setiap rilis mayor menerbitkan panduan migrasi resmi yang memuat tiga bagian penting: fitur baru, hal yang ditinggalkan (deprecated), dan perubahan yang merusak kompatibilitas mundur. Biasakan membaca bagian kedua dan ketiga sebelum upgrade — kebanyakan insiden upgrade berakar pada perubahan perilaku kecil yang tidak terbaca, bukan pada fitur baru. Status dukungan tiap versi (aktif, hanya keamanan, atau berakhir) dapat dipantau di halaman versi yang didukung pada situs php.net agar rencana upgrade Anda berpijak pada jadwal resmi.

Referensi #

Ringkasan #

PHP_VERSION_ID dan version_compare() menjadi gerbang versi di dalam kode; pola polyfill menjembatani fungsi yang belum lahir di runtime lama; deteksi fitur bertanya langsung pada lingkungan; matriks CI membuktikan klaim kompatibilitas; dan persyaratan platform di Composer menolak instalasi yang tidak memenuhi syarat sejak awal. Kombinasi kelimanya membuat dukungan multi-versi menjadi keputusan yang sadar dan teruji, bukan tebakan. Section berikutnya berbelok ke arah yang tampak berlawanan dari kompatibilitas yang lebar: memperketat kontrak kode melalui type system dan static analysis.