Process

簡介

Laravel 為 Symfony 的 Process Component 提供了一個語意化、極簡的 API,能讓我們方便地在 Laravel 專案中呼叫外部 Process(處理程序)。Laravel 的 Process 功能著重於最常見的使用情境,並提供優秀的開發人員經驗 (Developer Experience)。

呼叫 Process

若要呼叫 Process,可以使用 Process Facade 提供的 runstart 方法。run 方法會呼叫 Process,並等待該 Process 執行完畢。而 start 方法會以非同步方式執行 Process。我們將在此文件中詳細討論這兩種方法。首先,我們先來看看如何執行一個基本的同步 Process 並取得其執行結果:

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5return $result->output();
1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5return $result->output();

當然,run 方法回傳的 Illuminate\Contracts\Console\Process\ProcessResult 實體還包含了多種可用於檢查 Process 執行結果的實用方法:

1$result = Process::run('ls -la');
2 
3$result->successful();
4$result->failed();
5$result->exitCode();
6$result->output();
7$result->errorOutput();
1$result = Process::run('ls -la');
2 
3$result->successful();
4$result->failed();
5$result->exitCode();
6$result->output();
7$result->errorOutput();

擲回 Exception

若在取得 Process 執行結果後,希望能讓終止代碼 (Exit Code) 大於 0 的狀況 (表示執行失敗) 擲回 Illuminate\Console\Process\Exceptions\ProcessFailedException,可以使用 throwthrowIf 方法。若 Process 並未執行失敗,則會回傳 Process 執行結果的實體:

1$result = Process::run('ls -la')->throw();
2 
3$result = Process::run('ls -la')->throwIf($condition);
1$result = Process::run('ls -la')->throw();
2 
3$result = Process::run('ls -la')->throwIf($condition);

Process 選項

當然,你可能會需要在呼叫 Process 前自訂該 Process 的行為。在 Laravel 中,可以調整許多 Process 的功能,例如工作目錄 (Working Directory)、逾時與環境變數等。

工作目錄路徑

可以使用 path 方法來指定 Process 的工作目錄。若未呼叫此方法,則該 Process 會繼承目前執行 PHP Script 的工作目錄:

1$result = Process::path(__DIR__)->run('ls -la');
1$result = Process::path(__DIR__)->run('ls -la');

逾時

預設情況下,Process 會在執行超過 60 秒後擲回 Illuminate\Console\Process\Exceptions\ProcessTimedOutException 實體。不過,可以使用 timeout 方法來自定此行為:

1$result = Process::timeout(120)->run('bash import.sh');
1$result = Process::timeout(120)->run('bash import.sh');

或者,若要完全禁用 Process 的逾時,可以呼叫 forever 方法:

1$result = Process::forever()->run('bash import.sh');
1$result = Process::forever()->run('bash import.sh');

idleTimeout 方法可用來指定 Process 在不回傳任何輸出下可執行的最大秒數:

1$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
1$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');

環境變數

可以使用 env 方法來提供環境變數給 Process。被呼叫的 Process 也會繼承在系統中所定義的所有環境變數:

1$result = Process::forever()
2 ->env(['IMPORT_PATH' => __DIR__])
3 ->run('bash import.sh');
1$result = Process::forever()
2 ->env(['IMPORT_PATH' => __DIR__])
3 ->run('bash import.sh');

若想從呼叫的 Process 中移除繼承的環境變數,可以提供一個值為 false 的環境變數:

1$result = Process::forever()
2 ->env(['LOAD_PATH' => false])
3 ->run('bash import.sh');
1$result = Process::forever()
2 ->env(['LOAD_PATH' => false])
3 ->run('bash import.sh');

TTY 模式

tty 方法可用來在 Process 上啟用 TTY 模式。TTY 模式會將 Process 的 Input 與 Output 連結到你的程式的 Input 與 Output,讓你的 Process 能打開如 Vim 或 Nano 之類的 Process:

1Process::forever()->tty()->run('vim');
1Process::forever()->tty()->run('vim');

Process 的輸出

剛才提到過,可以使用 output (stdout) 與 errorOutput (stderr) 方法來在 Process 結果上取得 Process 的輸出:

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5echo $result->output();
6echo $result->errorOutput();
1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5echo $result->output();
6echo $result->errorOutput();

不過,也可以在呼叫 run 方法時傳入一個 Closure 作為第二個引數來即時取得輸出。該 Closure 會收到兩個引數:輸出的「類型 (Type)」(stdoutstderr) 與輸出字串本身:

