Setiap kali PHP membuka sebuah stream — baik berkas lokal, URL HTTP, arsip ZIP, bahkan data yang tertanam langsung di dalam string — alamat yang kita berikan selalu berbentuk URL: sebuah skema, tanda ://, lalu target. Mesin yang menerjemahkan URL tersebut menjadi operasi baca-tulis nyata disebut stream wrapper. Memahami wrapper berarti memahami mengapa fopen(), file_get_contents(), dan fread() bisa bekerja pada sumber data yang sangat berbeda-beda dengan antarmuka yang sama persis. Pada chapter ini kita akan menjelajahi wrapper bawaan PHP, lalu menutup dengan membuat wrapper kustom yang terdaftar sebagai skema buatan kita sendiri.

Konsep Scheme dan Target #

PHP memecah setiap alamat stream menjadi dua bagian: skema (scheme) sebelum tanda :// dan target setelahnya. Ketika menemukan skema http, PHP menyerahkan URL ke wrapper HTTP; ketika menemukan zip, kontrol berpindah ke wrapper arsip ZIP. Wrapper kemudian yang menentukan bagaimana target ditafsirkan dan opsi context apa saja yang dikenali — topik yang sudah kita bahas tuntas pada chapter stream contexts .

Jika tidak ada skema sama sekali, PHP memakai wrapper file sebagai default:

php
<?php
// Kedua pemanggilan ini setara bagi PHP:
$handleA = fopen('catatan.txt', 'r');
$handleB = fopen('file://catatan.txt', 'r');

Konsekuensinya praktis: seluruh kode pembaca-berkas yang selama ini Anda tulis sebenarnya sudah berjalan di atas wrapper file tanpa Anda sadari. Wrapper bukan fitur eksotis, melainkan fondasi dari setiap operasi I/O PHP.

Daftar Wrapper yang Terpasang #

PHP bisa menampilkan daftar wrapper yang aktif pada instalasi saat ini melalui stream_get_wrappers(). Memeriksa daftar ini penting sebelum bergantung pada wrapper tertentu, karena ketersediaannya bergantung pada ekstensi yang dikompilasi ke dalam binary PHP:

php
<?php
$wrappers = stream_get_wrappers();
echo "Jumlah wrapper terdaftar: " . count($wrappers) . "\n\n";

foreach (['file', 'http', 'https', 'ftp', 'data', 'php', 'zip', 'phar', 'compress.zlib'] as $nama) {
    $status = in_array($nama, $wrappers, true) ? 'tersedia' : 'tidak ada';
    echo str_pad($nama, 16) . $status . "\n";
}

Output yang Dihasilkan:

text
Jumlah wrapper terdaftar: 12

file            tersedia
http            tersedia
https           tersedia
ftp             tersedia
data            tersedia
php             tersedia
zip             tersedia
phar            tersedia
compress.zlib   tersedia

Skrip di atas menghitung seluruh wrapper terdaftar, lalu memeriksa sembilan nama yang paling sering dipakai satu per satu dengan in_array() dan perbandingan ketat. Berikut peran masing-masing wrapper tersebut dalam praktik sehari-hari:

WrapperKegunaan umum
fileBerkas lokal di disk — default ketika skema tidak ditulis
http / httpsMembaca konten dari server web secara read-only
ftp / ftpsMembaca dan menulis berkas melalui server FTP
dataData kecil yang tertanam langsung di dalam URL
phpI/O internal proses: stdin, stdout, memory, filter
zipMembaca berkas di dalam arsip ZIP
compress.zlibMembaca berkas .gz terkompresi tanpa mengekstraknya
pharMengakses berkas di dalam arsip Phar

Untuk zip, target ditulis dengan format khusus: path arsip, tanda pagar, lalu nama berkas di dalamnya, contohnya zip:///path/arsip.zip#catatan.txt. Detail lengkap setiap wrapper tersedia di halaman resmi daftar wrapper yang tercantum di bagian Referensi.

Wrapper data:// #

Wrapper data:// menerapkan konsep data URI (RFC 2397): seluruh isi “berkas” tertanam di dalam URL itu sendiri. Payload biner ditulis dalam Base64 agar aman berada di satu baris URL:

php
<?php
$payload = base64_encode("Halo dari wrapper data://!");
$url = "data://text/plain;base64,{$payload}";

