Notes from PHP 8.0

Sebenarnya aku cuman mau nulis tentang PHP 8.0, terus aku iseng-iseng nonton video pembahasan tentang PHP 8.0 versi bahasa Indonesia di Youtube. Nggak ada yang salah sih dengan videonya, cuman top komennya membuat aku ingin berkata kasar. Komen paling atasnya gini: “Selama ada Indonesia, maka PHP masih ada”. Gak perlu aku jabarkan lah maksudnya, yang jelas aku sangat nggak setuju. Tapi banyak yang ngelike komen itu.

Kita boleh berbeda sudut pandang tentang cara bagaimana sebuah code ditulis, cuman bahasa pemrograman (ataupun framework) kalau menurutku sih gak ada yang lebih baik dan gak ada yang lebih buruk. Semuanya punya tipe project masing-masing, tipe problem masing-masing, ciri masing-masing. Contohnya nih:

Mencintai OOP, class with inheritance, abstract classes, beautiful try-catch?
Mau tipe project yang waktu pengembangannya cepat, scopenya gak terlalu besar, monolitik?
Butuh pake wordpress nih.

Jawabannya PHP, PHP improved a lot.

Lebih suka bemain-main dengan structs daripada dengan classes?
Suka yang strict-strict? Mau code yang nggak banyak jalan menuju roma? Gampang dibaca dan dimengerti?
Projectnya concurrency banget? Microservices banget? gRPC banget?

Ini ciri-cirinya Go.

Cinta dengan dynamic type language?
Gak bisa lepas dengan syntax for dan if dalam satu baris?
cinta = [x if x == “kita” else “mereka” for x in masyarakat]
Projectnya data scientist banget yang banyak main-main dengan matrix dan vektor?

json.loads() dan list comprehension di Python memang membuat hidup semakin indah.

Lihat, mana ada yang superior dari yang lainnya.
Setiap bahasa pemrograman punya gayanya masing-masing. Semuanya memberikanku pengalaman baru dan memberikan pelajaran baru (kenapa jadi kayak ngomongin mantan).

Jadi, apakah PHP mati?
Ya enggak lah, kan masih ada Indonesia. Hahaha.
Ya enggak lah, ini ada update-an PHP versi 8.

JIT Compiler

Ini yang highlight di PHP 8.0, yaitu kehadiran JIT dengan 2 tipe compilation engine, Function-based dan Tracing-based. JIT kepanjangan dari “Just In Time”, artinya kode PHP kita bisa langsung dieksekusi sama CPU. Proses parsing ke Opcodes, compile ke Machine Codes bisa langsung dilewatkan. Kenapa bisa? Soalnya JIT ini ngecache hasil compile yang udah berupa Machine Codes, jadi ketika kode kita sudah pernah dicompile, bisa langsung dieksekusi.

PHP adalah salah satu Interpreted Languages di mana dalam menerjemahkan kode kita ke bahasa yang dimengerti oleh mesin, digunakan sebuah interpreter. Kalau pernah dengar Zend Engine, nah itu. Temannya Interpreted Language ada Compiled Language, di mana kode kita langsung dicompile ke bahasa mesin, contohnya kayak bahasa C, Go, Haskell, Rust. Kalau geng Interpreted Languages ada Java, Javascript, Python.

Sebenarnya JIT ini sudah digunakan lama oleh Java dengan JVM dan Javascript dengan V8nya. Keberadaan JIT ini bikin definisi tentang Interpreted dan Compiled Language jadi kabur. Interpreter yang menggunakan JIT punya kemampuan bisa langsung mengeksekusi kode layaknya Compiled Language, tapi juga sekaligus menerapkan prinsip Interpreted Language. Suka gitu emang, sama kayak kehadiran KVM yang membuat definisi hypervisor type 1 dan 2 jadi blur. Bahasa kerennya Hybrid. Tapi gak papa, kan semua demi performa.

Supaya lebih jelas apa yang dilakukan oleh PHP (tanpa JIT) di belakang, aku sudah bikin gambarnya:

PHP With Cache and Without Cache

Yang atas adalah PHP tanpa mengaktifkan cache, yang bawah menggunakan cache yaitu OPCache. Proses Parse to Opcodes itu adalah [kotak warna kuning] proses tokenisasi yang menghasilkan tokens, kemudian parsing yang menghasilkan Abstract Syntax Tree (AST) dan compiling yang menghasilkan Opcodes atau biasa disebut Byte Codes. Opcodes ini yang bisa dibaca oleh VM Executor untuk diterjemahkan ke Machine Codes. Makanya disebut Interpreted Language, karena kode diterjemahkan dulu ke Opcodes oleh interpreter, baru ke Machine Codes. Sebagai tambahan, untuk AST baru ditambahkan mulai PHP 7.0, sebelumnya proses Parser itu gabung ke Compiler.