1$result = Process::run('ls -la', function (string $type, string $output) {
2 echo $output;
3});
1$result = Process::run('ls -la', function (string $type, string $output) {
2 echo $output;
3});

Laravel 也提供了 seeInOutputseeInErrorOutput 方法。通過這兩個方法,就可以方便地判斷給定的字串是否包含在該 Process 的輸出中:

1if (Process::run('ls -la')->seeInOutput('laravel')) {
2 // ...
3}
1if (Process::run('ls -la')->seeInOutput('laravel')) {
2 // ...
3}

關閉 Process 的輸出

若 Process 會寫入大量不必要的輸出,可以完全關閉取得輸出來減少記憶體使用。若要關閉取得輸出,請在建構 Process 時呼叫 quietly 方法:

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::quietly()->run('bash import.sh');
1use Illuminate\Support\Facades\Process;
2 
3$result = Process::quietly()->run('bash import.sh');

非同步的 Process

run 方法會同步呼叫 Process,而 start 方法可用來非同步地呼叫 Process。這樣一來,你的程式就可以繼續執行其他任務,並讓 Process 在背景執行。Process 被呼叫後,可以使用 running 方法來判斷該 Process 是否還在執行:

1$process = Process::timeout(120)->start('bash import.sh');
2 
3while ($process->running()) {
4 // ...
5}
6 
7$result = $process->wait();
1$process = Process::timeout(120)->start('bash import.sh');
2 
3while ($process->running()) {
4 // ...
5}
6 
7$result = $process->wait();

讀者可能已經注意到,可以通過呼叫 wait 方法來等待 Process 完成執行,然後再取得 Process 的結果實體:

1$process = Process::timeout(120)->start('bash import.sh');
2 
3// ...
4 
5$result = $process->wait();
1$process = Process::timeout(120)->start('bash import.sh');
2 
3// ...
4 
5$result = $process->wait();

Process 的 ID 與 Signal

pid 方法可用來取得正在執行的 Process 由作業系統指派的 Process ID:

1$process = Process::start('bash import.sh');
2 
3return $process->pid();
1$process = Process::start('bash import.sh');
2 
3return $process->pid();

可以使用 signal 方法來向正在執行的 Process 傳送「訊號 (Signal)」。請參考《PHP 說明文件》以瞭解預先定義的 Signal 常數列表:

1$process->signal(SIGUSR2);
1$process->signal(SIGUSR2);

非同步 Process 的輸出

當非同步 Process 正在執行時,可以使用 outputerrorOutput 方法來取得該 Process 目前的完整輸出。而 latestOutputlatestErrorOutput 可用來存取自從上一次取得輸出後該 Process 所產生的最新輸出:

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}
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 方法也可以在呼叫時傳入一個 Closure 作為第二個引數來即時取得輸出。該 Closure 會收到兩個引數:輸出的「類型 (Type)」(stdoutstderr) 與輸出字串本身:

1$process = Process::start('bash import.sh', function (string $type, string $output) {
2 echo $output;
3});
4 
5$result = $process->wait();
1$process = Process::start('bash import.sh', function (string $type, string $output) {
2 echo $output;
3});
4 
5$result = $process->wait();

併行的 Process

Laravel 也讓管理平行的、非同步的 Process 集區 (Pool) 變的非常容易,讓你能輕鬆的同步執行多個任務。若要執行非同步 Process 集區,請執行 pool 方法。請傳入一個 Closure 給 pool 方法,該 Closure 會收到 Illuminate\Console\Process\Pool 的實體。

在該 Closure 中,可以定義屬於該集區的 Process。使用 start 方法開始 Process 集區後,可以使用 running 方法來存取一組包含正在執行的 Process 的 Collection

1use Illuminate\Console\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();
1use Illuminate\Console\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 方法來等待集區 Process 完成執行並解析這些 Process 的執行結果。wait 方法會回傳一個可使用陣列存取的物件 (Array Accessible Object),讓你能使用其索引鍵來存取集區中各個 Process 的 Process 執行結果實體:

1$results = $pool->wait();
2 
3echo $results[0]->output();
1$results = $pool->wait();
2 
3echo $results[0]->output();

或者,也可以使用方便的 concurrently 方法來開始一組非同步 Process 集區,並馬上開始等待其執行結果。當與 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();
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();

命名的 Pool Process

使用數字索引鍵來存取 Process 集區並不是很有表達性。因此,Laravel 可讓你使用 as 方法來為集區中的各個 Process 指派一個字串索引鍵。該索引鍵也會傳入提供給 start 方法的 Closure,讓你能判斷輸出屬於哪個 Process:

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();
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();

