Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【PHP8.x】proc_open()関数の使い方

proc_open関数の使い方について、初心者にもわかりやすく解説します。

作成日: 更新日:

基本的な使い方

proc_open関数は、パイプを開いて外部コマンドを実行する関数です。複数のパイプを開き、親プロセスと子プロセス間で入出力を双方向に行うことが可能です。この関数は、PHPスクリプトから他のプログラムを実行し、そのプログラムとの間でデータのやり取りを行いたい場合に非常に役立ちます。

具体的には、proc_open関数は、実行するコマンド、ディスクリプタの設定、パイプ、ワーキングディレクトリ、環境変数を引数として受け取ります。ディスクリプタの設定は、子プロセスの標準入力、標準出力、標準エラー出力をどのように扱うかを定義します。パイプは、親プロセスと子プロセス間のデータ伝送に使用されます。ワーキングディレクトリは、子プロセスの実行ディレクトリを指定します。環境変数は、子プロセスに渡される環境変数を設定します。

proc_open関数は、実行されたプロセスのリソースを返します。このリソースを使用して、proc_close関数でプロセスを終了させたり、proc_get_status関数でプロセスの状態を取得したり、fgetsfwriteなどの関数でパイプを通してデータの読み書きを行ったりすることができます。

proc_open関数を使用する際には、セキュリティ上の注意が必要です。外部コマンドを実行するため、コマンドインジェクション攻撃を受ける可能性があります。そのため、実行するコマンドは信頼できるソースからのみ取得し、ユーザーからの入力は適切に検証する必要があります。また、パイプを通じて機密情報が漏洩しないように注意することも重要です。proc_open関数は、複雑な処理を行うための強力なツールですが、適切なセキュリティ対策を講じて使用する必要があります。

構文(syntax)

1proc_open ( string $command , array $descriptor_spec , array &$pipes , string $cwd = null , array $env_vars = null , array $options = null ) : resource|false

引数(parameters)

string|array $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null

  • string|array $command: 実行するコマンドライン文字列、またはコマンドと引数の配列
  • array $descriptor_spec: 3つの標準ストリーム(標準入力、標準出力、標準エラー出力)のハンドリング方法を指定する連想配列
  • array &$pipes: 開かれたファイルポインタの配列。標準入力、標準出力、標準エラー出力にそれぞれ対応
  • ?string $cwd = null: コマンドを実行するカレントディレクトリ
  • ?array $env_vars = null: コマンド実行時に使用する環境変数の連想配列
  • ?array $options = null: プロセス実行時の追加オプションを指定する連想配列

戻り値(return)

リソース

proc_open関数は、新しいプロセスを開き、そのプロセスへのパイプ接続を確立します。この関数は、成功した場合、プロセスの標準入力、標準出力、標準エラー出力へのパイプを表す3つのファイルポインタを含むリソースを返します。このリソースは、後続のproc_close関数などで使用されます。

サンプルコード

PHP proc_openで非同期プロセスを実行する