echo "URL lengkap : {$url}\n";
echo "Isi terbaca : " . file_get_contents($url) . "\n";

Output yang Dihasilkan:

text
URL lengkap : data://text/plain;base64,SGFsbyBkYXJpIHdyYXBwZXIgZGF0YTovLyE=
Isi terbaca : Halo dari wrapper data://!

Kode di atas membangun URL secara bertahap: teks sasaran di-encode terlebih dahulu dengan base64_encode(), lalu disisipkan ke dalam pola data://text/plain;base64,.... Ketika file_get_contents() menerima URL tersebut, wrapper data mendekode payload dan menyerahkan hasilnya seolah-olah berasal dari berkas sungguhan — tanpa satu byte pun menyentuh disk.

Wrapper ini cocok untuk data kecil yang ingin ikut berpindah bersama konfigurasi, misalnya fixture pada pengujian. Untuk data berukuran besar, data:// justru buruk: seluruh URL harus muat di memori dan dioperasikan sebagai string, sehingga berkas sungguhan tetap pilihan yang benar.

Keluarga php:// #

Wrapper php adalah gerbang menuju sumber daya internal proses PHP itu sendiri. Anggota kelompok ini yang paling berguna antara lain php://stdin, php://stdout, dan php://stderr untuk I/O standar di CLI, php://memory dan php://temp untuk buffer di memori, serta php://filter untuk mentransformasi data saat lewat. Contoh berikut menunjukkan php://memory — sebuah “berkas” yang seluruhnya hidup di RAM:

php
<?php
$stream = fopen('php://memory', 'w+');

fwrite($stream, "Belajar stream wrapper\n");
fwrite($stream, "dengan php://memory\n");

rewind($stream);

while (($baris = fgets($stream)) !== false) {
    echo "Terbaca: " . trim($baris) . "\n";
}

fclose($stream);

Output yang Dihasilkan:

text
Terbaca: Belajar stream wrapper
Terbaca: dengan php://memory

Mode w+ membuka stream untuk baca dan tulis sekaligus. Setelah dua kali fwrite(), posisi baca berada di akhir stream, sehingga rewind() memundurkannya ke awal sebelum loop fgets() membaca ulang seluruh isi. Bandingkan php://temp: perilakunya serupa, tetapi isi otomatis dipindahkan ke berkas sementara di disk ketika ukurannya melewati batas sekitar 2 MiB — pilihan yang tepat untuk data yang mungkin membesar.

Anggota paling unik adalah php://filter, yang menyisipkan rantai filter di antara kode kita dan sumber data aslinya. Filter dijalankan pada data yang sedang lewat, tanpa perlu memuat semuanya terlebih dahulu:

php
<?php
$url = 'php://filter/read=string.toupper/resource=data://text/plain,Belajar Stream Wrapper';

echo file_get_contents($url);

Output yang Dihasilkan:

text
BELAJAR STREAM WRAPPER

Baca URL di atas dari kanan ke kiri: sumber data sebenarnya adalah data://text/plain,Belajar Stream Wrapper, dan sebelum datanya sampai ke kode kita, filter string.toupper mengubah seluruh hurufnya menjadi kapital. Filter lain yang tersedia mencakup string.rot13, konversi base64, hingga kompresi zlib — dan keduanya bisa dirangkai menjadi pipeline panjang.

Wrapper Kustom dengan stream_wrapper_register #

Fitur paling kuat dari arsitektur wrapper adalah kemampuan mendaftarkan skema kita sendiri. Kita cukup menulis class yang mengimplementasikan metode-metode bernama khusus — stream_open(), stream_read(), stream_write(), dan kawan-kawannya — lalu mendaftarkannya dengan stream_wrapper_register(). Setelah terdaftar, seluruh API berkas standar (fopen(), fgets(), file_get_contents()) otomatis bisa beroperasi pada skema buatan kita.

Contoh berikut membangun wrapper var:// — sebuah “disk virtual” sederhana yang menyimpan isi berkasnya di array statis, sehingga data bertahan selama proses berjalan:

