跳到內容

程序

簡介

Laravel 提供了一個圍繞 Symfony Process 組件 的表達式豐富、最小化的 API,讓您可以方便地從您的 Laravel 應用程式中調用外部程序。 Laravel 的程序功能著重於最常見的使用案例和絕佳的開發人員體驗。

調用程序

若要調用程序,您可以使用 Process 外觀提供的 runstart 方法。 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 的實例,您可以使用 throwthrowIf 方法。 如果程序未失敗,則會傳回程序結果實例。

$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 方法的第二個引數傳遞來即時收集輸出。 閉包將收到兩個引數:「輸出類型」(stdoutstderr) 和輸出字串本身。

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

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

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 方法的第二個引數傳遞來即時收集程序輸出。 閉包將收到兩個引數:「輸出類型」(stdoutstderr) 和輸出字串本身。

$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 方法可用於擷取作業系統指派給執行中程序的程序 ID。

$process = Process::start('bash import.sh');
 
return $process->id();

您可以使用 signal 方法將「訊號」傳送到執行中的程序。 預定義的訊號常數清單可以在 PHP 文件中找到。

$process->signal(SIGUSR2);

非同步程序輸出

當非同步程序執行時,您可以使用 outputerrorOutput 方法存取其完整的目前輸出;然而,您可以使用 latestOutputlatestErrorOutput 來存取自上次擷取輸出後,程序所產生的輸出。

$process = Process::timeout(120)->start('bash import.sh');
 
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
 
sleep(1);
}

如同 run 方法,您也可以透過傳遞一個閉包作為 start 方法的第二個參數,從非同步程序即時收集輸出。該閉包將接收兩個參數:輸出的「類型」(stdoutstderr)以及輸出字串本身。

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

您可以不等待程序完成,而是使用 waitUntil 方法根據程序的輸出停止等待。當傳遞給 waitUntil 方法的閉包回傳 true 時,Laravel 將停止等待程序完成。

$process = Process::start('bash import.sh');
 
$process->waitUntil(function (string $type, string $output) {
return $output === 'Ready...';
});

並發程序

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 服務都提供了功能來幫助您輕鬆且富有表達力地編寫測試,而 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),
]);

讓我們深入研究上面的例子。使用 outputerrorOutput 方法,我們可以指定將按順序回傳的多行輸出。 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...',
]);
 
// Fake response is returned...
Process::run('ls -la');
 
// An exception is thrown...
Process::run('bash import.sh');