Ketika menggunakan OPCache (gambar bawah), proses Parse to Opcodes bisa dilewatkan kalau code pernah diparsing sebelumnya. Di kotak orange juga aku tambahin kondisi kalau kita menggunakan PHP 7.4 Preload.

Sekarang lanjut ke JIT. Penjelasan bagus dan cukup detail yang bisa aku dapatkan tentang PHP JIT ada di sini. Aku juga pake gambar dari website itu.

PHP 8.0 with JIT

Kalau OPCache nyimpan Opcodes, JIT nyimpan Machine Codes. Seperti yang terlihat pada gambar di atas, JIT adalah bagian dari OPCache. Jadi kalau kita mau mengaktifkan JIT, maka kita harus mengaktifkan OPCache.

There's a so called “monitor” that will look at the code as it's running. When this monitor detects parts of your code that are re-executed, it will mark those parts as “warm” or “hot”, depending on the frequency. These hot parts can be compiled as optimised machine code, and used on the fly instead of the real code.
―sticher.io

Di dalam JIT ada sebuah functionality untuk mendeteksi kode kita sebagai “warm” atau “hot” untuk di cache sama JIT. Untuk thresholdnya bisa kita atur sendiri di opcache.jit_hot_loop, opcache.jit_hot_func, opcache.jit_hot_return, dan opcache.jit_hot_side_exit.

Performa dari JIT official bisa dilihat di sini. Kalau dilihat untuk aplikasi-aplikasi kayak Wordpress atau Symphony, performa JIT gak terlalu kelihatan. Sedangkan pada sintaktik benchmark, performanya bisa meningkat bahkan 3 kali lipat. Untuk Function vs Tracing lebih perform yang Tracing.

Karena aku penasaran pengen cobain sendiri, aku siapin code buat benchmarking dan menjalankannya di local machine ku. Begini kira-kira hasilnya:

PHP 8.0 Benchmarking

Aku sediain beberapa skenario pengujian dengan gambar atas JIT Enable diset false, sedangkan yang bawah true. Yang kiri menggunakan Function-based, dan yang kanan menggunakan Tracing-based. JIT terlihat bekerja dengan baik di sini, semua skenario membutuhkan waktu proses yang lebih sedikit ketika menggunakan JIT. Improvements yang signifikan terlihat pada loops, fibonacci, dan if else. Kalau kita merujuk ke kodenya, ketiga skenario ini adalah proses yang berulang. Maksudnya melakukan hal yang sama itu lagi itu lagi. Sedangkan untuk math dan string manipulation yang dilakuin adalah fungsi yang beda-beda. Karena kita sudah mengetahui cara kerja JIT yang mendeteksi bagian “warm", “hot", dan sudah melihat juga performa JIT official, jadi menurutku ini jelas. Untuk compilation enginenya, tidak terlalu terlihat perbedaan yang signifikan.

Null Safe Operator

// PHP 7
$country = null;
if ($session !== null) {
 $user = $session->user;
 if ($user !== null) {
   $address = $user->getAddress();
    if ($address !== null) {
       $country = $address->country;
     }
   }
}

// PHP 8
$country = $session?->user?->getAddress()?->country;

Dengan null safe operator, kita bisa punya pengecekan yang panjang bertingkat-tingkat hanya dengan satu baris. Pas aku cobain, ini kalau kita mau debugging, agak susah untuk tau bagian mana yang null. Pokoknya kalau salah satu null, dia langsung ngembalikan aja nilai null secara safe. Nah bagusnya sih dibatasin, sampai dua tingkat aja, jangan lebih kalau gak mau ribet debugging. Jadi ibaratnya untuk menggantikan ini:

$address = $user->getAddress();
$country = $address ? $address->country : null;

jadi ini:

$country = $user?->getAddress()?->country;

Constructor Property Promotion

Ini perlu nih diimplementasikan untuk menghemat waktu dan tenaga.

// php 7
class Dimension {
    public float $x;
    public float $y;
    public ?float $z;
 
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        ?float $z = 0.0,
    )
    {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

// php 8
class Dimension {
   public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public ?float $z = 0.0,
    )
    {}
}

