跳轉至內容

程序

簡介

Laravel 基於 Symfony Process 元件 提供了一個富有表現力且精簡的 API,讓你可以方便地從 Laravel 應用程式中呼叫外部程序。Laravel 的程序功能專注於最常見的用例,旨在提供卓越的開發者體驗。

呼叫程序

要呼叫程序,可以使用 Process 門面提供的 runstart 方法。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->command();
4$result->successful();
5$result->failed();
6$result->output();
7$result->errorOutput();
8$result->exitCode();

丟擲異常

如果你想在程序結果的退出碼大於零(即表示失敗)時丟擲 Illuminate\Process\Exceptions\ProcessFailedException 異常,可以使用 throwthrowIf 方法。如果程序未失敗,則會返回 ProcessResult 例項。

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

Windows 不支援 TTY 模式。

程序輸出

正如前面所討論的,可以使用程序結果上的 output (stdout) 和 errorOutput (stderr) 方法來訪問程序輸出。

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

此外,透過將閉包作為 run 方法的第二個引數傳遞,也可以即時獲取輸出。該閉包將接收兩個引數:輸出的“型別”(stdoutstderr)以及輸出字串本身。

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

Laravel 還提供了 seeInOutputseeInErrorOutput 方法,它們提供了一種便捷的方式來確定給定的字串是否包含在程序的輸出中。

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

停用程序輸出

如果你的程序產生了大量你並不需要的輸出,你可以透過完全停用輸出獲取來節省記憶體。為此,請在構建程序時呼叫 quietly 方法。

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

管道 (Pipelines)

有時你可能希望將一個程序的輸出作為另一個程序的輸入。這通常被稱為將一個程序的輸出“管道化 (piping)”到另一個程序。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 方法的第二個引數傳遞,可以即時收集程序輸出。閉包將接收兩個引數:輸出的“型別”(stdoutstderr)以及輸出字串本身。

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}, 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 方法來等待程序執行完畢並獲取 ProcessResult 例項。

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

非同步程序輸出

當非同步程序執行時,你可以使用 outputerrorOutput 方法訪問其全部當前輸出;你也可以利用 latestOutputlatestErrorOutput 來訪問自上次獲取輸出以來產生的最新輸出。

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 方法的第二個引數傳遞,也可以從非同步程序中即時獲取輸出。閉包將接收兩個引數:輸出的“型別”(stdoutstderr)以及輸出字串本身。

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

非同步程序超時

在非同步程序執行時,你可以使用 ensureNotTimedOut 方法驗證程序是否已超時。如果程序已超時,此方法將丟擲 超時異常

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

併發程序

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 方法返回一個數組可訪問物件,允許你根據鍵訪問池中每個程序的 ProcessResult 例項。

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 在呼叫程序時返回存根(stubbed)/模擬結果。

偽造程序 (Faking Processes)

為了探索 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\Contracts\Process\ProcessResult;
4use Illuminate\Process\PendingProcess;
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\Contracts\Process\ProcessResult;
6use Illuminate\Process\PendingProcess;
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('/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]);

讓我們深入瞭解上面的示例。使用 outputerrorOutput 方法,我們可以指定將按順序返回的多行輸出。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 === 60
5);

傳遞給 assertRan 閉包的 $processIlluminate\Process\PendingProcess 的一個例項,而 $resultIlluminate\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 方法也接受一個閉包,該閉包將接收 PendingProcessProcessResult 的例項,允許你檢查程序的配置選項。如果此閉包返回 true 且程序被呼叫了指定的次數,斷言將“透過”。

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

防止意外程序

如果你想確保在整個測試或測試套件中所有被呼叫的程序都已被偽造,可以呼叫 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');