#はじめに
Laravel は、Symfony Process コンポーネント をラップした表現力豊かでシンプルな API を提供し、Laravel アプリケーションから外部プロセスを便利に呼び出せます。Laravel のプロセス機能は、最も一般的なユースケースに焦点を当て、優れた開発者体験を提供します。
#プロセスの呼び出し
プロセスを呼び出すには、Process ファサードの run と start メソッドを使います。run メソッドはプロセスを呼び出して終了まで待機し、start メソッドは非同期実行に使います。このドキュメントでは両方の方法を説明します。まずは基本的な同期プロセスの呼び出しと結果の確認方法を見てみましょう:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
return $result->output();
もちろん、run メソッドが返す Illuminate\Contracts\Process\ProcessResult インスタンスには、プロセス結果を確認するための便利なメソッドが多数用意されています:
$result = Process::run('ls -la');
$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();
#例外のスロー
プロセス結果があり、終了コードがゼロより大きい(失敗を示す)場合に Illuminate\Process\Exceptions\ProcessFailedException をスローしたい場合は、throw と throwIf メソッドを使えます。プロセスが失敗しなければ、プロセス結果インスタンスが返されます:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
#プロセスオプション
もちろん、プロセスを呼び出す前に動作をカスタマイズしたい場合があります。Laravel では作業ディレクトリ、タイムアウト、環境変数など、さまざまなプロセス機能を調整できます。
#作業ディレクトリのパス
path メソッドでプロセスの作業ディレクトリを指定できます。このメソッドを呼ばない場合、現在実行中の PHP スクリプトの作業ディレクトリを継承します:
$result = Process::path(__DIR__)->run('ls -la');
#入力
input メソッドを使って、プロセスの標準入力にデータを渡せます:
$result = Process::input('Hello World')->run('cat');
#タイムアウト
デフォルトでは、プロセスは 60 秒以上実行すると Illuminate\Process\Exceptions\ProcessTimedOutException をスローします。ただし、timeout メソッドでこの動作をカスタマイズできます:
$result = Process::timeout(120)->run('bash import.sh');
また、タイムアウトを完全に無効化したい場合は、forever メソッドを呼び出せます:
$result = Process::forever()->run('bash import.sh');
idleTimeout メソッドは、プロセスが出力を返さずに実行できる最大秒数を指定します:
$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
#環境変数
env メソッドでプロセスに環境変数を渡せます。呼び出されたプロセスはシステムで定義された環境変数も継承します:
$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');
継承された環境変数をプロセスから除外したい場合は、その環境変数に false を指定してください:
$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');
#TTY モード
tty メソッドでプロセスの TTY モードを有効にできます。TTY モードはプロセスの入出力をプログラムの入出力に接続し、Vim や Nano のようなエディタをプロセスとして開けるようにします:
Process::forever()->tty()->run('vim');
#プロセス出力
前述の通り、プロセス出力はプロセス結果の output(標準出力)と errorOutput(標準エラー出力)メソッドで取得できます:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();
また、run メソッドの第2引数にクロージャを渡すことで、リアルタイムに出力を取得できます。クロージャは出力の「タイプ」(stdout または stderr)と出力文字列の2つの引数を受け取ります:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});
Laravel は seeInOutput と seeInErrorOutput メソッドも提供しており、プロセス出力に特定の文字列が含まれているか簡単に判定できます:
if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}
#プロセス出力の無効化
プロセスが大量の出力を生成し、出力を必要としない場合は、メモリ節約のために出力取得を完全に無効化できます。これにはプロセス構築時に quietly メソッドを呼び出します:
use Illuminate\Support\Facades\Process;
$result = Process::quietly()->run('bash import.sh');
#パイプライン
あるプロセスの出力を別のプロセスの入力にしたい場合があります。これを「パイプ」と呼びます。Process ファサードの pipe メソッドを使うと簡単に実現できます。pipe メソッドはパイプされたプロセスを同期的に実行し、パイプラインの最後のプロセス結果を返します:
use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Process;
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
});
if ($result->successful()) {
// ...
}
パイプラインを構成する個々のプロセスをカスタマイズする必要がなければ、コマンド文字列の配列を pipe メソッドに渡すだけで済みます:
$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);
pipe メソッドの第2引数にクロージャを渡すと、リアルタイムにプロセス出力を取得できます。クロージャは出力の「タイプ」(stdout または stderr)と出力文字列の2つの引数を受け取ります:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});
Laravel はパイプライン内の各プロセスに文字列キーを割り当てるための as メソッドも提供します。このキーは pipe メソッドに渡す出力クロージャにも渡され、どのプロセスの出力か判別できます:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->as('first')->command('cat example.txt');
$pipe->as('second')->command('grep -i "laravel"');
})->start(function (string $type, string $output, string $key) {
// ...
});
#非同期プロセス
run メソッドは同期的にプロセスを呼び出しますが、start メソッドは非同期にプロセスを呼び出せます。これにより、プロセスがバックグラウンドで実行されている間に他の処理を続けられます。呼び出した後は running メソッドでプロセスがまだ実行中か確認できます:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
// ...
}
$result = $process->wait();
ご覧の通り、wait メソッドを呼ぶとプロセスの終了まで待機し、プロセス結果インスタンスを取得できます:
$process = Process::timeout(120)->start('bash import.sh');
// ...
$result = $process->wait();
#プロセスIDとシグナル
id メソッドで実行中プロセスの OS が割り当てたプロセスIDを取得できます:
$process = Process::start('bash import.sh');
return $process->id();
signal メソッドで実行中プロセスに「シグナル」を送れます。定義済みのシグナル定数は PHP ドキュメント にあります:
$process->signal(SIGUSR2);
#非同期プロセスの出力
非同期プロセス実行中は、output と errorOutput メソッドで現在の全出力を取得できますが、latestOutput と latestErrorOutput を使うと、最後に取得してからの差分出力だけを取得できます:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
sleep(1);
}
run メソッド同様、start メソッドの第2引数にクロージャを渡すことで非同期プロセスの出力をリアルタイムに取得できます。クロージャは出力の「タイプ」(stdout または stderr)と出力文字列の2つの引数を受け取ります:
$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
$result = $process->wait();
#同時実行プロセス
Laravel は同時に複数の非同期プロセスを管理するプールも簡単に扱え、多数のタスクを同時に実行できます。まず pool メソッドを呼び、Illuminate\Process\Pool インスタンスを受け取るクロージャを渡します。
このクロージャ内でプールに属するプロセスを定義します。start メソッドでプールを開始した後は、running メソッドで実行中のプロセスの コレクション にアクセスできます:
use Illuminate\Process\Pool;
use Illuminate\Support\Facades\Process;
$pool = Process::pool(function (Pool $pool) {
$pool->path(__DIR__)->command('bash import-1.sh');
$pool->path(__DIR__)->command('bash import-2.sh');
$pool->path(__DIR__)->command('bash import-3.sh');
})->start(function (string $type, string $output, int $key) {
// ...
});
while ($pool->running()->isNotEmpty()) {
// ...
}
$results = $pool->wait();
ご覧の通り、wait メソッドでプール内のすべてのプロセスの終了を待ち、結果を取得できます。wait は配列アクセス可能なオブジェクトを返し、キーで各プロセス結果インスタンスにアクセスできます:
$results = $pool->wait();
echo $results[0]->output();
また、利便性のために concurrently メソッドを使うと、非同期プロセスプールを開始してすぐに結果を待機できます。PHP の配列分解構文と組み合わせると特に表現力豊かです:
[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('ls -la');
$pool->path(app_path())->command('ls -la');
$pool->path(storage_path())->command('ls -la');
});
echo $first->output();
#プールプロセスの名前付け
数値キーでプール結果にアクセスするのは分かりにくいため、Laravel は as メソッドでプール内の各プロセスに文字列キーを割り当てられます。このキーは start メソッドに渡すクロージャにも渡され、どのプロセスの出力か判別できます:
$pool = Process::pool(function (Pool $pool) {
$pool->as('first')->command('bash import-1.sh');
$pool->as('second')->command('bash import-2.sh');
$pool->as('third')->command('bash import-3.sh');
})->start(function (string $type, string $output, string $key) {
// ...
});
$results = $pool->wait();
return $results['first']->output();
#プールプロセスのIDとシグナル
プールの running メソッドはプール内のすべての呼び出されたプロセスのコレクションを返すため、基盤となるプールプロセスIDに簡単にアクセスできます:
$processIds = $pool->running()->each->id();
また、利便性のためにプールに対して signal メソッドを呼ぶと、プール内のすべてのプロセスにシグナルを送れます:
$pool->signal(SIGUSR2);
#テスト
多くの Laravel サービスはテストを簡単かつ表現力豊かに書ける機能を提供しており、プロセスサービスも例外ではありません。Process ファサードの fake メソッドで、プロセス呼び出し時にスタブ/ダミー結果を返すよう Laravel に指示できます。
#プロセスのフェイク
Laravel のプロセスフェイク機能を試すために、プロセスを呼び出すルートを想定しましょう:
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
Process::run('bash import.sh');
return 'Import complete!';
});
このルートをテストする際、Process ファサードの fake メソッドを引数なしで呼ぶと、呼び出されたすべてのプロセスに対してフェイクの成功結果を返すよう指示できます。さらに、特定のプロセスが「実行された」ことを アサート できます:
<?php
namespace Tests\Feature;
use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_process_is_invoked(): void
{
Process::fake();
$response = $this->get('/import');
// シンプルなプロセスアサーション...
Process::assertRan('bash import.sh');
// または、プロセス設定の検査...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}
前述の通り、Process ファサードの fake メソッドを呼ぶと、常に成功したプロセス結果(出力なし)を返すよう Laravel に指示します。ただし、Process ファサードの result メソッドでフェイクプロセスの出力や終了コードを簡単に指定できます:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
#特定プロセスのフェイク
前の例で示したように、Process ファサードの fake メソッドに配列を渡すと、プロセスごとに異なるフェイク結果を指定できます。
配列のキーはフェイクしたいコマンドパターン、値は対応する結果です。* はワイルドカードとして使えます。フェイクされていないプロセスコマンドは実際に呼び出されます。Process ファサードの result メソッドでこれらのコマンドのスタブ/フェイク結果を作成できます:
Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);
フェイクプロセスの終了コードやエラー出力をカスタマイズしない場合は、単純な文字列でフェイク結果を指定する方が便利です:
Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);
#プロセスシーケンスのフェイク
テスト対象のコードが同じコマンドで複数のプロセスを呼び出す場合、呼び出しごとに異なるフェイク結果を割り当てたいことがあります。Process ファサードの sequence メソッドで実現できます:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
#非同期プロセスのライフサイクルのフェイク
これまで主に run メソッドで同期的に呼び出すプロセスのフェイクを説明しましたが、start で呼び出す非同期プロセスをテストする場合は、より高度なフェイク記述が必要です。
例えば、以下のような非同期プロセスとやり取りするルートを想定しましょう:
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
$process = Process::start('bash import.sh');
while ($process->running()) {
Log::info($process->latestOutput());
Log::info($process->latestErrorOutput());
}
return 'Done';
});
このプロセスを正しくフェイクするには、running メソッドが何回 true を返すか指定できる必要があります。また、複数行の出力を順に返すことも指定したいでしょう。これには Process ファサードの describe メソッドを使います:
Process::fake([
'bash import.sh' => Process::describe()
->output('First line of standard output')
->errorOutput('First line of error output')
->output('Second line of standard output')
->exitCode(0)
->iterations(3),
]);
上記例を詳しく見てみましょう。output と errorOutput メソッドで順に返す複数行の出力を指定できます。exitCode メソッドでフェイクプロセスの最終終了コードを指定し、iterations メソッドで running が true を返す回数を指定します。
#利用可能なアサーション
前述 の通り、Laravel は機能テスト用にいくつかのプロセスアサーションを提供します。以下でそれぞれ説明します。
#assertRan
指定したプロセスが呼び出されたことをアサートします:
use Illuminate\Support\Facades\Process;
Process::assertRan('ls -la');
assertRan メソッドはクロージャも受け取り、プロセスインスタンスとプロセス結果を受け取って設定を検査できます。このクロージャが true を返すとアサーションは「成功」します:
Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);
assertRan に渡される $process は Illuminate\Process\PendingProcess のインスタンスで、$result は Illuminate\Contracts\Process\ProcessResult のインスタンスです。
#assertDidntRun
指定したプロセスが呼び出されなかったことをアサートします:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');
assertRan メソッドと同様に、assertDidntRun メソッドもクロージャを受け取ります。クロージャにはプロセスのインスタンスとプロセスの結果が渡され、プロセスに設定されたオプションを検査できます。もしこのクロージャが true を返した場合、アサーションは「失敗」します:
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);
#assertRanTimes
指定したプロセスが指定回数呼び出されたことをアサートします:
use Illuminate\Support\Facades\Process;
Process::assertRanTimes('ls -la', times: 3);
assertRanTimes もクロージャを受け取り、プロセスと結果を検査できます。このクロージャが true を返し、指定回数呼び出されていればアサーションは「成功」します:
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
#迷子プロセスの防止
テスト中に呼び出されたすべてのプロセスがフェイクされていることを保証したい場合は、preventStrayProcesses メソッドを呼びます。このメソッドを呼んだ後は、対応するフェイク結果がないプロセス呼び出しは例外をスローし、実際のプロセスは開始されません:
use Illuminate\Support\Facades\Process;
Process::preventStrayProcesses();
Process::fake([
'ls *' => 'Test output...',
]);
// フェイクレスポンスが返される...
Process::run('ls -la');
// 例外がスローされる...
Process::run('bash import.sh');