php
<?php
class VarStream
{
    public $context;                    // otomatis diisi PHP
    private static array $storage = []; // "disk" virtual
    private string $key = '';
    private string $mode = 'r';
    private string $isi = '';
    private int $posisi = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
    {
        $this->key = parse_url($path, PHP_URL_HOST) ?? '';
        $this->mode = $mode;
        $this->isi = str_contains($mode, 'w') ? '' : (self::$storage[$this->key] ?? '');
        $this->posisi = str_contains($mode, 'a') ? strlen($this->isi) : 0;

        return true;
    }

    public function stream_read(int $count): string
    {
        $potongan = substr($this->isi, $this->posisi, $count);
        $this->posisi += strlen($potongan);

        return $potongan;
    }

    public function stream_write(string $data): int
    {
        $this->isi = substr_replace($this->isi, $data, $this->posisi, strlen($data));
        $this->posisi += strlen($data);

        return strlen($data);
    }

    public function stream_eof(): bool
    {
        return $this->posisi >= strlen($this->isi);
    }

    public function stream_tell(): int
    {
        return $this->posisi;
    }

    public function stream_seek(int $offset, int $whence): bool
    {
        $target = match ($whence) {
            SEEK_SET => $offset,
            SEEK_CUR => $this->posisi + $offset,
            SEEK_END => strlen($this->isi) + $offset,
        };

        if ($target < 0) {
            return false;
        }
        $this->posisi = $target;

        return true;
    }

    public function stream_stat(): array
    {
        return ['size' => strlen($this->isi)];
    }

    public function stream_close(): void
    {
        if (!str_starts_with($this->mode, 'r') || str_contains($this->mode, '+')) {
            self::$storage[$this->key] = $this->isi;
        }
    }
}

echo 'Sebelum register: ';
var_dump(in_array('var', stream_get_wrappers(), true));

stream_wrapper_register('var', VarStream::class);

echo 'Sesudah register: ';
var_dump(in_array('var', stream_get_wrappers(), true));

$tulis = fopen('var://sapaan', 'w');
fwrite($tulis, "Halo dari wrapper kustom!\n");
fclose($tulis);

$handle = fopen('var://sapaan', 'r');
echo fgets($handle);
fclose($handle);

Output yang Dihasilkan:

text
Sebelum register: bool(false)
Sesudah register: bool(true)
Halo dari wrapper kustom!

Mari kita bedah bagian-bagian terpentingnya:

  1. stream_open() dipanggil oleh fopen(). Nama “berkas” diambil dari bagian host URL (var://sapaan menghasilkan sapaan), mode disimpan untuk nanti, dan isi awal disiapkan: mode yang mengandung w mengosongkan isi lama, sedangkan mode a meletakkan posisi tulis di akhir.
  2. stream_read() dan stream_write() memindahkan data antara kode pemanggil dan properti $isi, sambil menjaga $posisi agar tetap konsisten — inilah tiruan perilaku pointer berkas pada disk.
  3. stream_eof(), stream_tell(), stream_seek(), dan stream_stat() membuat feof(), ftell(), fseek(), dan fstat() bekerja seperti biasa di atas wrapper ini.
  4. stream_close() dipanggil oleh fclose(). Isi hanya disimpan kembali ke $storage ketika mode membuka kemungkinan menulis — mode r murni tidak pernah mengubah data.
  5. Bagian demo membuktikan siklus penuhnya: var belum terdaftar, lalu terdaftar setelah stream_wrapper_register(), lalu fwrite() dan fclose() menyimpan teks, dan fgets() pada pembukaan kedua berhasil membacanya kembali.

Contoh ini sengaja memakai array statis sebagai penyimpanan agar bisa dijalankan tanpa perangkat tambahan. Dengan mengganti isi $storage — misalnya ke Redis, database, atau API — pola yang sama berubah menjadi adapter yang membuat layanan eksternal terasa seperti berkas lokal bagi seluruh kode aplikasi.

Ringkasan #

Stream wrapper adalah lapisan yang menyatukan semua sumber data di bawah satu antarmuka: skema pada URL menentukan driver mana yang bekerja, stream_get_wrappers() memetakan driver yang tersedia, dan stream_wrapper_register() membuka pintu untuk skema buatan sendiri. Arsitektur stream kini lengkap secara konsep — tetapi ketika beberapa proses menulis ke berkas yang sama secara bersamaan, kita butuh satu alat lagi untuk menjaga integritas data: file locking, topik chapter berikutnya.

Referensi #