メインコンテンツまでスキップ

プロセス

はじめに

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(stdout)および errorOutput(stderr)メソッドを使用してアクセスできます:

use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

echo $result->output();
echo $result->errorOutput();

ただし、run メソッドの第二引数にクロージャを渡すことで、リアルタイムで出力を収集することも可能です。クロージャは、出力の "タイプ"(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');

パイプライン

時には、1つのプロセスの出力を別のプロセスの入力にしたい場合があります。これは、プロセスの出力を別のプロセスに「パイプする」とも言われます。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引数としてクロージャを渡すことでリアルタイムで収集することができます。クロージャは2つの引数を受け取ります:出力の「タイプ」(stdoutまたはstderr)と出力文字列そのもの:

$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});

Laravelでは、パイプライン内の各プロセスに文字列キーを割り当てることもできます。このキーは、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メソッドを使用して、実行中のプロセスのオペレーティングシステムに割り当てられたプロセス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) と出力文字列そのもの:

$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});

$result = $process->wait();

並行プロセス

Laravel では、複数の並行かつ非同期プロセスを管理することが簡単に行えます。これにより、多くのタスクを簡単に同時に実行できます。開始するには、Illuminate\Process\Pool のインスタンスを受け取るクロージャを受け入れる 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 メソッドを使用して、プール内の各プロセスのプロセス結果インスタンスにアクセスできる配列アクセス可能なオブジェクトが返されます:

$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 サービスは、テストを簡単かつ表現豊かに書くのに役立つ機能を提供しており、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 メソッドを引数なしで呼び出すことで、呼び出されるプロセスごとに偽の成功したプロセス結果を返すように Laravel に指示できます。さらに、特定のプロセスが「実行」されたことを アサート することさえできます:

<?php

use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;

test('process is invoked', function () {
Process::fake();

$response = $this->get('/import');

// Simple process assertion...
Process::assertRan('bash import.sh');

// Or, inspecting the process configuration...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
});
<?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');

// Simple process assertion...
Process::assertRan('bash import.sh');

// Or, inspecting the process configuration...
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 クロージャに渡される $processIlluminate\Process\PendingProcess のインスタンスであり、$resultIlluminate\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');