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);
Menulis File #
<?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);
// <script>alert("xss")</script>
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_contentsdanfile_put_contentsuntuk file kecil-sedang;fopen/fread/fwrite/fcloseuntuk file besar yang perlu di-streaming per chunk agar tidak kehabisan memori.- Selalu tutup file handle dengan
fclose()— gunakantry/finallyuntuk memastikan handle selalu ditutup meski terjadi exception.LOCK_EXuntuk 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://memorydanphp://tempuntuk file sementara dalam memori — sangat cepat untuk operasi intermediate yang hasilnya tidak perlu disimpan ke disk.SplFileObjectdenganREAD_CSVflag menjadikan parsing CSV lebih bersih dan bisa diiterasi langsung denganforeach.- Validasi path dari user input selalu — gunakan
realpath()dan periksa bahwa path berada dalam direktori yang diizinkan untuk mencegah path traversal attack.