【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 * @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 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で忘れずに解放し、リソースリークを防ぎましょう。

関連コンテンツ