Pool Process 的 ID 與 Signal

由於 Process 集區的 running 方法提供了一組包含集區中所有已呼叫 Process 的 Collection,因此你可以輕鬆地存取集區中相應的 Process ID:

1$processIds = $pool->running()->each->pid();
1$processIds = $pool->running()->each->pid();

而且,也可以在 Process 集區上使用 signal 方法來方便地傳送 Signal 給集區中的每一個 Process:

1$pool->signal(SIGUSR2);
1$pool->signal(SIGUSR2);

測試

許多 Laravel 的服務都提供了能讓你輕鬆且表達性地撰寫測試的方法,而 Laravel 的 Process 服務也不例外。使用 Process Facade 的 fake 方法,能讓你指定要 Laravel 在執行 Process 時回傳一組模擬的執行結果。

模擬 Process

若要瞭解 Laravel 中模擬 Process 的功能,我們先來想像有個 Route 會呼叫一個 Process:

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});
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});

在測試此 Route 時,我們可以不帶任何引數呼叫 Process Facade 上的 fake 方法,讓 Laravel 在每一個被呼叫的 Process 上回傳一組模擬的成功 Process 執行結果。此外,我們還可以 Assert 判斷給定的 Process 是否已執行:

1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Console\Process\PendingProcess;
6use Illuminate\Contracts\Console\Process\ProcessResult;
7use Illuminate\Support\Facades\Process;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 public function test_process_is_invoked(): void
13 {
14 Process::fake();
15 
16 $response = $this->get('/');
17 
18 // 簡單的 Process Assertion...
19 Process::assertRan('bash import.sh');
20 
21 // 或者,也可以檢查 Process 的設定...
22 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
23 return $process->command === 'bash import.sh' &&
24 $process->timeout === 60;
25 });
26 }
27}
1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Console\Process\PendingProcess;
6use Illuminate\Contracts\Console\Process\ProcessResult;
7use Illuminate\Support\Facades\Process;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 public function test_process_is_invoked(): void
13 {
14 Process::fake();
15 
16 $response = $this->get('/');
17 
18 // 簡單的 Process Assertion...
19 Process::assertRan('bash import.sh');
20 
21 // 或者,也可以檢查 Process 的設定...
22 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
23 return $process->command === 'bash import.sh' &&
24 $process->timeout === 60;
25 });
26 }
27}

剛才也提到過,在 Process Facade 上呼叫 fake 方法會讓 Laravel 為每個 Process 回傳沒有輸出的 Process 執行結果。不過,你可以使用 Process Facade 的 result 方法來輕鬆地指定模擬 Process 的輸出與結束代碼 (Exit Code):

1Process::fake([
2 '*' => Process::result(
3 output: 'Test output',
4 errorOutput: 'Test error output',
5 exitCode: 1,
6 ),
7]);
1Process::fake([
2 '*' => Process::result(
3 output: 'Test output',
4 errorOutput: 'Test error output',
5 exitCode: 1,
6 ),
7]);

模擬特定的 Process

讀者可能已經在前一個例子中注意到,通過 Process Facade,就可以通過傳入一組陣列給 fake 方法來指定各個 Process 的模擬執行結果。

陣列的索引鍵代表要模擬的指令格式,以及其相應的執行結果。可使用 * 字元來作為萬用字元。沒有被模擬的 Process 指令會被實際執行。可以使用 Process Facade 的 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 *' => Process::result(
3 output: 'Test "cat" output',
4 ),
5 'ls *' => Process::result(
6 output: 'Test "ls" output',
7 ),
8]);

若不需要自定模擬 Process 的終止代碼或錯誤輸出,那麼使用字串來指定 Process 的模擬結果可能會更方便:

1Process::fake([
2 'cat *' => 'Test "cat" output',
3 'ls *' => 'Test "ls" output',
4]);
1Process::fake([
2 'cat *' => 'Test "cat" output',
3 'ls *' => 'Test "ls" output',
4]);

模擬 Process 序列

若要測試的程式碼會以相同指令來呼叫多個 Process,則可為各個 Process 呼叫指定不同的 Process 模擬執行結果。若要為各個 Process 呼叫設定各自的執行結果,請使用 Process Facade 的 sequence 方法:

1Process::fake([
2 'ls *' => Process::sequence()
3 ->push(Process::result('First invocation'))
4 ->push(Process::result('Second invocation')),
5]);
1Process::fake([
2 'ls *' => Process::sequence()
3 ->push(Process::result('First invocation'))
4 ->push(Process::result('Second invocation')),
5]);

