Laravel 提供了一個表達性強、最小 API,圍繞著 Symfony Process component,讓您可以方便地從 Laravel 應用程序中調用外部進程。Laravel 的進程功能專注於最常見的用例和出色的開發者體驗。
要調用一個進程,您可以使用 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->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 模式將進程的輸入和輸出連接到您程序的輸入和輸出,從而使您的進程能夠像 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
)和輸出字符串本身:
$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');
有時您可能希望將一個進程的輸出作為另一個進程的輸入。這通常被稱為將一個進程的輸出“管道”到另一個進程。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
方法的第二個引數,可以即時收集流程輸出。閉包將接收兩個引數:輸出的 "類型"(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"');
})->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:
$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
方法的第二個參數,也可以即時從異步進程中收集輸出。閉包將接收兩個引數:輸出的 "類型"(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...';
});
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();
由於進程池的 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為您的功能測試提供了幾個進程斷言。我們將在下面討論每個斷言。
確認特定進程是否被調用:
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
的一個實例。
確認特定進程未被調用:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');
與 assertRan
方法類似,assertDidntRun
方法還接受一個閉包,該閉包將接收一個進程實例和一個進程結果,允許您檢查進程的配置選項。如果此閉包返回 true
,則斷言將“失敗”:
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);
確認特定進程被調用指定次數:
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');