1<?php
2
3/**
4 * プロセスを非同期で実行し、その出力を読み取ります。
5 *
6 * この関数は、外部プロセスをすぐに完了を待たずに開始し、
7 * その標準出力 (stdout) および標準エラー (stderr) ストリームから
8 * 非ブロッキング方式でデータを読み取る方法を示します。
9 *
10 * @param string $command 実行するコマンド文字列。例: 'php -r "sleep(3); echo \'Hello\';"'
11 * @return void
12 */
13function runAsyncCommand(string $command): void
14{
15    // 1. ディスクリプタ仕様の定義: 子プロセスのI/O (入力/出力) ストリームの処理方法を定義します。
16    //    - 0 => stdin: 子プロセスがここから読み取ります。書き込み可能なパイプとして設定します。
17    //    - 1 => stdout: 子プロセスが通常出力を行います。読み取り可能なパイプとして設定します。
18    //    - 2 => stderr: 子プロセスがエラー出力を行います。読み取り可能なパイプとして設定します。
19    $descriptorSpec = [
20        0 => ['pipe', 'r'], // 子プロセスのstdin (親が書き込む)
21        1 => ['pipe', 'w'], // 子プロセスのstdout (親が読み取る)
22        2 => ['pipe', 'w']  // 子プロセスのstderr (親が読み取る)
23    ];
24
25    $pipes = []; // この配列には、proc_openによってパイプへのファイルポインタが設定されます。
26    $process = proc_open($command, $descriptorSpec, $pipes);
27
28    // 2. エラーハンドリング: プロセスが正常に開かれたかを確認します。
29    if (!is_resource($process)) {
30        echo "エラー: コマンド '{$command}' のプロセスを開けませんでした。\n";
31        return;
32    }
33
34    echo "親: コマンド '{$command}' のプロセスが開始されました。親スクリプトを続行します...\n";
35
36    // 3. stdinパイプを閉じます: この例では、子プロセスに何も入力する必要がないため、すぐに閉じます。
37    //    これにより、子プロセスが入力待ちで無限にブロックするのを防ぎます。
38    fclose($pipes[0]);
39
40    // 4. stdoutとstderrのパイプを非ブロッキングモードに設定します。
41    //    これは非同期読み取りに不可欠です。データが利用可能でなくても、`fread()` や `stream_get_contents()` は
42    //    待機せずにすぐに戻ります。
43    stream_set_blocking($pipes[1], false);
44    stream_set_blocking($pipes[2], false);
45
46    // 子プロセスからの出力を蓄積するためのバッファ。
47    $stdoutBuffer = '';
48    $stderrBuffer = '';
49
50    // 5. 非同期読み取りループ: 子プロセスからの出力を継続的にチェックします。
51    //    このループは、子プロセスが終了し、すべての出力が読み取られるまで実行されます。
52    while (true) {
53        $read = [$pipes[1], $pipes[2]]; // 読み取り可能性を監視するパイプの配列。
54        $write = null;                  // 書き込み可能性を監視するパイプはありません。
55        $except = null;                 // 例外条件を監視するパイプはありません。
56
57        // `stream_select()` は、指定されたストリームのいずれかでアクティビティがあるのを待ちます。
58        // タイムアウトが0の場合は非ブロッキング、指定された秒数までブロックします。
59        // ここでは最大1秒間ブロックし、親スクリプトが応答性を保ちます。
60        $numChangedStreams = stream_select($read, $write, $except, 1);
61
62        if ($numChangedStreams === false) {
63            // stream_select中にエラーが発生しました (例: パイプが予期せず閉じられた)。
64            echo "親: ストリームチェック中にエラーが発生しました。\n";
65            break;
66        } elseif ($numChangedStreams === 0) {
67            // 1秒後も読み取り可能なストリームはありませんでした。
68            // これにより、親は他のタスクを実行したり、再チェックしたりできます。
69            // ここでプロセスのステータスを確認します。
70            $status = proc_get_status($process);
71            if (!$status['running']) {
72                // プロセスが実行中でなく、過去1秒間にデータが来ていない場合、終了した可能性が高い。
73                // 以下の `feof` チェックの方がより堅牢です。
74                break;
75            }
76            continue; // ループを続行して再度チェックします。
77        }
78
79        // stdoutが読み取り可能であれば、そこから読み取ります。
80        if (in_array($pipes[1], $read)) {
81            $output = fread($pipes[1], 8192); // 最大8KB読み取ります。
82            if ($output !== false && $output !== '') {
83                echo "親 (stdout): " . $output;
84                $stdoutBuffer .= $output;
85            }
86        }
87
88        // stderrが読み取り可能であれば、そこから読み取ります。
89        if (in_array($pipes[2], $read)) {
90            $error = fread($pipes[2], 8192); // 最大8KB読み取ります。
91            if ($error !== false && $error !== '') {
92                echo "親 (stderr): " . $error;
93                $stderrBuffer .= $error;
94            }
95        }
96
97        // 6. プロセスステータスの確認: 子プロセスがまだ実行中かどうかを判断します。
98        $status = proc_get_status($process);
99
100        // プロセスが実行中でなく、かつすべてのパイプがファイル終端 (もうデータがない) に達している場合、ループを終了します。
101        // これにより、プロセスが終了した後もバッファされたすべての出力が読み取られることが保証されます。
102        if (!$status['running'] && feof($pipes[1]) && feof($pipes[2])) {
103            break;
104        }
105    }
106
107    // 7. クリーンアップ: 残っているすべてのパイプリソースを閉じます。
108    fclose($pipes[1]);
109    fclose($pipes[2]);
110
111    // 8. プロセスリソースを閉じ、その終了コードを取得します。
112    $returnCode = proc_close($process);
113
114    echo "親: プロセスは終了コード {$returnCode} で終了しました。\n";
115    // プロセス終了後に収集されたすべての出力を表示するには、以下の行のコメントを解除してください。
116    // echo "親: --- 全体 STDOUT ---\n" . $stdoutBuffer . "\n----------------------\n";
117    // echo "親: --- 全体 STDERR ---\n" . $stderrBuffer . "\n----------------------\n";
118}
119
120// --- 使用例 ---
121// このコマンドは、0.5秒ごとにメッセージを3回出力する簡単なPHPスクリプトを実行し、
122// 増分出力がある時間のかかるタスクをシミュレートします。
123runAsyncCommand('php -r "
124    for ($i = 0; $i < 3; $i++) {
125        echo \"Child output $i\\n\"; // エスケープされた改行を使用
126        usleep(500000); // 0.5秒待機 (500,000マイクロ秒)
127    }
128    echo \"Child finished.\\n\";
129"');
130
131echo "\n親: このメッセージは `runAsyncCommand` が呼び出された直後に表示され、子プロセスが完全に終了する前です。\n";
132echo "親: メインスクリプトは、子がバックグラウンドで実行されている間も、その作業を続行できます。\n";
133

PHPのproc_open関数は、指定されたコマンドを新しいプロセスとして実行し、その入出力(I/O)ストリームを制御するためのリソースを返す関数です。第一引数$commandには実行したいコマンド文字列を指定し、第二引数$descriptor_specでは標準入力・出力・エラー(stdin/stdout/stderr)をパイプなどでどのように扱うかを定義します。第三引数&$pipesには、定義したパイプへのファイルポインタが設定され、戻り値は開かれたプロセスを表すリソースとなります。

このサンプルコードでは、proc_openを用いて外部プロセスを非同期で実行する方法を示しています。非同期実行の主な利点は、子プロセスの完了を待たずに親スクリプトが次の処理に進め、システム全体の応答性を高められる点です。具体的には、stream_set_blocking($pipes[N], false)でI/Oパイプを非ブロッキングモードに設定し、stream_select関数を使って、データが読み取り可能になったパイプを効率的に監視します。これにより、データがない場合でもプログラムが停止せず、他のタスクを実行できます。子プロセスからの出力は、データがあるたびにループ内で読み取られ、プロセスが終了した後、proc_closeでリソースが解放されます。このようにproc_openと関連関数を組み合わせることで、PHPで高度なプロセス間通信と非同期処理を実現できます。

proc_openは外部プロセスを非同期で実行する際に利用されます。まず、子プロセスの入出力(stdin/stdout/stderr)と親プロセスのパイプ接続をディスクリプタ仕様で正確に定義することが重要です。子プロセスへの入力が不要な場合は、stdinパイプは速やかに閉じ、子プロセスが入力待ちで停止しないようにします。非同期でパイプから読み取るには、stream_set_blockingで非ブロッキングモードにし、stream_selectで複数のパイプの状態を効率的に監視してください。プロセスの終了判断は、proc_get_statusfeofを組み合わせて、プロセスが終了し、かつすべての出力が読み取られたことを確認するのが確実です。最後に、全てのパイプとプロセスリソースはfcloseproc_closeで忘れずに解放し、リソースリークを防ぎましょう。

PHPでproc_openを非同期実行する

1<?php
2
3/**
4 * 非同期(ノンブロッキング)で外部コマンドを実行し、標準出力と標準エラー出力をリアルタイムで取得します。
5 *
6 * @param string $command 実行するコマンド文字列。
7 * @return array 実行結果を格納した連想配列(stdout, stderr, exitCode, error)。
8 */
9function runNonBlockingProcess(string $command): array
10{
11    // プロセスとの通信方法を定義するディスクリプタ仕様
12    // 0 => stdin (子プロセスが親から読み込む)
13    // 1 => stdout (子プロセスが親に書き込む)
14    // 2 => stderr (子プロセスが親に書き込む)
15    $descriptorSpec = [
16        0 => ['pipe', 'r'], // stdin (読み込みモード)
17        1 => ['pipe', 'w'], // stdout (書き込みモード)
18        2 => ['pipe', 'w'], // stderr (書き込みモード)
19    ];
20
21    $pipes = [];
22    // proc_open() で子プロセスを開始し、パイプを確立
23    $process = proc_open($command, $descriptorSpec, $pipes);
24
25    if (!is_resource($process)) {
26        return ['error' => 'Failed to open process.', 'stdout' => '', 'stderr' => '', 'exitCode' => -1];
27    }
28
29    // stdout と stderr のパイプをノンブロッキングモードに設定
30    // これにより、fread() がデータがない場合にすぐに戻るようになります。
31    stream_set_blocking($pipes[1], false);
32    stream_set_blocking($pipes[2], false);
33
34    $stdoutBuffer = '';
35    $stderrBuffer = '';
36
37    // stdin はこの例では使用しないため、すぐに閉じます
38    fclose($pipes[0]);
39    unset($pipes[0]); // stream_selectの対象から外すためunsetする
40
41    // プロセスが実行中であるか、またはパイプから読み取るべきデータがある限りループを継続
42    while (true) {
43        $read = []; // 読み込み可能なストリームの配列
44        $write = null; // 書き込み可能なストリームの配列(この例では使用しない)
45        $except = null; // 例外が発生したストリームの配列(この例では使用しない)
46
47        // まだ閉じられていないstdoutとstderrパイプを監視対象に追加
48        if (isset($pipes[1])) {
49            $read[] = $pipes[1];
50        }
51        if (isset($pipes[2])) {
52            $read[] = $pipes[2];
53        }
54
55        // 監視対象のパイプが一つもなければ、ループを抜ける準備ができたことを意味する
56        if (empty($read)) {
57            // パイプがすべて閉じられたので、プロセスの状態を確認して終了
58            $status = proc_get_status($process);
59            if (!$status['running']) {
60                break;
61            }
62        }
63
64        // stream_select() を使用して、データが利用可能になるまで待機します。
65        // 第5引数はタイムアウトをマイクロ秒で指定します。0.1秒待機します。
66        // これにより、CPU使用率が高くなるのを防ぎつつ、頻繁に状態を確認できます。
67        $numChangedStreams = stream_select($read, $write, $except, 0, 100000);
68
69        if ($numChangedStreams === false) {
70            // stream_select でエラーが発生した場合
71            break;
72        }
73
74        // stdout にデータがある場合、読み取る
75        if (isset($pipes[1]) && in_array($pipes[1], $read)) {
76            $data = fread($pipes[1], 8192); // 最大8KB読み取る
77            if ($data === false || $data === '') {
78                // EOF (ファイルの終端) またはエラーの場合、パイプを閉じる
79                fclose($pipes[1]);
80                unset($pipes[1]);
81            } else {
82                $stdoutBuffer .= $data;
83            }
84        }
85
86        // stderr にデータがある場合、読み取る
87        if (isset($pipes[2]) && in_array($pipes[2], $read)) {
88            $data = fread($pipes[2], 8192); // 最大8KB読み取る
89            if ($data === false || $data === '') {
90                // EOF またはエラーの場合、パイプを閉じる
91                fclose($pipes[2]);
92                unset($pipes[2]);
93            } else {
94                $stderrBuffer .= $data;
95            }
96        }
97
98        // プロセスの現在の状態を取得
99        $status = proc_get_status($process);
100
101        // プロセスが終了し、かつすべてのパイプが閉じられている場合、ループを終了
102        if (!$status['running'] && empty($pipes[1]) && empty($pipes[2])) {
103            break;
104        }
105    }
106
107    // 残っている可能性のあるパイプをすべて閉じる
108    foreach ($pipes as $pipe) {
109        if (is_resource($pipe)) {
110            fclose($pipe);
111        }
112    }
113    // プロセスリソースを閉じ、終了コードを取得
114    $exitCode = proc_close($process);
115
116    return [
117        'stdout' => $stdoutBuffer,
118        'stderr' => $stderrBuffer,
119        'exitCode' => $exitCode,
120        'error' => null,
121    ];
122}
123
124// --- 使用例 ---
125
126// このコマンドは3秒間、1秒ごとに標準出力にメッセージを出力し、その後標準エラーに出力します。
127$commandToRun = 'php -r "
128    for ($i=0; $i<3; $i++) {
129        echo \'Stdout line \' . $i . \' from child process\' . PHP_EOL;
130        sleep(1);
131    }
132    file_put_contents(\'php://stderr\', \'Stderr message after stdout from child.\' . PHP_EOL);
133    sleep(1); // stderrがフラッシュされるのを少し待つ
134"';
135
136echo "--- 非同期プロセスの開始 ---\n";
137echo "コマンド: " . $commandToRun . "\n";
138
139$result = runNonBlockingProcess($commandToRun);
140
141echo "\n--- プロセス終了 ---\n";
142if ($result['error']) {
143    echo "エラー: " . $result['error'] . "\n";
144}
145echo "終了コード: " . $result['exitCode'] . "\n";
146echo "--- STDOUT ---\n" . $result['stdout'];
147echo "--- STDERR ---\n" . $result['stderr'];
148
149// 別の簡単なコマンドの例
150echo "\n--- 簡単なコマンドの実行例 ---\n";
151$simpleCommand = 'echo "Hello from a quick child process" && echo "Error output here" >&2';
152echo "コマンド: " . $simpleCommand . "\n";
153
154$simpleResult = runNonBlockingProcess($simpleCommand);
155
156echo "\n--- プロセス終了 ---\n";
157if ($simpleResult['error']) {
158    echo "エラー: " . $simpleResult['error'] . "\n";
159}
160echo "終了コード: " . $simpleResult['exitCode'] . "\n";
161echo "--- STDOUT ---\n" . $simpleResult['stdout'];
162echo "--- STDERR ---\n" . $simpleResult['stderr'];
163
164?>

PHPのproc_open関数は、指定された外部プログラムを実行し、その標準入出力や標準エラー出力とパイプを通じて通信するための強力な機能です。このサンプルコードでは、proc_openを活用し、外部コマンドを非同期(ノンブロッキング)で実行しながら、子プロセスの出力(標準出力および標準エラー出力)をリアルタイムで取得する方法を示しています。

第一引数$commandには実行したいコマンドの文字列を指定します。第二引数$descriptor_specは、子プロセスとの間でどのようにデータをやり取りするか(例: 標準入力を親から子へ渡す、子の標準出力を親が受け取るなど)を詳細に定義する配列です。特に、パイプを設定することで、親プロセスと子プロセスがデータのストリームを交換できるようになります。第三引数$pipesは参照渡しされ、proc_openの実行後にこれらのパイプリソースが格納されます。

proc_open自体は子プロセスを表すリソースを戻り値として返します。このリソースを通じて、proc_get_statusでプロセスの実行状態を監視し、最終的にproc_closeでプロセスを終了させます。サンプルコードでは、stream_set_blocking関数でパイプをノンブロッキングモードに設定し、stream_select関数を使ってデータが利用可能になるまで効率的に待機することで、CPUリソースの無駄遣いを防ぎながら、子プロセスの完了を待たずに親プロセスが他の処理を進められるようにしています。これにより、子プロセスが出力を生成するたびに親プロセスがそれを即座に読み取り、バッファリングすることが可能です。最終的に、実行されたコマンドの標準出力、標準エラー出力、および終了コードを連想配列として返します。

proc_openで外部コマンドを実行する際は、セキュリティ確保のため、ユーザーからの入力値をコマンドに直接含めず、escapeshellcmdなどの関数で必ず適切にエスケープしてください。これを怠ると、コマンドインジェクションによる重大な脆弱性につながります。また、開いたプロセスやパイプなどのリソースは、処理完了後にproc_closefcloseで確実に解放することが非常に重要です。解放しない場合、リソースリークやシステムリソース枯渇の原因となります。ノンブロッキング処理は、stream_set_blockingstream_selectを用いて、パイプからのデータ読み込みとプロセスの終了を継続的に監視する複雑なロジックを必要とします。プロセス終了後もパイプにデータが残る可能性があるため、すべてのデータを読み切ってからリソースを閉じるよう注意してください。

PHPで外部コマンドを実行し出力を取得する

1<?php
2
3/**
4 * Executes a shell command using proc_open and captures its output.
5 *
6 * This function demonstrates how to run an external command, capture its standard output
7 * and standard error, and then close the process properly.
8 *
9 * @param string $command The command to execute.
10 * @return array|false An associative array containing 'stdout', 'stderr', and 'return_code'.
11 *                     Returns false if the process could not be opened.
12 */
13function executeCommandWithProcOpen(string $command): array|false
14{
15    // Define how the child process's standard I/O streams should be handled.
16    // 0 => stdin: The child process can read from this pipe (parent writes to it).
17    // 1 => stdout: The child process writes to this pipe (parent reads from it).
18    // 2 => stderr: The child process writes to this pipe (parent reads from it).
19    $descriptorSpec = [
20        0 => ['pipe', 'r'],
21        1 => ['pipe', 'w'],
22        2 => ['pipe', 'w']
23    ];
24
25    $pipes = []; // This array will be populated by proc_open with file pointers for each pipe.
26
27    // Open the process. The $pipes argument is passed by reference.
28    $process = proc_open($command, $descriptorSpec, $pipes);
29
30    if (!is_resource($process)) {
31        echo "Error: Failed to open process for command: {$command}\n";
32        return false;
33    }
34
35    // We are not sending any input to the command in this example,
36    // so we immediately close the stdin pipe to prevent the child process
37    // from potentially waiting for input and hanging.
38    fclose($pipes[0]);
39
40    // Read the standard output (stdout) from the child process.
41    $stdout = stream_get_contents($pipes[1]);
42    fclose($pipes[1]); // Close the stdout pipe after reading.
43
44    // Read the standard error (stderr) from the child process.
45    $stderr = stream_get_contents($pipes[2]);
46    fclose($pipes[2]); // Close the stderr pipe after reading.
47
48    // Close the process and retrieve its exit status (return code).
49    $returnCode = proc_close($process);
50
51    return [
52        'stdout' => $stdout,
53        'stderr' => $stderr,
54        'return_code' => $returnCode
55    ];
56}
57
58// --- Example Usage ---
59
60// Example 1: Execute a simple command that outputs to stdout.
61// This command uses 'echo' which works on most operating systems.
62$commandToExecute = 'echo "Hello from proc_open! Current PHP version: ' . PHP_VERSION . '"';
63echo "Executing command: {$commandToExecute}\n";
64
65$result = executeCommandWithProcOpen($commandToExecute);
66
67if ($result === false) {
68    echo "Failed to execute command: {$commandToExecute}\n";
69} else {
70    echo "--- Command Output ---\n";
71    echo "STDOUT:\n";
72    echo $result['stdout'];
73    echo "STDERR:\n";
74    echo $result['stderr'];
75    echo "Return Code: " . $result['return_code'] . "\n";
76
77    if ($result['return_code'] !== 0) {
78        echo "Note: Command finished with a non-zero exit code, indicating an error or specific status.\n";
79    }
80}
81
82echo "\n"; // Add a newline for better readability between examples.
83
84// Example 2: Execute a command that is likely to produce an error (e.g., a non-existent command).
85$nonExistentCommand = 'this_command_does_not_exist_12345';
86echo "Executing command: {$nonExistentCommand}\n";
87
88$errorResult = executeCommandWithProcOpen($nonExistentCommand);
89
90if ($errorResult === false) {
91    echo "Failed to execute command: {$nonExistentCommand}\n";
92} else {
93    echo "--- Command Output ---\n";
94    echo "STDOUT:\n";
95    echo $errorResult['stdout'];
96    echo "STDERR:\n";
97    echo $errorResult['stderr'];
98    echo "Return Code: " . $errorResult['return_code'] . "\n";
99
100    if ($errorResult['return_code'] !== 0) {
101        echo "Note: Command finished with a non-zero exit code, as expected for a non-existent command.\n";
102    }
103}

PHPのproc_open関数は、PHPスクリプトから外部のプログラムやシェルコマンドを実行し、その標準入力、標準出力、標準エラー出力を詳細に制御するための機能です。

この関数は、実行したいコマンドの文字列である$command、子プロセスの標準入出力(stdin, stdout, stderr)をどのように扱うかを定義する配列$descriptor_spec、そしてproc_openが生成するパイプ(ファイルポインタ)が格納される参照渡し配列&$pipesなどを受け取ります。$descriptor_specでは、0番を子プロセスの標準入力、1番を標準出力、2番を標準エラー出力としてパイプ経由でPHPから読み書きできるように設定します。関数は成功すると子プロセスを表すリソースを返し、失敗した場合はfalseを返します。

サンプルコードのexecuteCommandWithProcOpen関数は、このproc_openを利用して外部コマンドを実行し、その標準出力、標準エラー出力、および終了コードを取得する例です。まずproc_openで子プロセスを起動し、取得したパイプの中から標準入力パイプ($pipes[0])を閉じます。次に、stream_get_contents関数で標準出力パイプ($pipes[1])と標準エラー出力パイプ($pipes[2])からコマンドの実行結果を読み込みます。すべてのパイプをfcloseで閉じた後、proc_closeで子プロセスを終了させ、その終了コードを取得します。これにより、外部コマンドの実行結果を確実に捕捉し、適切なエラーハンドリングを行うことが可能になります。

proc_openは外部コマンドを実行するため、セキュリティには特に注意が必要です。ユーザーからの入力をコマンドに含める場合は、コマンドインジェクションを防ぐためescapeshellarg()などで適切にエスケープ処理を行ってください。

開いたプロセス(proc_openの戻り値)と、$pipesで取得した各パイプは、必ずproc_close()fclose()で閉じることが重要です。これを怠ると、リソースリークやシステム負荷の原因となります。

コマンド実行の成否は、proc_openがリソースを返したかだけでなく、proc_closeで得られる終了コード(通常0が成功)も確認してください。標準入力のパイプは、データを送らない場合でも早期に閉じることで、プロセスのハングアップを防ぎます。

PHPで外部コマンドを実行し、結果を取得する

1<?php
2
3/**
4 * proc_open を使って外部コマンドを実行し、その出力と終了コードを取得する関数。
5 *
6 * この関数は、外部コマンドの標準出力 (stdout) と標準エラー出力 (stderr) を捕捉し、
7 * プロセスの終了コードを返します。
8 * 'proc_open not working' の一般的な原因である出力の読み取り漏れやプロセスリソースの解放忘れを解決します。
9 *
10 * @param string $command 実行するコマンド。
11 * @param string|null $cwd コマンドを実行するカレントワーキングディレクトリ。null の場合は現在のディレクトリ。
12 * @param array|null $env_vars 子プロセスに渡す環境変数の配列。null の場合は現在の環境変数が継承される。
13 * @return array 実行結果を格納した連想配列。キー: 'stdout' (標準出力), 'stderr' (標準エラー出力), 'exitCode' (終了コード)。
14 */
15function runExternalCommand(string $command, ?string $cwd = null, ?array $env_vars = null): array
16{
17    // 子プロセスへのファイルディスクリプタの設定
18    // 0: stdin - 子プロセスへの入力。PHP側からは書き込みパイプとして扱われる。
19    // 1: stdout - 子プロセスからの標準出力。PHP側からは読み取りパイプとして扱われる。
20    // 2: stderr - 子プロセスからの標準エラー出力。PHP側からは読み取りパイプとして扱われる。
21    $descriptorSpec = [
22        0 => ['pipe', 'r'], // stdin: PHPが子プロセスにデータを書き込むためのパイプ
23        1 => ['pipe', 'w'], // stdout: PHPが子プロセスの標準出力を読み込むためのパイプ
24        2 => ['pipe', 'w'], // stderr: PHPが子プロセスの標準エラー出力を読み込むためのパイプ
25    ];
26
27    $pipes = []; // パイプのファイルポインタを格納する配列
28
29    // proc_open を使って外部コマンドを実行
30    // 第1引数: 実行するコマンド (string または string[]。ここでは string を想定)
31    // 第2引数: descriptor_spec (ファイルディスクリプタの設定)
32    // 第3引数: pipes (パイプのファイルポインタが格納される配列、参照渡し)
33    // 第4引数: cwd (子プロセスのカレントワーキングディレクトリ)
34    // 第5引数: env_vars (子プロセスの環境変数)
35    $process = proc_open($command, $descriptorSpec, $pipes, $cwd, $env_vars);
36
37    $stdout = '';
38    $stderr = '';
39    $exitCode = -1; // デフォルトのエラーコード。proc_open が失敗した場合など。
40
41    if (is_resource($process)) {
42        // stdinパイプは使わない場合が多いため、すぐに閉じても良い
43        // 必要であればfwrite($pipes[0], 'input data'); などで書き込む
44        fclose($pipes[0]);
45
46        // stdout と stderr から出力を読み取る
47        // stream_get_contents はストリームの最後まで読み込む。
48        // これをしないと、子プロセスがバッファフルでブロックされ、デッドロック状態になる場合がある。
49        $stdout = stream_get_contents($pipes[1]);
50        $stderr = stream_get_contents($pipes[2]);
51
52        // 出力パイプを閉じる
53        fclose($pipes[1]);
54        fclose($pipes[2]);
55
56        // プロセスが終了するのを待ち、終了コードを取得する。
57        // proc_close はプロセスリソースを解放し、終了コードを返す。
58        $exitCode = proc_close($process);
59    } else {
60        // proc_open が失敗した場合
61        $stderr = "Error: Failed to open process for command: '{$command}'";
62    }
63
64    return [
65        'stdout' => $stdout,
66        'stderr' => $stderr,
67        'exitCode' => $exitCode,
68    ];
69}
70
71// --- サンプルコードの利用例 ---
72
73// 1. 正常に実行されるコマンドの例 (例: ディレクトリの内容をリスト表示)
74echo "--- Running 'ls -l' (or 'dir' on Windows) command ---\n";
75$resultLs = runExternalCommand(PHP_OS_FAMILY === 'Windows' ? 'dir' : 'ls -l');
76echo "STDOUT:\n{$resultLs['stdout']}";
77echo "STDERR:\n{$resultLs['stderr']}";
78echo "Exit Code: {$resultLs['exitCode']}\n\n";
79
80// 2. 存在しないコマンドの例 (stderr にエラーが出力されることを確認)
81echo "--- Running 'non_existent_command' ---\n";
82$resultError = runExternalCommand('non_existent_command');
83echo "STDOUT:\n{$resultError['stdout']}";
84echo "STDERR:\n{$resultError['stderr']}";
85echo "Exit Code: {$resultError['exitCode']}\n\n";
86
87// 3. カレントワーキングディレクトリを指定する例
88echo "--- Running command in a specific directory ---\n";
89// 例: Linux/macOSの場合 '/tmp' ディレクトリのコンテンツをリスト
90// Windowsの場合 'C:\Windows' ディレクトリのコンテンツをリスト
91$targetDir = (PHP_OS_FAMILY === 'Windows') ? 'C:\\Windows' : '/tmp';
92$commandCwd = (PHP_OS_FAMILY === 'Windows') ? 'dir' : 'ls -l';
93$resultCwd = runExternalCommand($commandCwd, $targetDir);
94echo "Target Directory: {$targetDir}\n";
95echo "STDOUT:\n{$resultCwd['stdout']}";
96echo "STDERR:\n{$resultCwd['stderr']}";
97echo "Exit Code: {$resultCwd['exitCode']}\n\n";
98
99// 4. 環境変数を指定する例 (例: 'env' または 'set' コマンドでカスタム変数を表示)
100echo "--- Running command with custom environment variables ---\n";
101$envVars = ['MY_CUSTOM_VAR' => 'Hello From PHP Process', 'ANOTHER_VAR' => 'Value2'];
102$commandEnv = (PHP_OS_FAMILY === 'Windows') ? 'set' : 'env'; // Windowsでは'set'、それ以外では'env'
103$resultEnv = runExternalCommand($commandEnv, null, $envVars);
104echo "STDOUT:\n{$resultEnv['stdout']}";
105echo "STDERR:\n{$resultEnv['stderr']}";
106echo "Exit Code: {$resultEnv['exitCode']}\n\n";
107
108?>

proc_openは、PHPから外部コマンドを実行し、そのプロセスを詳細に制御するための重要な関数です。この関数は、外部コマンドの標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)をPHPスクリプトとパイプで接続し、プロセスを起動します。

引数としては、$commandで実行するコマンドを指定します。$descriptor_spec配列で各入出力ストリームの接続方法(例:パイプとして扱うか、ファイルにリダイレクトするか)を定義し、&$pipesには定義されたパイプのファイルポインタが参照渡しで格納されます。これにより、PHPスクリプトから子プロセスへのデータ書き込みや、子プロセスからのデータ読み込みが可能になります。また、$cwdで子プロセスのカレントワーキングディレクトリ、$env_varsで子プロセスに引き渡す環境変数を指定できます。成功するとプロセスリソース、失敗するとfalseを返します。

サンプルコードでは、proc_openを使って外部コマンドを実行し、その標準出力と標準エラー出力の内容、および終了コードを安全に取得する方法を示しています。特に、「proc_open not working」の一般的な原因となるパイプからの出力読み取り漏れや、proc_closeによるプロセスリソースの解放忘れを防ぐための対策が盛り込まれています。stream_get_contents関数でパイプの出力を最後まで読み込むことで、子プロセスのブロック(デッドロック)を回避し、その後proc_closeを呼び出すことで、プロセスを適切に終了させて終了コードを取得し、関連するリソースを確実に解放しています。これにより、外部コマンドの実行結果を正確に捕捉し、リソースリークを防止できます。

このサンプルコードは、proc_open利用時の主要な注意点である出力の読み取り漏れとリソース解放忘れに対応しています。外部コマンドの標準出力と標準エラー出力をstream_get_contentsで最後まで読み取り、その後fcloseでパイプを閉じることが、子プロセスがブロックされる(デッドロック)のを防ぐ上で非常に重要です。また、proc_closeを呼び出してプロセスリソースを確実に解放し、終了コードを取得することも忘れてはなりません。外部コマンドの実行においては、セキュリティ確保のため、$command引数にユーザー入力などを直接含めず、常に適切にエスケープ処理やバリデーションを行ってください。proc_openが失敗する可能性もあるため、戻り値がリソースであるかの確認(is_resource)を行い、エラーハンドリングを実装することが堅牢なコードの作成につながります。

関連コンテンツ

関連IT用語

関連プログラミング言語