【PHP8.x】popen関数の使い方

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

作成日: 更新日:

基本的な使い方

popen関数は、指定された外部コマンドを実行し、そのプロセスとの間でデータのやり取りを行うためのファイルポインタを開く関数です。この関数を利用することで、PHPスクリプトから外部プログラムの標準入力や標準出力に対して、まるでファイルのように読み書きできるようになります。

第一引数には実行したい外部コマンドを文字列で指定します。例えば、ls -lのようにシェルコマンドを記述します。第二引数には、外部プロセスとの通信モードを文字列で指定します。rを指定すると、外部コマンドの標準出力をPHPスクリプトから読み取ることが可能になり、wを指定すると、PHPスクリプトから外部コマンドの標準入力へデータを書き込むことが可能になります。

関数が成功した場合、外部プロセスへのパイプを表すファイルポインタ(リソース)を返します。このファイルポインタに対してfread()fwrite()などの通常のファイル操作関数を使用することで、外部コマンドとの間でデータを送受信できます。もし外部コマンドの実行に失敗したり、パイプを開けなかったりした場合は、falseを返します。

popen関数で開いたファイルポインタは、処理が終わった後に必ずpclose()関数を使って閉じる必要があります。また、外部コマンドの実行は、悪意のある入力によって意図しないコマンドが実行される「シェルインジェクション」といったセキュリティリスクを伴うため、信頼できないユーザーからの入力値をコマンドに直接含める際は、十分な検証と対策を行うことが重要です。

構文(syntax)

1popen(string $command, string $mode): resource|false

引数(parameters)

string $command, string $mode

  • string $command: 実行するコマンドを指定する文字列
  • string $mode: ファイルポインタのモードを指定する文字列('r':読み取り、'w':書き込み)

戻り値(return)

resource|false

指定されたコマンドを実行し、そのプロセスへのファイルポインタを返します。コマンドの実行に失敗した場合は false を返します。

サンプルコード

PHP popenで非同期コマンド実行

1<?php
2
3/**
4 * Executes a shell command using popen in a non-blocking (asynchronous) manner.
5 *
6 * This function starts a command as a child process and reads its output
7 * without blocking the main script's execution, allowing other tasks to run concurrently.
8 *
9 * @param string $command The shell command to execute.
10 * @return string The collected output from the command, or an empty string if an error occurs.
11 */
12function runNonBlockingCommand(string $command): string
13{
14    // popen() を使用してコマンドを実行し、子プロセスとのパイプを開きます。
15    // 'r' モードは、子プロセスの標準出力を親プロセスが読み取ることを可能にします。
16    $pipe = popen($command, 'r');
17    if ($pipe === false) {
18        error_log("Failed to open pipe for command: " . $command);
19        return '';
20    }
21
22    // パイプを非ブロッキングモードに設定します。
23    // これにより、fgets() などの読み取り関数はデータが利用可能になるまで待機せず、
24    // すぐに制御を戻すようになります。
25    stream_set_blocking($pipe, false);
26
27    $output = '';
28    $startTime = microtime(true);
29    $timeout = 10; // コマンドの最大実行時間 (秒) を設定します。
30
31    // パイプがクローズされるまでループを続けます。
32    while (!feof($pipe)) {
33        $read = [$pipe]; // 読み取りを監視するストリームの配列。
34        $write = null;   // 書き込みを監視するストリーム (今回は使用しません)。
35        $except = null;  // 例外を監視するストリーム (今回は使用しません)。
36        $tv_sec = 1;     // stream_select の待機時間 (秒)。この間、メインスクリプトは他の処理を実行できます。
37
38        // stream_select() は、指定されたストリームが読み取り可能になるまで待機します。
39        // 非ブロッキングモードと組み合わせることで、メインスクリプトは他の処理を短い間隔で実行できます。
40        $numChangedStreams = stream_select($read, $write, $except, $tv_sec);
41
42        if ($numChangedStreams === false) {
43            // stream_select でエラーが発生した場合。
44            error_log("stream_select failed.");
45            break;
46        } elseif ($numChangedStreams > 0) {
47            // パイプが読み取り可能になった場合。
48            // fgets() で利用可能なデータをすべて読み取ります。
49            while (($buffer = fgets($pipe, 4096)) !== false) {
50                $output .= $buffer;
51            }
52        }
53
54        // コマンド実行のタイムアウトをチェックします。
55        if (microtime(true) - $startTime > $timeout) {
56            error_log("Command timed out after {$timeout} seconds: " . $command);
57            break;
58        }
59    }
60
61    // パイプを閉じ、子プロセスを終了させます。
62    pclose($pipe);
63
64    return $output;
65}
66
67// --- 使用例 ---
68echo "--- 非同期コマンド実行の開始 ---\n";
69
70// 例として、3秒間隔で3行を出力するPHPスクリプトをバックグラウンドで実行します。
71// flush() は、子プロセスの出力を即座にパイプに送るために重要です。
72$longRunningCommand = 'php -r "
73    for ($i = 0; $i < 3; $i++) {
74        echo \'Line \' . ($i + 1) . \' from child process\\n\';
75        sleep(1); // 1秒待機
76        flush();  // 出力を強制的にフラッシュ
77    }
78    echo \'Child process finished.\\n\';
79"';
80
81// 非同期関数を呼び出します。
82$result = runNonBlockingCommand($longRunningCommand);
83
84echo "メインスクリプトは続行しています...\n";
85// メインスクリプトは、子プロセスがバックグラウンドで実行されている間に、
86// ここで他の処理を実行できます。
87sleep(2); // 例として、メインスクリプトで2秒待機します。
88
89echo "--- コマンド出力の受信 ---\n";
90echo $result;
91echo "--- 非同期コマンド実行の終了 ---\n";
92
93?>

