Pada titik tertentu, tool PHP Anda perlu memanggil program lain: menjalankan git untuk membaca status repositori, tar untuk mengemas arsip, atau bahkan skrip PHP lain sebagai pekerja terpisah. PHP menyediakan beberapa fungsi untuk hal ini, tetapi hanya satu yang memberi kendali penuh — proc_open(): saluran stdin/stdout/stderr terpisah, environment khusus untuk proses anak, pemantauan status, hingga kemampuan menghentikannya paksa. Pada chapter ini kita bedah fungsi itu dari nol, lalu melengkapi pola pemakaiannya yang paling penting: pemisahan output, override environment, batas waktu, pengamanan argumen, dan kontrak exit code.
Memilih Fungsi yang Tepat #
Sebelum masuk ke proc_open(), mari lihat peta lengkapnya. Perbedaan antar fungsi terletak pada tiga hal: apakah perintah ditafsirkan shell, seberapa besar kendali atas saluran proses anak, dan bagaimana exit code didapatkan.
| Fungsi | Lewat shell? | stdin anak | stdout/stderr terpisah | Exit code |
|---|---|---|---|---|
shell_exec() | ya (sh -c) | tidak | tidak | tidak tersedia |
exec() | ya | tidak | tidak | parameter ketiga |
passthru() | ya | tidak | tidak | parameter kedua |
proc_open() | opsional (bisa tanpa) | ya, pipe | ya | dari proc_close() |
Untuk perintah cepat yang output-nya sederhana, exec() memang lebih ringkas. Namun begitu Anda butuh salah satu dari — mengirim input ke anak, memisahkan stderr dari stdout, mengatur environment, atau membatasi durasi — proc_open() adalah satu-satunya pilihan. Karena alasan itu pula seluruh contoh chapter ini memakainya.
proc_open dari Nol #
Pola dasarnya selalu sama: siapkan daftar descriptor yang mendeskripsikan saluran apa yang ingin kita sambungkan, panggil proc_open() untuk melahirkan anak, lalu bicara lewat pipe yang tersedia. Berikut versi lengkapnya dengan ketiga saluran sekaligus:
<?php
$perintah = [PHP_BINARY, "-r", "echo 'hasil dari anak: ', 21 * 2;"];
$deskriptor = [
0 => ["pipe", "r"], // stdin anak
1 => ["pipe", "w"], // stdout anak
2 => ["pipe", "w"], // stderr anak
];
$proses = proc_open($perintah, $deskriptor, $pipes);
if (!is_resource($proses)) {
fwrite(STDERR, "Gagal memulai proses anak\n");
exit(1);
}
fwrite($pipes[0], "pesan untuk anak\n"); // anak boleh membacanya
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proses);
echo "stdout : " . trim($stdout) . "\n";
echo "stderr : " . var_export(trim($stderr), true) . "\n";
echo "exit : $exitCode\n";Output yang Dihasilkan:
stdout : hasil dari anak: 42
stderr : ''
exit : 0Mari kita bedah per bagian:
- Bentuk array
$perintah: perintah ditulis sebagai array, bukan string — PHP meneruskan argumen langsung keexecvetanpa shell, sehingga spasi atau titik koma dalam argumen tidak punya makna khusus. $deskriptor: kunci0,1,2adalah nomor file descriptor standar Unix;"pipe"berarti kita menghubungkannya ke pipe yang bisa dibaca-tulis dari PHP.$pipes: setelah panggilan sukses, array ini berisi resource stream — indeksnya cocok dengan descriptor yang kita minta.fwrite($pipes[0], ...)lalufclose(): menulis ke stdin anak, lalu menutupnya agar anak melihat end-of-file.stream_get_contents(): membaca seluruh output anak sampai pipe tertutup — sekaligus menunggu anak selesai bekerja.proc_close(): memungut exit code anak sekaligus membersihkan resource proses.
Memisahkan stdout dan stderr #
Fungsi shell_exec() menggabungkan kedua saluran ke satu string — kecuali Anda menempelkan 2>&1 pada perintahnya. Dengan proc_open(), keduanya lahir terpisah sejak awal, sehingga tool Anda bisa memperlakukan hasil dan diagnostik secara berbeda:
<?php
$kodeAnak = 'echo "hasil sukses";'
. ' fwrite(STDERR, "peringatan: mode demo");';
$proses = proc_open(
[PHP_BINARY, "-r", $kodeAnak],
[0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]],
$pipes
);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($proses);
echo "stdout berisi: " . $stdout . "\n";
echo "stderr berisi: " . $stderr . "\n";Output yang Dihasilkan:
stdout berisi: hasil sukses
stderr berisi: peringatan: mode demoAnak proses sengaja menulis ke dua saluran berbeda, dan keduanya tiba sebagai variabel terpisah — siap dimasukkan ke laporan untuk yang satu dan ke log untuk yang lain. Bila salah satu saluran tidak Anda butuhkan, tak perlu membuat pipe-nya: descriptor "file" dengan target /dev/null membuangnya langsung tanpa pernah menyentuh memori PHP, misalnya 2 => ["file", "/dev/null", "w"].
Meneruskan Environment Variabel #
Secara default anak mewarisi seluruh environment induk. Parameter kelima proc_open() menggantinya total dengan array yang Anda berikan — berguna untuk menjalankan anak dengan konfigurasi berbeda tanpa mengubah proses induk. Pola amannya: mulai dari environment saat ini dengan getenv(), lalu timpa kunci yang relevan:
<?php
$env = array_merge(getenv(), [
"APP_ENV" => "staging",
"DB_HOST" => "db-staging.example.com",
]);
$kodeAnak = 'echo "APP_ENV=" . getenv("APP_ENV");'
. ' echo " DB_HOST=" . getenv("DB_HOST");';
$proses = proc_open(
[PHP_BINARY, "-r", $kodeAnak],
[0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]],
$pipes,
null,
$env
);
fclose($pipes[0]);
fclose($pipes[2]);
echo trim(stream_get_contents($pipes[1])) . "\n";
fclose($pipes[1]);
proc_close($proses);Output yang Dihasilkan:
APP_ENV=staging DB_HOST=db-staging.example.comAnak membaca APP_ENV dan DB_HOST persis seperti skrip apa pun membaca environment — nilainya adalah nilai yang kita sisipkan, bukan warisan induk. Parameter keempat (null di atas) adalah direktori kerja anak; isi dengan jalur tertentu bila program eksternal menuntutnya, misalnya repositori git sebelum memanggil git log.
Membatasi Waktu Eksekusi #
Program eksternal bisa menggantung: layanan lambat, berkas korup, atau bug di pihak anak. Tanpa pengaman, tool Anda ikut menggantung selamanya. Kombinasi proc_get_status() dan proc_terminate() memberi batas waktu yang deterministic:
<?php
$perintah = [PHP_BINARY, "-r", "sleep(10); echo 'tidak pernah tercetak';"];
$proses = proc_open($perintah, [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]], $pipes);
fclose($pipes[0]);
$batasDetik = 1;
$mulai = microtime(true);
while (true) {
$status = proc_get_status($proses);
if (!$status["running"]) {
break; // anak selesai sendiri sebelum batas waktu
}
if (microtime(true) - $mulai > $batasDetik) {
proc_terminate($proses); // kirim sinyal penghentian
echo "anak dihentikan karena melewati batas {$batasDetik} detik\n";
break;
}
usleep(50_000);
}
stream_get_contents($pipes[1]); // buang sisa output
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($proses);Output yang Dihasilkan:
anak dihentikan karena melewati batas 1 detikAnak disuruh tidur sepuluh detik, tetapi loop pemantau memeriksa statusnya setiap 50 milidetik dan mengirim sinyal penghentian begitu batas satu detik terlampaui — program utama selesai dalam sekitar satu detik, tidak sepuluh. Angka sleep(10) sengaja dibuat lebih besar daripada batas agar jalur pemaksaan pasti terambil; di tool nyata, batas seperti ini melindungi pipeline dari pekerja yang macet.
Mengamankan Argumen dengan escapeshellarg #
Bentuk perintah array pada proc_open() aman secara alami karena tidak ada shell yang menafsirkan metakarakter. Namun begitu Anda terpaksa memakai bentuk string — misalnya karena fungsi lain atau format perintah yang sudah ada — setiap argumen dari luar wajib dibungkus escapeshellarg():
<?php
$namaBerkas = "Laporan Q1; rm -rf.pdf"; // masukan berbahaya
// bentuk string: shell ikut menafsirkan metakarakter, WAJIB di-escape
$perintahString = "php alat.php proses " . escapeshellarg($namaBerkas);
echo $perintahString . "\n";
// bentuk array: tanpa shell sama sekali, argumen diteruskan apa adanya
$perintahArray = ["php", "alat.php", "proses", $namaBerkas];
echo implode(" ", $perintahArray) . "\n";Output yang Dihasilkan:
php alat.php proses 'Laporan Q1; rm -rf.pdf'
php alat.php proses Laporan Q1; rm -rf.pdfPerhatikan baris pertama: escapeshellarg() membungkus nama berkas dalam tanda kutip tunggal sehingga shell memperlakukannya sebagai satu argumen utuh — titik komanya kehilangan kekuatan pemisah perintah. Baris kedua memperlihatkan versi array yang tidak butuh escaping sama sekali karena memang tidak melewati shell.
Kontrak Exit Code #
Exit code proses anak diambil oleh proc_close(). Tool yang menyusun banyak langkah sebaiknya meneruskan kode itu ke pemanggilnya sendiri — kegagalan anak menjadi kegagalan tool, terbaca jelas oleh pipeline shell dan CI. Contoh berikut sengaja menghasilkan kode 3:
<?php
$perintah = [PHP_BINARY, "-r", "exit(3);"];
$proses = proc_open($perintah, [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]], $pipes);
fclose($pipes[0]);
stream_get_contents($pipes[1]); // tunggu anak selesai sekaligus kosongkan pipe
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proses);
echo "anak keluar dengan kode $exitCode\n";
exit($exitCode); // teruskan kode ke pemanggil (mis. pipeline shell)
Output yang Dihasilkan:
anak keluar dengan kode 3Verifikasi otomatis chapter ini mengonfirmasi blok di atas keluar dengan kode 3 — persis kode yang dipilih anak. Dari sisi terminal, kontrak itu terlihat seperti ini:
php impor.php
echo "exit code: $?"Output yang Dihasilkan:
anak keluar dengan kode 3
exit code: 3Ringkasan #
proc_open() memberi kendali penuh atas proses eksternal: descriptor pipe yang dipilih sendiri, pemisahan stdout dan stderr, environment khusus, batas waktu lewat proc_terminate(), dan exit code lewat proc_close(). Bentuk perintah array menghilangkan risiko injeksi shell; escapeshellarg() menjaga bentuk string bila terpaksa. Anak proses yang andal butuh konfigurasi yang andal juga — di sinilah chapter Configuration Files berikutnya membantu, dengan pola pencarian berkas konfigurasi dan override environment.