模擬非同步 Process 的生命週期

到目前為止,我們主要針對使用 run 方法同步呼叫的 Process 討論要如何進行模擬。不過,若要測試的程式碼中有使用 start 來非同步呼叫 Process,就需要使用更複雜的方法來模擬 Process。

舉例來說,假設有下列 Route 會觸發非同步 Process:

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});
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});

若要正確模擬此 Process,我們需要能夠描述 running 方法要回傳幾次 true。此外,我們可能還需要指定要依序回傳的多行輸出。為此,我們可以使用 Process Facade 的 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]);
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]);

讓我們來仔細看看上面的範例。使用 outputerrorOutput 方法,我們可以指定要依序回傳的多行輸出。exitCode 方法可用來指定模擬 Process 最終的終止代碼。最後,iterations 方法可用來指定 running 方法要回傳幾次 true

可用的 Assertion

就像剛才提到過的,Laravel 為功能測試 (Feature Test) 提供了多個 Process 的 Assertion(判斷提示)。我們會在接下來的部分討論這些 Assertion。

assertRan

判斷給定 Process 是否已被呼叫:

1use Illuminate\Support\Facades\Process;
2 
3Process::assertRan('ls -la');
1use Illuminate\Support\Facades\Process;
2 
3Process::assertRan('ls -la');

也可傳入一個 Closure 給 assertRun 方法。該 Closure 會收到 Process 的實體與 Process 的執行結果,讓你能檢查 Process 上的設定。若讓該 Closure 回傳 true,則該 Assertion 就會通過 (Pass):

1Process::assertRan(fn ($process, $result) =>
2 $process->command === 'ls -la' &&
3 $process->path === __DIR__ &&
4 $process->timeout === 60
5);
1Process::assertRan(fn ($process, $result) =>
2 $process->command === 'ls -la' &&
3 $process->path === __DIR__ &&
4 $process->timeout === 60
5);

傳給 assertRun Closure 的 $processIlluminate\Console\Process\PendingProcess 的實體,而 $resultIlluminate\Contracts\Console\Process\ProcessResult 的實體。

assertDidntRun

判斷給定 Process 是否未被呼叫:

1use Illuminate\Support\Facades\Process;
2 
3Process::assertDidntRun('ls -la');
1use Illuminate\Support\Facades\Process;
2 
3Process::assertDidntRun('ls -la');

assertRun 方法類似,assertDidntRun 方法也可被傳入一個 Closure。傳給 assertDidntRun 的 Closure 會收到 Process 實體與 Process 的執行結果,讓你能檢查 Process 上的設定。若該 Closure 回傳 true,則該 Assertion 就會失敗 (Fail):

1Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
2 $process->command === 'ls -la'
3);
1Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
2 $process->command === 'ls -la'
3);

assertRanTimes

判斷給定 Process 是否被呼叫了給定次數:

1use Illuminate\Support\Facades\Process;
2 
3Process::assertRanTimes('ls -la', times: 3);
1use Illuminate\Support\Facades\Process;
2 
3Process::assertRanTimes('ls -la', times: 3);

也可傳入一個 Closure 給 assertRanTimes 方法。該 Closure 會收到 Process 的實體與 Process 的執行結果,讓你能檢查 Process 上的設定。若讓該 Closure 回傳 true,且該 Process 被呼叫了給定的次數,則該 Assertion 就會通過 (Pass):

1Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
2 return $process->command === 'ls -la';
3}, times: 3);
1Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
2 return $process->command === 'ls -la';
3}, times: 3);

避免漏掉的 Process

若想在個別測試或整個測試套件中,確保所有呼叫的 Process 都被模擬,則可呼叫 preventStrayProcesses 方法。呼叫該方法後,若某個 Process 沒有相對應的模擬結果,該 Process 就不會被執行,而會擲回一個 Exception:

1use Illuminate\Support\Facades\Process;
2 
3Process::preventStrayProcesses();
4 
5Process::fake([
6 'ls *' => 'Test output...',
7]);
8 
9// 回傳模擬的輸出...
10Process::run('ls -la');
11 
12// 擲回 Exception...
13Process::run('bash import.sh');
1use Illuminate\Support\Facades\Process;
2 
3Process::preventStrayProcesses();
4 
5Process::fake([
6 'ls *' => 'Test output...',
7]);
8 
9// 回傳模擬的輸出...
10Process::run('ls -la');
11 
12// 擲回 Exception...
13Process::run('bash import.sh');
翻譯進度
100% 已翻譯
更新時間:
2023年11月5日 上午11:45:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.