Named Arguments

Emang sih, PHP sendiri aja suka nggak konsisten bikin urutan argumen. Datanglah named arguments di PHP 8.0. Kita coba pake class Dimension kece kita di atas.

class Dimension {
   public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public ?float $z = 0.0,
    )
    {}
}

$square = new Dimension(y:10, x:12);

Pertama, kita gak perlu argumennya sesuai urutan, kedua kita bisa skip optional paramater. Ketiga, (ini kabar terbaiknya) kalau kita punya argumen yang punya default value, kita gak perlu lagi tarohnya di paling belakang supaya bisa diskip.

Terus kita bisa kombinasiin juga pake array spread operator yang sudah diperkenalkan lebih dulu di PHP 7.4:

$cubeDim = [
    'x' => 10, 
    'y' => 20, 
    'z' => 30,
];

$cube = new Dimension(...$cubeDim);

Oh iya contoh di atas itu juga ada fitur baru PHP 8.0 lainnya yaitu Trailing Comma in Parameter Lists. Keberadaan koma di akhir public ?float $z = null, dan 'z' => 30, gak akan bikin code kita error.
Oke. Bungkus.

Union Types

Sudah semenjak versi 7, PHP menunjukkan keseriusannya terhadap strict typing. Nah sekarang ada lagi nih, union types:

public function foo(Foo|Bar $input): int|float;
public function bar(?Bar $bar): void;

Untuk catatan, baris kedua itu sama kayak:

public function bar(Bar|null $bar): void;

Lumayan lah, di PHP aku suka bikin fungsi dengan satu argumen yang bakal ngembalikan nilai aslinya atau false kalau error.

Mixed Types

Nah ini menurutku kelanjutan dari si union types di atas. Kan kentang banget kalau kita nggak tau datanya bakal tipe apa, terus kita tulis semua tipe data misal kayak gini: array|bool|int|float|null|object|string.
Hadir lah mixed types.

private mixed $rawdata;

Yang pasti batasin pake type ini, haram kalau keseringan. Kayak interfaces{} di Go.

Match Expression

switch ($statusCode) {
    case 200:
    case 300:
        $message = null;
        break;
    case 400:
        $message = 'not found';
        break;
    case 500:
        $message = 'server error';
        break;
    default:
        $message = 'unknown status code';
        break;
}

menjadi:

$message = match ($statusCode) {
    200, 300 => null,
    400 => 'not found',
    500 => 'server error',
    default => 'unknown status code',
};

Jadi sekarang ada cara yang lebih bagus dalam menuliskan switch case. Kabar baiknya, match ini bisa mengkombinasi kondisi, gak perlu explicit break, dan setara dengan ===. Tapi kalau kita butuh logic yang multiline itu nggak bisa. Bagusnya juga kalo switch case emang jangan multiline, agak kotor. Eh tapi selera sih, intinya match gak menggantikan switch case.

Throwing Expression

Sekarang throw jadi expression, bukan lagi statement. Ini adalah hal terbaiknya, sudah lama kutunggu-tunggu. Yeey, sekarang kita bisa throwing exceptions di tempat yang lebih banyak. Langsung implementasiin pake Match Expression ya:

class ServerError extends Exception {};
class NotFound extends Exception {};

function error($statusCode) {
    match ($statusCode) {
        200, 300 => null,
        400 => throw new NotFound(),
        default => throw new ServerError(),
    };
}

try
{
    $statusCode = 500;
    error($statusCode);
}
catch (NotFound)
{
    echo "Page Not Found";
}
catch (ServerError)
{
    echo "Internal Server Error";
}

Code di atas juga ada fitur baru lainnya, yaitu Capturing Catches without Variable. Exception sekarang gak harus disimpan di variabel kalau gak digunain. Dulu kan harus gini:

catch (ServerError $e)
{
    echo "Internal Server Error";
}

PHP ini memang tipe bahasa yang flexible. Jadi kita harus bikin definisi yang jelas sebagai programmer dalam menulis code. Kalau code kita buruk, jangan salahkan bahasa pemrogramannya karena terlalu flexible. Motto PHP sebagai penutup: Better performance, better syntax, improved type safety.

Reference:
https://www.php.net/releases/8.0/index.php
https://stitcher.io/blog/new-in-php-8
https://finematics.com/compiled-vs-interpreted-programming-languages
https://www.zend.com/blog/exploring-new-php-jit-compiler
https://php.watch/articles/jit-in-depth