PHPのpopen関数は、指定されたシェルコマンド($command)を子プロセスとして実行し、その子プロセスとの間でデータのやり取りを行うための「パイプ」を開きます。このパイプは、親プロセスが子プロセスの出力(標準出力)を読み取ったり、入力(標準入力)を子プロセスに書き込んだりするために使用されます。$mode引数には、パイプを読み取り用('r')または書き込み用('w')として開くかを指定します。関数が成功すると、パイプを表すresource型のリソースを返し、失敗した場合はfalseを返します。

提供されたサンプルコードでは、popenを利用してシェルコマンドを「非同期」で実行する仕組みを構築しています。通常、popenで開いたパイプからデータを読み込む際、データが利用可能になるまでスクリプトが停止(ブロック)することがありますが、stream_set_blocking($pipe, false)でパイプを非ブロッキングモードに設定することでこれを回避します。さらに、stream_select関数を用いることで、パイプにデータが来たときだけ読み込みを行い、データがない間はメインスクリプトが他の処理を進められるようになります。これにより、時間のかかるコマンドをバックグラウンドで実行しつつ、メインスクリプトはブロックされずに動作を継続できます。コマンドの実行が完了するか、タイムアウトすると、pclose関数でパイプを閉じ、関連するリソースを解放します。この非同期処理により、ユーザー体験の向上や複数のタスクの同時進行が可能になります。

popenは外部コマンドを実行し、その出力を受け取るための関数です。子プロセスの出力がすぐ親プロセスで読み込めるよう、子プロセス側でflush()を行うと良いでしょう。stream_set_blocking(false)stream_selectを組み合わせることで、メインスクリプトを待機させずに外部コマンドの実行状況を監視し、非同期的な処理が可能です。無限ループやリソース枯渇を防ぐため、必ずタイムアウトを設定し、処理終了時にはpcloseでパイプを閉じるようにしてください。また、popenで実行するコマンドには、セキュリティ上の脆弱性がないよう、信頼できる文字列のみを使用することが重要です。

関連コンテンツ