プロセス
イントロダクション
Laravelは、Symfony Processコンポーネントをラップした、表現力豊かで最小限のAPIを提供し、Laravelアプリケーションから外部プロセスを便利に呼び出せます。Laravelのプロセス機能は、最も一般的なユースケースと素晴らしい開発者エクスペリエンスに焦点を当てています。
プロセスの起動
プロセスを呼び出すには、Processファサードが提供するrunメソッドとstartメソッドを使用します。runメソッドはプロセスを呼び出し、そのプロセスの実行が終了するのを待ちます。一方、startメソッドは非同期のプロセス実行に使用します。このドキュメントでは、両方のアプローチを検証します。まず、基本的な同期プロセスを呼び出し、その結果を検査する方法を見てみましょう。
1use Illuminate\Support\Facades\Process;2 3$result = Process::run('ls -la');4 5return $result->output();
もちろん、runメソッドが返すIlluminate\Contracts\Process\ProcessResultインスタンスは、プロセスの結果を検査するために使用できるさまざまな便利なメソッドを提供します。
1$result = Process::run('ls -la');2 3$result->successful();4$result->failed();5$result->exitCode();6$result->output();7$result->errorOutput();
例外のスロー
プロセス結果があり、終了コードが0より大きい(つまり失敗を示している)場合にIlluminate\Process\Exceptions\ProcessFailedExceptionのインスタンスをスローしたい場合は、throwメソッドとthrowIfメソッドを使用できます。プロセスが失敗しなかった場合、プロセス結果のインスタンスが返されます。
1$result = Process::run('ls -la')->throw();2 3$result = Process::run('ls -la')->throwIf($condition);
プロセスオプション
もちろん、プロセスを呼び出す前にその動作をカスタマイズする必要がある場合もあります。ありがたいことに、Laravelでは作業ディレクトリ、タイムアウト、環境変数など、さまざまなプロセス機能を調整できます。
ワーキングディレクトリパス
pathメソッドを使用して、プロセスのワーキングディレクトリを指定できます。このメソッドを呼び出さない場合、プロセスは現在実行中のPHPスクリプトのワーキングディレクトリを継承します。
1$result = Process::path(__DIR__)->run('ls -la');
入力
プロセスの「標準入力」を介して入力を提供するには、inputメソッドを使用します。
1$result = Process::input('Hello World')->run('cat');
タイムアウト
デフォルトでは、プロセスは60秒以上実行されるとIlluminate\Process\Exceptions\ProcessTimedOutExceptionのインスタンスをスローします。しかし、この動作はtimeoutメソッドを介してカスタマイズできます。
1$result = Process::timeout(120)->run('bash import.sh');
あるいは、プロセスのタイムアウトを完全に無効にしたい場合は、foreverメソッドを呼び出せます。
1$result = Process::forever()->run('bash import.sh');
idleTimeoutメソッドは、プロセスが出力を返さずに実行できる最大秒数を指定するために使用します。
1$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
環境変数
環境変数はenvメソッドを介してプロセスに提供できます。呼び出されたプロセスは、システムで定義されているすべての環境変数も継承します。
1$result = Process::forever()2 ->env(['IMPORT_PATH' => __DIR__])3 ->run('bash import.sh');
呼び出されたプロセスから継承された環境変数を削除したい場合は、その環境変数にfalseの値を指定します。
1$result = Process::forever()2 ->env(['LOAD_PATH' => false])3 ->run('bash import.sh');
TTYモード
ttyメソッドを使用して、プロセスのTTYモードを有効にできます。TTYモードは、プロセスの入力と出力をプログラムの入力と出力に接続し、プロセスがVimやNanoのようなエディタをプロセスとして開くことができるようにします。
1Process::forever()->tty()->run('vim');
プロセス出力
前述のように、プロセス出力はプロセス結果のoutput(stdout)メソッドとerrorOutput(stderr)メソッドを使用してアクセスできます。
1use Illuminate\Support\Facades\Process;2 3$result = Process::run('ls -la');4 5echo $result->output();6echo $result->errorOutput();
ただし、runメソッドの第2引数としてクロージャを渡すことで、出力をリアルタイムで収集することもできます。このクロージャは、出力の「タイプ」(stdoutまたはstderr)と出力文字列自体の2つの引数を受け取ります。
1$result = Process::run('ls -la', function (string $type, string $output) {2 echo $output;3});
Laravelは、seeInOutputメソッドとseeInErrorOutputメソッドも提供しています。これらは、特定の文字列がプロセスの出力に含まれていたかどうかを判断する便利な方法を提供します。
1if (Process::run('ls -la')->seeInOutput('laravel')) {2 // ...3}
プロセス出力の無効化
プロセスが興味のない大量の出力を書き込んでいる場合、出力の取得を完全に無効にすることでメモリを節約できます。これを実現するには、プロセスの構築中にquietlyメソッドを呼び出します。
1use Illuminate\Support\Facades\Process;2 3$result = Process::quietly()->run('bash import.sh');
パイプライン
あるプロセスの出力を別のプロセスの入力にしたい場合があります。これは、プロセスの出力を別のプロセスに「パイプする」とよく呼ばれます。Processファサードが提供するpipeメソッドは、これを簡単に実現します。pipeメソッドはパイプされたプロセスを同期的に実行し、パイプラインの最後のプロセスのプロセス結果を返します。
1use Illuminate\Process\Pipe; 2use Illuminate\Support\Facades\Process; 3 4$result = Process::pipe(function (Pipe $pipe) { 5 $pipe->command('cat example.txt'); 6 $pipe->command('grep -i "laravel"'); 7}); 8 9if ($result->successful()) {10 // ...11}
パイプラインを構成する個々のプロセスをカスタマイズする必要がない場合は、コマンド文字列の配列をpipeメソッドに渡すだけです。
1$result = Process::pipe([2 'cat example.txt',3 'grep -i "laravel"',4]);
pipeメソッドの第2引数としてクロージャを渡すことで、プロセス出力をリアルタイムで収集できます。このクロージャは、出力の「タイプ」(stdoutまたはstderr)と出力文字列自体の2つの引数を受け取ります。
1$result = Process::pipe(function (Pipe $pipe) {2 $pipe->command('cat example.txt');3 $pipe->command('grep -i "laravel"');4}, function (string $type, string $output) {5 echo $output;6});
Laravelでは、asメソッドを介してパイプライン内の各プロセスに文字列キーを割り当てることもできます。このキーはpipeメソッドに提供される出力クロージャにも渡され、出力がどのプロセスに属しているかを判断できます。
1$result = Process::pipe(function (Pipe $pipe) {2 $pipe->as('first')->command('cat example.txt');3 $pipe->as('second')->command('grep -i "laravel"');4})->start(function (string $type, string $output, string $key) {5 // ...6});
非同期プロセス
runメソッドがプロセスを同期的に呼び出すのに対し、startメソッドはプロセスを非同期に呼び出すために使用できます。これにより、プロセスがバックグラウンドで実行されている間、アプリケーションは他のタスクを実行し続けることができます。プロセスが呼び出されたら、runningメソッドを利用してプロセスがまだ実行中かどうかを判断できます。
1$process = Process::timeout(120)->start('bash import.sh');2 3while ($process->running()) {4 // ...5}6 7$result = $process->wait();
お気づきかもしれませんが、waitメソッドを呼び出して、プロセスが実行を終了し、プロセス結果インスタンスを取得するまで待機できます。
1$process = Process::timeout(120)->start('bash import.sh');2 3// ...4 5$result = $process->wait();
プロセスIDとシグナル
idメソッドを使用して、実行中のプロセスにオペレーティングシステムが割り当てたプロセスIDを取得できます。
1$process = Process::start('bash import.sh');2 3return $process->id();
signalメソッドを使用して、実行中のプロセスに「シグナル」を送信できます。定義済みのシグナル定数のリストは、PHPドキュメントにあります。
1$process->signal(SIGUSR2);
非同期プロセスの出力
非同期プロセスの実行中、outputメソッドとerrorOutputメソッドを使用して現在のすべての出力にアクセスできます。しかし、latestOutputメソッドとlatestErrorOutputメソッドを利用して、出力が最後に取得されてから発生したプロセスからの出力にアクセスできます。
1$process = Process::timeout(120)->start('bash import.sh');2 3while ($process->running()) {4 echo $process->latestOutput();5 echo $process->latestErrorOutput();6 7 sleep(1);8}
runメソッドと同様に、startメソッドの第2引数としてクロージャを渡すことで、非同期プロセスからリアルタイムで出力を収集することもできます。このクロージャは、出力の「タイプ」(stdoutまたはstderr)と出力文字列自体の2つの引数を受け取ります。
1$process = Process::start('bash import.sh', function (string $type, string $output) {2 echo $output;3});4 5$result = $process->wait();
プロセスが終了するまで待つ代わりに、waitUntilメソッドを使用してプロセスの出力に基づいて待機を停止できます。waitUntilメソッドに渡されたクロージャがtrueを返すと、Laravelはプロセスの終了を待つのをやめます。
1$process = Process::start('bash import.sh');2 3$process->waitUntil(function (string $type, string $output) {4 return $output === 'Ready...';5});
並行プロセス
Laravelは、並行非同期プロセスのプール管理も簡単に行えるようにし、多くのタスクを同時に簡単に実行できるようにします。開始するには、poolメソッドを呼び出します。これは、Illuminate\Process\Poolのインスタンスを受け取るクロージャを引数に取ります。
このクロージャ内で、プールに属するプロセスを定義できます。プロセスプールがstartメソッドを介して開始されると、runningメソッドを介して実行中のプロセスのコレクションにアクセスできます。
1use Illuminate\Process\Pool; 2use Illuminate\Support\Facades\Process; 3 4$pool = Process::pool(function (Pool $pool) { 5 $pool->path(__DIR__)->command('bash import-1.sh'); 6 $pool->path(__DIR__)->command('bash import-2.sh'); 7 $pool->path(__DIR__)->command('bash import-3.sh'); 8})->start(function (string $type, string $output, int $key) { 9 // ...10});11 12while ($pool->running()->isNotEmpty()) {13 // ...14}15 16$results = $pool->wait();
ご覧のとおり、waitメソッドを介して、すべてのプールプロセスが実行を終了し、その結果が解決されるまで待機できます。waitメソッドは配列アクセス可能なオブジェクトを返し、プールの各プロセスのプロセス結果インスタンスにキーでアクセスできます。
1$results = $pool->wait();2 3echo $results[0]->output();
または、便宜上、concurrentlyメソッドを使用して、非同期プロセスプールを開始し、すぐにその結果を待つこともできます。これは、PHPの配列デストラクチャリング機能と組み合わせると、特に表現力豊かな構文を提供できます。
1[$first, $second, $third] = Process::concurrently(function (Pool $pool) {2 $pool->path(__DIR__)->command('ls -la');3 $pool->path(app_path())->command('ls -la');4 $pool->path(storage_path())->command('ls -la');5});6 7echo $first->output();
プールプロセスの命名
数値キーを介してプロセスプールの結果にアクセスするのはあまり表現力豊かではありません。そのため、Laravelではasメソッドを介してプール内の各プロセスに文字列キーを割り当てることができます。このキーはstartメソッドに提供されるクロージャにも渡され、出力がどのプロセスに属しているかを判断できます。
1$pool = Process::pool(function (Pool $pool) { 2 $pool->as('first')->command('bash import-1.sh'); 3 $pool->as('second')->command('bash import-2.sh'); 4 $pool->as('third')->command('bash import-3.sh'); 5})->start(function (string $type, string $output, string $key) { 6 // ... 7}); 8 9$results = $pool->wait();10 11return $results['first']->output();
プールプロセスのIDとシグナル
プロセスプールのrunningメソッドは、プール内で呼び出されたすべてのプロセスのコレクションを提供するため、基になるプールプロセスのIDに簡単にアクセスできます。
1$processIds = $pool->running()->each->id();
そして、便宜上、プロセスプールでsignalメソッドを呼び出して、プール内のすべてのプロセスにシグナルを送信できます。
1$pool->signal(SIGUSR2);
テスト
多くのLaravelサービスは、テストを簡単かつ表現力豊かに記述するのに役立つ機能を提供しており、Laravelのプロセスサービスも例外ではありません。Processファサードのfakeメソッドを使用すると、プロセスが呼び出されたときにスタブ化/ダミーの結果を返すようにLaravelに指示できます。
プロセスの偽装
Laravelのプロセス偽装機能を詳しく調べるために、プロセスを呼び出すルートを想像してみましょう。
1use Illuminate\Support\Facades\Process;2use Illuminate\Support\Facades\Route;3 4Route::get('/import', function () {5 Process::run('bash import.sh');6 7 return 'Import complete!';8});
このルートをテストするとき、Processファサードのfakeメソッドを引数なしで呼び出すことにより、呼び出されたすべてのプロセスに対して偽の成功したプロセス結果を返すようにLaravelに指示できます。さらに、特定のプロセスが「実行された」ことをアサートすることもできます。
1<?php 2 3use Illuminate\Process\PendingProcess; 4use Illuminate\Contracts\Process\ProcessResult; 5use Illuminate\Support\Facades\Process; 6 7test('process is invoked', function () { 8 Process::fake(); 9 10 $response = $this->get('/import');11 12 // Simple process assertion...13 Process::assertRan('bash import.sh');14 15 // Or, inspecting the process configuration...16 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {17 return $process->command === 'bash import.sh' &&18 $process->timeout === 60;19 });20});
1<?php 2 3namespace Tests\Feature; 4 5use Illuminate\Process\PendingProcess; 6use Illuminate\Contracts\Process\ProcessResult; 7use Illuminate\Support\Facades\Process; 8use Tests\TestCase; 9 10class ExampleTest extends TestCase11{12 public function test_process_is_invoked(): void13 {14 Process::fake();15 16 $response = $this->get('/import');17 18 // Simple process assertion...19 Process::assertRan('bash import.sh');20 21 // Or, inspecting the process configuration...22 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {23 return $process->command === 'bash import.sh' &&24 $process->timeout === 60;25 });26 }27}
説明したように、Processファサードのfakeメソッドを呼び出すと、Laravelは常に出力なしで成功したプロセス結果を返すように指示されます。しかし、Processファサードのresultメソッドを使用して、偽装プロセスの出力と終了コードを簡単に指定できます。
1Process::fake([2 '*' => Process::result(3 output: 'Test output',4 errorOutput: 'Test error output',5 exitCode: 1,6 ),7]);
特定プロセスの偽装
前の例で見たように、Processファサードでは、fakeメソッドに配列を渡すことで、プロセスごとに異なる偽の結果を指定できます。
配列のキーは、偽装したいコマンドパターンとそれに関連する結果を表す必要があります。*文字はワイルドカード文字として使用できます。偽装されていないプロセスコマンドは実際に呼び出されます。Processファサードのresultメソッドを使用して、これらのコマンドのスタブ/偽の結果を構築できます。
1Process::fake([2 'cat *' => Process::result(3 output: 'Test "cat" output',4 ),5 'ls *' => Process::result(6 output: 'Test "ls" output',7 ),8]);
偽装プロセスの終了コードやエラー出力をカスタマイズする必要がない場合は、偽装プロセスの結果を単純な文字列として指定する方が便利な場合があります。
1Process::fake([2 'cat *' => 'Test "cat" output',3 'ls *' => 'Test "ls" output',4]);
プロセスシーケンスの偽装
テスト対象のコードが同じコマンドで複数のプロセスを呼び出す場合、各プロセスの呼び出しに異なる偽のプロセス結果を割り当てたい場合があります。これは、Processファサードのsequenceメソッドを介して実現できます。
1Process::fake([2 'ls *' => Process::sequence()3 ->push(Process::result('First invocation'))4 ->push(Process::result('Second invocation')),5]);
非同期プロセスのライフサイクルの偽装
これまで、主にrunメソッドを使用して同期的に呼び出されるプロセスの偽装について説明してきました。しかし、startを介して呼び出される非同期プロセスと対話するコードをテストしようとしている場合は、偽のプロセスを記述するためにより洗練されたアプローチが必要になる場合があります。
たとえば、非同期プロセスと対話する次のルートを想像してみましょう。
1use Illuminate\Support\Facades\Log; 2use Illuminate\Support\Facades\Route; 3 4Route::get('/import', function () { 5 $process = Process::start('bash import.sh'); 6 7 while ($process->running()) { 8 Log::info($process->latestOutput()); 9 Log::info($process->latestErrorOutput());10 }11 12 return 'Done';13});
このプロセスを適切に偽装するには、runningメソッドがtrueを返す回数を記述できる必要があります。さらに、シーケンスで返されるべき複数の出力行を指定したい場合があります。これを実現するために、Processファサードのdescribeメソッドを使用できます。
1Process::fake([2 'bash import.sh' => Process::describe()3 ->output('First line of standard output')4 ->errorOutput('First line of error output')5 ->output('Second line of standard output')6 ->exitCode(0)7 ->iterations(3),8]);
上の例を詳しく見てみましょう。outputメソッドとerrorOutputメソッドを使用して、シーケンスで返される複数の出力行を指定できます。exitCodeメソッドは、偽のプロセスの最終的な終了コードを指定するために使用します。最後に、iterationsメソッドを使用して、runningメソッドがtrueを返す回数を指定できます。
利用可能なアサーション
前述のように、Laravelは機能テスト用にいくつかのプロセスアサーションを提供しています。以下でこれらの各アサーションについて説明します。
assertRan
特定のプロセスが呼び出されたことをアサートします。
1use Illuminate\Support\Facades\Process;2 3Process::assertRan('ls -la');
assertRanメソッドはクロージャも受け入れます。これはプロセスとプロセス結果のインスタンスを受け取り、プロセスの設定オプションを検査できます。このクロージャがtrueを返した場合、アサーションは「パス」します。
1Process::assertRan(fn ($process, $result) =>2 $process->command === 'ls -la' &&3 $process->path === __DIR__ &&4 $process->timeout === 605);
assertRanクロージャに渡される$processはIlluminate\Process\PendingProcessのインスタンスであり、$resultはIlluminate\Contracts\Process\ProcessResultのインスタンスです。
assertDidntRun
特定のプロセスが呼び出されなかったことをアサートします。
1use Illuminate\Support\Facades\Process;2 3Process::assertDidntRun('ls -la');
assertRanメソッドと同様に、assertDidntRunメソッドもクロージャを受け入れます。これはプロセスとプロセス結果のインスタンスを受け取り、プロセスの設定オプションを検査できます。このクロージャがtrueを返した場合、アサーションは「失敗」します。
1Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>2 $process->command === 'ls -la'3);
assertRanTimes
特定のプロセスが指定された回数呼び出されたことをアサートします。
1use Illuminate\Support\Facades\Process;2 3Process::assertRanTimes('ls -la', times: 3);
assertRanTimesメソッドはクロージャも受け入れます。これはプロセスとプロセス結果のインスタンスを受け取り、プロセスの設定オプションを検査できます。このクロージャがtrueを返し、プロセスが指定された回数呼び出された場合、アサーションは「パス」します。
1Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {2 return $process->command === 'ls -la';3}, times: 3);
Stray(さまよい)プロセスの防止
個々のテストまたは完全なテストスイート全体で、呼び出されたすべてのプロセスが偽装されていることを確認したい場合は、preventStrayProcessesメソッドを呼び出せます。このメソッドを呼び出した後、対応する偽の結果を持たないプロセスは、実際のプロセスを開始する代わりに例外をスローします。
1use Illuminate\Support\Facades\Process; 2 3Process::preventStrayProcesses(); 4 5Process::fake([ 6 'ls *' => 'Test output...', 7]); 8 9// Fake response is returned...10Process::run('ls -la');11 12// An exception is thrown...13Process::run('bash import.sh');