簡介 (Introduction)
Laravel 圍繞 Symfony Process 元件 提供了富有表現力且精簡的 API,讓你可以方便地從 Laravel 應用程式呼叫外部程序。Laravel 的程序功能專注於最常見的使用案例和出色的開發人員體驗。
呼叫程序 (Invoking Processes)
要呼叫程序,你可以使用 Process facade 提供的 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->command();
$result->successful();
$result->failed();
$result->output();
$result->errorOutput();
$result->exitCode();
拋出例外 (Throwing Exceptions)
如果你有一個程序結果,並且希望在退出代碼大於零(表示失敗)時拋出 Illuminate\Process\Exceptions\ProcessFailedException 實例,你可以使用 throw 和 throwIf 方法。如果程序沒有失敗,將傳回 ProcessResult 實例:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
程序選項 (Process Options)
當然,你可能需要在呼叫程序之前自訂其行為。幸運的是,Laravel 允許你調整各種程序功能,例如工作目錄、逾時和環境變數。
工作目錄路徑 (Working Directory Path)
你可以使用 path 方法指定程序的工作目錄。如果未呼叫此方法,程序將繼承當前執行的 PHP 腳本的工作目錄:
$result = Process::path(__DIR__)->run('ls -la');
輸入 (Input)
你可以使用 input 方法透過程序的「標準輸入」提供輸入:
$result = Process::input('Hello World')->run('cat');
逾時 (Timeouts)
預設情況下,程序在執行超過 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');
環境變數 (Environment Variables)
可以透過 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 Mode)
tty 方法可用於為你的程序啟用 TTY 模式。TTY 模式將程序的輸入和輸出連接到你的程式的輸入和輸出,允許你的程序打開像 Vim 或 Nano 這樣的編輯器作為程序:
Process::forever()->tty()->run('vim');
[!WARNING] Windows 不支援 TTY 模式。
程序輸出 (Process Output)
如前所述,可以使用程序結果上的 output (stdout) 和 errorOutput (stderr) 方法存取程序輸出:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();
但是,也可以透過將閉包作為第二個參數傳遞給 run 方法來即時收集輸出。閉包將接收兩個參數:輸出的「類型」(stdout 或 stderr)和輸出字串本身:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});
Laravel 還提供了 seeInOutput 和 seeInErrorOutput 方法,這提供了一種方便的方法來確定給定字串是否包含在程序的輸出中:
if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}
停用程序輸出 (Disabling Process Output)
如果你的程序正在寫入大量你不感興趣的輸出,你可以透過完全停用輸出檢索來節省記憶體。要做到這一點,請在建立程序時呼叫 quietly 方法:
use Illuminate\Support\Facades\Process;
$result = Process::quietly()->run('bash import.sh');
管道 (Pipelines)
有時你可能希望將一個程序的輸出作為另一個程序的輸入。這通常被稱為將程序的輸出「管道」傳輸到另一個程序。Process facades 提供的 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 方法來即時收集程序輸出。閉包將接收兩個參數:輸出的「類型」(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 還允許你透過 as 方法為管道中的每個程序分配字串鍵。此鍵也將傳遞給提供給 pipe 方法的輸出閉包,允許你確定輸出屬於哪個程序:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->as('first')->command('cat example.txt');
$pipe->as('second')->command('grep -i "laravel"');
}, function (string $type, string $output, string $key) {
// ...
});
非同步程序 (Asynchronous Processes)
雖然 run 方法同步呼叫程序,但 start 方法可用於非同步呼叫程序。這允許你的應用程式在程序在背景執行時繼續執行其他任務。一旦程序被呼叫,你可以利用 running 方法來確定程序是否仍在執行:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
// ...
}
$result = $process->wait();
正如你可能已經注意到的,你可以呼叫 wait 方法來等待程序執行完成並檢索 ProcessResult 實例:
$process = Process::timeout(120)->start('bash import.sh');
// ...
$result = $process->wait();
程序 ID 和訊號 (Process IDs and Signals)
id 方法可用於檢索作業系統分配給正在執行的程序的程序 ID:
$process = Process::start('bash import.sh');
return $process->id();
你可以使用 signal 方法向正在執行的程序發送「訊號」。預定義訊號常數的列表可以在 PHP 文件中找到:
$process->signal(SIGUSR2);
非同步程序輸出 (Asynchronous Process Output)
當非同步程序正在執行時,你可以使用 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 方法來即時收集非同步程序的輸出。閉包將接收兩個參數:輸出的「類型」(stdout 或 stderr)和輸出字串本身:
$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...';
});
非同步程序逾時 (Asynchronous Process Timeouts)
當非同步程序正在執行時,你可以使用 ensureNotTimedOut 方法驗證程序是否尚未逾時。如果程序已逾時,此方法將拋出 逾時例外:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
$process->ensureNotTimedOut();
// ...
sleep(1);
}
並行程序 (Concurrent Processes)
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 方法傳回一個可存取陣列的物件,允許你透過其鍵存取池中每個程序的 ProcessResult 實例:
$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();
命名池程序 (Naming Pool Processes)
透過數字鍵存取程序池結果不是很直觀;因此,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 和訊號 (Pool Process IDs and Signals)
由於程序池的 running 方法提供了池中所有被呼叫程序的集合,你可以輕鬆存取底層池程序 ID:
$processIds = $pool->running()->each->id();
並且,為了方便起見,你可以在程序池上呼叫 signal 方法向池中的每個程序發送訊號:
$pool->signal(SIGUSR2);
測試 (Testing)
許多 Laravel 服務提供功能來幫助你輕鬆且富有表現力地編寫測試,Laravel 的程序服務也不例外。Process facade 的 fake 方法允許你指示 Laravel 在呼叫程序時傳回存根 / 虛擬結果。
偽造程序 (Faking Processes)
為了探索 Laravel 偽造程序的能力,讓我們想像一個呼叫程序的路由:
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
Process::run('bash import.sh');
return 'Import complete!';
});
在測試此路由時,我們可以透過在 Process facade 上呼叫不帶參數的 fake 方法來指示 Laravel 為每個被呼叫的程序傳回一個偽造的、成功的程序結果。此外,我們甚至可以 斷言 給定的程序已「執行」:
<?php
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
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\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
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 facade 上呼叫 fake 方法將指示 Laravel 始終傳回一個成功的程序結果,且沒有輸出。但是,你可以使用 Process facade 的 result 方法輕鬆指定偽造程序的輸出和退出代碼:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
偽造特定程序 (Faking Specific Processes)
正如你在前面的範例中可能已經注意到的,Process facade 允許你透過將陣列傳遞給 fake 方法來為每個程序指定不同的偽造結果。
陣列的鍵應代表你希望偽造的命令模式及其關聯的結果。* 字元可用作萬用字元。任何未被偽造的程序命令將實際被呼叫。你可以使用 Process facade 的 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',
]);
偽造程序序列 (Faking Process Sequences)
如果你正在測試的程式碼使用相同的命令呼叫多個程序,你可能希望為每個程序呼叫分配不同的偽造程序結果。你可以透過 Process facade 的 sequence 方法來實現這一點:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
偽造非同步程序生命週期 (Faking Asynchronous Process Lifecycles)
到目前為止,我們主要討論了偽造使用 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 facade 的 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 多少次。
可用的斷言 (Available Assertions)
如 前所述,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 方法也接受一個閉包,該閉包將接收 PendingProcess 和 ProcessResult 的實例,允許你檢查程序的設定選項。如果此閉包傳回 true 並且程序被呼叫了指定的次數,則斷言將「通過」:
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
防止雜散程序 (Preventing Stray Processes)
如果你想確保在你的單個測試或整個測試套件中所有被呼叫的程序都被偽造,你可以呼叫 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');