I/O

I/O #

I/O (Input/Output) adalah fondasi dari hampir semua aplikasi nyata — membaca konfigurasi dari file, menulis log, menerima input dari pengguna CLI, mengunduh data dari URL, atau streaming konten besar tanpa menghabiskan memori. PHP memiliki sistem I/O yang kaya dan fleksibel, dibangun di atas konsep stream — abstraksi yang menyatukan file, jaringan, stdin/stdout, kompresi, dan bahkan custom data source dalam satu antarmuka yang konsisten. Artikel ini membahas operasi file dan direktori secara mendalam, stream dan context yang memungkinkan mengakses URL seperti file, penanganan stdin/stdout untuk aplikasi CLI, stream filter untuk transformasi data on-the-fly, dan SplFileObject sebagai pendekatan OOP yang lebih modern.

Konsep Stream di PHP #

Semua I/O di PHP bekerja melalui stream — saluran data yang bisa dibaca, ditulis, atau keduanya. Fungsi seperti fopen(), file_get_contents(), dan fread() semuanya bekerja pada stream di bawahnya.

flowchart LR
    App[Kode PHP] --> S[Stream\nAbstraksi Terpadu]
    S --> F[file://\nFile sistem]
    S --> H[http://\nhttps://\nURL/Network]
    S --> P[php://\nstdin/stdout\nmemory/temp]
    S --> Z[compress.gz://\nzip://\nKompresi]
    S --> C[Custom\nStream Wrapper]

    style S fill:#fef9c3,stroke:#ca8a04
<?php
// Semua ini menggunakan stream, meski sintaksnya berbeda
$isi1 = file_get_contents('data.txt');           // file stream
$isi2 = file_get_contents('https://api.example.com/data'); // http stream
$isi3 = file_get_contents('php://stdin');        // stdin stream

// Wrapper stream yang tersedia bawaan PHP
$wrappers = stream_get_wrappers();
// ['https', 'ftps', 'compress.zlib', 'compress.bzip2', 'php', 'file', 'glob', 'data', 'http', 'ftp', 'phar', 'zip']

Operasi File Dasar #

Membaca File #

PHP menyediakan beberapa cara membaca file, masing-masing cocok untuk situasi berbeda:

<?php
// 1. file_get_contents() — baca seluruh file sekaligus ke string
// Cocok untuk file kecil-sedang (< beberapa MB)
$isi = file_get_contents('config.json');
$config = json_decode($isi, true);

// 2. file() — baca seluruh file sebagai array of lines
$baris = file('data.csv', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($baris as $i => $baris) {
    echo "$i: $baris\n";
}

// 3. fopen/fread/fclose — baca dengan kontrol penuh
// Cocok untuk file besar (streaming)
$handle = fopen('data_besar.log', 'r');
if ($handle === false) {
    throw new \RuntimeException("Gagal membuka file");
}

try {
    while (!feof($handle)) {
        $baris = fgets($handle, 4096); // baca satu baris, max 4KB
        if ($baris === false) break;
        // proses baris...
    }
} finally {
    fclose($handle); // selalu tutup
}

// 4. readfile() — baca dan langsung output ke browser
// Berguna untuk streaming download
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="laporan.pdf"');
readfile('/path/to/laporan.pdf');

// 5. Baca sekian byte dari posisi tertentu
$handle = fopen('data.bin', 'rb'); // 'b' untuk binary mode
fseek($handle, 1024);              // loncat ke byte 1024
$chunk  = fread($handle, 512);     // baca 512 byte
$posisi = ftell($handle);          // posisi saat ini: 1536
fclose($handle);
<?php
// 1. file_put_contents() — tulis string ke file
// Menimpa isi lama secara default
$bytes = file_put_contents('output.txt', "Baris pertama\n");
echo "Ditulis $bytes byte\n";

// Append — tambahkan di akhir file
file_put_contents('log.txt', date('Y-m-d H:i:s') . " — event terjadi\n", FILE_APPEND);

// Dengan locking — aman untuk tulis bersamaan
file_put_contents('data.json', json_encode($data), LOCK_EX);

// 2. fopen/fwrite/fclose — kontrol penuh
$handle = fopen('laporan.csv', 'w'); // 'w' = tulis, buat jika belum ada, truncate jika ada
if ($handle === false) {
    throw new \RuntimeException("Gagal membuka file untuk ditulis");
}

try {
    // Tulis header CSV
    fputcsv($handle, ['ID', 'Nama', 'Email', 'Total']);

    // Tulis data
    foreach ($data as $row) {
        fputcsv($handle, [$row['id'], $row['nama'], $row['email'], $row['total']]);
    }
} finally {
    fclose($handle);
}

// Mode file fopen yang penting:
// 'r'  — baca saja, pointer di awal
// 'r+' — baca dan tulis, pointer di awal
// 'w'  — tulis saja, buat/truncate, pointer di awal
// 'w+' — baca dan tulis, buat/truncate
// 'a'  — append saja, pointer di akhir
// 'a+' — baca dan append
// 'x'  — tulis saja, gagal jika sudah ada (aman untuk create new)
// 'x+' — baca dan tulis, gagal jika sudah ada

File Lock — Akses Aman dari Beberapa Proses #

<?php
// Atomic read-modify-write dengan exclusive lock
function incrementCounter(string $file): int
{
    $handle = fopen($file, 'c+'); // buka atau buat, tidak truncate

    // Dapatkan exclusive lock — tunggu jika proses lain sedang akses
    flock($handle, LOCK_EX);

    try {
        $nilai  = (int) fread($handle, 20);
        $nilai++;

        rewind($handle);           // kembali ke awal
        ftruncate($handle, 0);     // hapus isi lama
        fwrite($handle, (string) $nilai);

        return $nilai;
    } finally {
        flock($handle, LOCK_UN);   // lepas lock
        fclose($handle);
    }
}

// Aman dipanggil dari beberapa proses bersamaan
echo incrementCounter('/tmp/counter.txt'); // selalu increment dengan benar

Operasi Direktori #

<?php
// Buat direktori
mkdir('/tmp/hasil', 0755);
mkdir('/tmp/a/b/c', 0755, recursive: true); // buat semua level sekaligus

// Cek dan navigasi
echo getcwd();                          // direktori saat ini
chdir('/tmp');                          // pindah direktori
echo realpath('../config');             // path absolut yang sudah di-resolve
echo dirname('/var/www/app/index.php'); // '/var/www/app'
echo basename('/var/www/app/index.php'); // 'index.php'

// Scan direktori
$entries = scandir('/var/www/app');
foreach ($entries as $entry) {
    if ($entry === '.' || $entry === '..') continue;
    $path = '/var/www/app/' . $entry;
    echo ($entry . (is_dir($path) ? '/' : '') . "\n");
}

// Glob — pattern matching untuk file
$phpFiles  = glob('/var/www/app/src/**/*.php');
$csvFiles  = glob('/data/export/*.csv');
$allFiles  = glob('/tmp/{*.log,*.txt}', GLOB_BRACE); // multiple pattern

// Informasi file dan direktori
$file = '/var/www/app/index.php';
echo file_exists($file) ? "ada" : "tidak ada";
echo is_file($file) ? "file" : "bukan file";
echo is_dir(dirname($file)) ? "direktori ada" : "tidak";
echo is_readable($file) ? "bisa dibaca" : "tidak";
echo is_writable($file) ? "bisa ditulis" : "tidak";
echo filesize($file) . " byte";
echo date('Y-m-d H:i:s', filemtime($file)); // waktu modifikasi terakhir

// Salin, pindah, hapus
copy('/tmp/source.txt', '/tmp/dest.txt');
rename('/tmp/old.txt', '/tmp/new.txt');  // pindah atau rename
unlink('/tmp/hapus.txt');                // hapus file
rmdir('/tmp/dir-kosong');                // hapus direktori (harus kosong)

Hapus Direktori Rekursif #

<?php
function hapusDirRekursif(string $path): void
{
    if (!is_dir($path)) {
        throw new \InvalidArgumentException("$path bukan direktori");
    }

    $entries = new \RecursiveIteratorIterator(
        new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
        \RecursiveIteratorIterator::CHILD_FIRST // hapus isi dulu, baru kontainer
    );

    foreach ($entries as $entry) {
        if ($entry->isDir()) {
            rmdir($entry->getPathname());
        } else {
            unlink($entry->getPathname());
        }
    }

    rmdir($path); // hapus direktori utama
}

// Salin direktori beserta isinya
function salinDirRekursif(string $sumber, string $tujuan): void
{
    if (!is_dir($tujuan)) {
        mkdir($tujuan, 0755, true);
    }

    $iterator = new \RecursiveIteratorIterator(
        new \RecursiveDirectoryIterator($sumber, \RecursiveDirectoryIterator::SKIP_DOTS),
        \RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $item) {
        $target = $tujuan . DIRECTORY_SEPARATOR . $iterator->getSubPathname();
        if ($item->isDir()) {
            mkdir($target, 0755, true);
        } else {
            copy($item->getPathname(), $target);
        }
    }
}

Stream Context — Konfigurasi Akses Network #

Stream context memungkinkan konfigurasi HTTP request, SSL, FTP, dan opsi lain saat mengakses URL sebagai file:

<?php
// HTTP GET dengan header kustom
$context = stream_context_create([
    'http' => [
        'method'  => 'GET',
        'header'  => implode("\r\n", [
            'Authorization: Bearer ' . $token,
            'Accept: application/json',
            'User-Agent: MyApp/1.0',
        ]),
        'timeout' => 10, // timeout 10 detik
    ],
    'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
    ],
]);

$response = file_get_contents('https://api.example.com/data', false, $context);
$data     = json_decode($response, true);

// HTTP POST — kirim JSON
$payload = json_encode(['nama' => 'Budi', 'email' => '[email protected]']);

$context = stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => implode("\r\n", [
            'Content-Type: application/json',
            'Content-Length: ' . strlen($payload),
            'Authorization: Bearer ' . $token,
        ]),
        'content' => $payload,
        'timeout' => 30,
        'ignore_errors' => true, // dapatkan body meski HTTP error 4xx/5xx
    ],
]);

$response = file_get_contents('https://api.example.com/users', false, $context);
$httpCode = $http_response_header[0]; // "HTTP/1.1 201 Created"

// Ambil HTTP status code
preg_match('/HTTP\/\d\.\d (\d{3})/', $httpCode, $m);
$statusCode = (int) $m[1]; // 201

Stream PHP Khusus #

PHP menyediakan stream wrapper php:// untuk berbagai tujuan khusus:

<?php
// php://stdin — input dari terminal (blocking)
$input = fgets(STDIN);
echo "Kamu input: $input";

// php://stdout dan php://stderr — output
fwrite(STDOUT, "Pesan normal\n");
fwrite(STDERR, "Pesan error\n"); // tidak di-buffer, langsung keluar

// php://memory — file sementara di memori (cepat, tidak ada I/O disk)
$handle = fopen('php://memory', 'r+');
fwrite($handle, "data sementara\n");
rewind($handle);
$isi = stream_get_contents($handle);
fclose($handle);
// $isi = "data sementara\n"

// php://temp — di memori sampai 2MB, lalu pindah ke file sementara
$handle = fopen('php://temp', 'r+');
fwrite($handle, str_repeat('x', 3_000_000)); // > 2MB, pindah ke disk

// php://input — raw request body (sekali baca, tidak bisa di-rewind)
// Berguna untuk menerima JSON atau binary dari POST request
$body  = file_get_contents('php://input');
$data  = json_decode($body, true);

// php://filter — terapkan filter saat membaca
// Encode base64 sebuah file saat dibaca
$base64 = file_get_contents('php://filter/convert.base64-encode/resource=gambar.png');

Stream Filter — Transformasi Data On-the-Fly #

Stream filter memungkinkan data ditransformasi saat mengalir melalui stream — tanpa harus membaca seluruh file ke memori:

<?php
// Filter bawaan yang berguna
// string.toupper, string.tolower, string.rot13
// convert.base64-encode, convert.base64-decode
// zlib.deflate, zlib.inflate
// bzip2.compress, bzip2.decompress

// Baca file dan konversi ke uppercase sekaligus
$handle = fopen('data.txt', 'r');
stream_filter_append($handle, 'string.toupper');

while (!feof($handle)) {
    echo fread($handle, 1024); // sudah uppercase saat keluar filter
}
fclose($handle);

// Kompresi saat menulis
$handle = fopen('data.gz', 'wb');
stream_filter_append($handle, 'zlib.deflate', STREAM_FILTER_WRITE, ['level' => 6]);
fwrite($handle, "data yang akan dikompres\n");
fwrite($handle, str_repeat("lorem ipsum ", 1000));
fclose($handle);

// Implementasi filter kustom
class SanitasiHtmlFilter extends \php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = htmlspecialchars($bucket->data, ENT_QUOTES, 'UTF-8');
            $consumed    += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('sanitasi.html', SanitasiHtmlFilter::class);

// Gunakan filter kustom
$handle = fopen('php://memory', 'r+');
stream_filter_append($handle, 'sanitasi.html');
fwrite($handle, '<script>alert("xss")</script>');
rewind($handle);
echo fread($handle, 100);
// &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

SplFileObject — I/O Berorientasi Objek #

SplFileObject adalah cara OOP untuk bekerja dengan file, mengimplementasikan Iterator sehingga bisa langsung di-foreach:

<?php
// Membaca CSV dengan SplFileObject
$file = new \SplFileObject('data.csv', 'r');
$file->setFlags(
    \SplFileObject::READ_CSV |        // parse sebagai CSV otomatis
    \SplFileObject::SKIP_EMPTY |      // lewati baris kosong
    \SplFileObject::DROP_NEW_LINE     // hapus newline di akhir baris
);

// Baca header
$file->rewind();
$header = $file->current();
$file->next();

// Baca data
while (!$file->eof()) {
    $row = $file->current();
    if ($row === false || $row === [null]) {
        $file->next();
        continue;
    }

    $data = array_combine($header, $row);
    // proses $data...
    $file->next();
}

// Menulis CSV dengan SplFileObject
$output = new \SplFileObject('laporan.csv', 'w');
$output->fputcsv(['ID', 'Nama', 'Email']); // header

foreach ($users as $user) {
    $output->fputcsv([$user['id'], $user['nama'], $user['email']]);
}

// SplFileObject juga bisa dipakai dengan foreach langsung
$file = new \SplFileObject('access.log', 'r');
$file->setFlags(\SplFileObject::DROP_NEW_LINE);

$errorCount = 0;
foreach ($file as $baris) {
    if (str_contains($baris, ' 500 ')) {
        $errorCount++;
    }
}
echo "Error 500 ditemukan: $errorCount\n";

File Sementara #

<?php
// tmpfile() — buat file sementara yang otomatis dihapus saat ditutup
$tmp = tmpfile();
fwrite($tmp, "data sementara");
rewind($tmp);
$isi = fread($tmp, 100);
fclose($tmp); // file otomatis dihapus

// tempnam() — buat nama file sementara yang unik
$namaFile = tempnam(sys_get_temp_dir(), 'prefix_');
// Contoh: /tmp/prefix_abc123

file_put_contents($namaFile, $data);
// proses...
unlink($namaFile); // harus hapus manual

// sys_get_temp_dir() — direktori temp yang sesuai OS
echo sys_get_temp_dir(); // /tmp di Linux, C:\Windows\Temp di Windows

I/O untuk Aplikasi CLI #

<?php
// Baca dari stdin — input interaktif
echo "Masukkan nama kamu: ";
$nama = trim(fgets(STDIN));
echo "Halo, $nama!\n";

// Deteksi apakah input dari terminal atau pipe
if (posix_isatty(STDIN)) {
    echo "Input dari terminal\n";
} else {
    echo "Input dari pipe atau file\n";
}

// Baca password tanpa echo (Linux/macOS)
function bacaPassword(string $prompt = "Password: "): string
{
    if (PHP_OS_FAMILY === 'Windows') {
        // Windows tidak support cara ini
        echo $prompt;
        return trim(fgets(STDIN));
    }

    echo $prompt;
    system('stty -echo'); // matikan echo terminal
    $password = trim(fgets(STDIN));
    system('stty echo');  // nyalakan kembali
    echo "\n";
    return $password;
}

// Output terstruktur ke stderr (untuk error) dan stdout (untuk data)
function sukses(string $pesan): void
{
    fwrite(STDOUT, "\033[32m✓ $pesan\033[0m\n"); // hijau
}

function error(string $pesan): void
{
    fwrite(STDERR, "\033[31m✗ $pesan\033[0m\n"); // merah
}

function info(string $pesan): void
{
    fwrite(STDOUT, "\033[36mℹ $pesan\033[0m\n"); // cyan
}

// Progress bar sederhana
function progressBar(int $selesai, int $total, int $lebar = 40): void
{
    $persen  = $total > 0 ? round($selesai / $total * 100) : 0;
    $terisi  = (int) ($lebar * $selesai / max($total, 1));
    $kosong  = $lebar - $terisi;
    $bar     = str_repeat('█', $terisi) . str_repeat('░', $kosong);

    // \r — kembali ke awal baris (overwrite, bukan newline baru)
    printf("\r[%s] %d%% (%d/%d)", $bar, $persen, $selesai, $total);

    if ($selesai >= $total) {
        echo "\n"; // newline di akhir
    }
}

$total = 100;
for ($i = 1; $i <= $total; $i++) {
    usleep(50000); // simulasi pekerjaan
    progressBar($i, $total);
}

Anti-Pattern I/O yang Sering Ditemui #

<?php
// ✗ Anti-pattern 1: baca seluruh file besar ke memori
$isi = file_get_contents('data_1gb.csv'); // PHP kehabisan memori!
$baris = explode("\n", $isi);             // perburuk masalah

// ✓ Streaming per baris
$handle = fopen('data_1gb.csv', 'r');
while (($baris = fgets($handle)) !== false) {
    proses($baris); // hanya satu baris di memori
}
fclose($handle);

// ✗ Anti-pattern 2: tidak menutup file handle
function bacaData(): string
{
    $handle = fopen('data.txt', 'r');
    $isi    = fread($handle, 1024);
    return $isi; // handle TIDAK ditutup — resource leak!
}

// ✓ Selalu tutup — gunakan try/finally
function bacaDataBenar(): string
{
    $handle = fopen('data.txt', 'r');
    try {
        return fread($handle, 1024);
    } finally {
        fclose($handle); // selalu ditutup meski ada exception
    }
}

// ✗ Anti-pattern 3: tidak validasi path dari user input
$file = $_GET['file'];
echo file_get_contents($file); // Path traversal attack! ../../etc/passwd

// ✓ Validasi dan batasi path
function bacaFileAman(string $namaFile, string $baseDir): string
{
    // Normalisasi path
    $fullPath   = realpath($baseDir . '/' . $namaFile);

    // Pastikan file ada dalam direktori yang diizinkan
    if ($fullPath === false || !str_starts_with($fullPath, realpath($baseDir))) {
        throw new \InvalidArgumentException("Akses file tidak diizinkan");
    }

    if (!is_readable($fullPath)) {
        throw new \RuntimeException("File tidak bisa dibaca");
    }

    return file_get_contents($fullPath);
}

// ✗ Anti-pattern 4: tulis ke file yang sama dari banyak proses tanpa lock
file_put_contents('log.txt', $pesan, FILE_APPEND); // bisa corrupt saat bersamaan

// ✓ Gunakan FILE_APPEND dengan LOCK_EX
file_put_contents('log.txt', $pesan . "\n", FILE_APPEND | LOCK_EX);

Ringkasan #

  • Semua I/O PHP bekerja via stream — abstraksi terpadu yang menyatukan file, URL, stdin/stdout, dan custom data source dalam satu antarmuka yang konsisten.
  • file_get_contents dan file_put_contents untuk file kecil-sedang; fopen/fread/fwrite/fclose untuk file besar yang perlu di-streaming per chunk agar tidak kehabisan memori.
  • Selalu tutup file handle dengan fclose() — gunakan try/finally untuk memastikan handle selalu ditutup meski terjadi exception.
  • LOCK_EX untuk exclusive lock saat menulis dari beberapa proses bersamaan — cegah data corrupt di log dan counter file.
  • Stream context memungkinkan konfigurasi HTTP request (method, header, timeout, SSL) saat mengakses URL dengan file_get_contents() — tanpa perlu Guzzle untuk kasus sederhana.
  • php://memory dan php://temp untuk file sementara dalam memori — sangat cepat untuk operasi intermediate yang hasilnya tidak perlu disimpan ke disk.
  • SplFileObject dengan READ_CSV flag menjadikan parsing CSV lebih bersih dan bisa diiterasi langsung dengan foreach.
  • Validasi path dari user input selalu — gunakan realpath() dan periksa bahwa path berada dalam direktori yang diizinkan untuk mencegah path traversal attack.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact