跳到主要內容

程序

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

程序

簡介

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->command();
$result->successful();
$result->failed();
$result->output();
$result->errorOutput();
$result->exitCode();

拋出異常

如果您有一個程序結果,並且希望在退出碼大於零(因此表示失敗)時拋出 Illuminate\Process\Exceptions\ProcessFailedException 的實例,可以使用 throwthrowIf 方法。如果程序未失敗,將返回 ProcessResult 實例:

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

timeoutidleTimeout 方法還接受 CarbonInterval 實例:

use function Illuminate\Support\minutes;

$result = Process::timeout(minutes(2))->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');

[!WARNING] Windows 上不支援 TTY 模式。

程序輸出

如前所述,可以使用程式結果上的 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"');
}, 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 方法來等待程式完成執行,並檢索 ProcessResult 實例:

$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...';
});

非同步程序逾時

當非同步程序運行時,您可以使用 ensureNotTimedOut 方法來驗證程式是否未逾時。如果程式已逾時,此方法將拋出逾時異常

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

while ($process->running()) {
    $process->ensureNotTimedOut();

    // ...

    sleep(1);
}

並發程序

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

命名池程序

透過數字鍵存取程式池結果不是很富有表達力;因此,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\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;

test('process is invoked', function () {
    Process::fake();

    $response = $this->get('/import');

    // 簡單的程序斷言...
    Process::assertRan('bash import.sh');

    // 或者,檢查程序配置...
    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');

        // 簡單的程序斷言...
        Process::assertRan('bash import.sh');

        // 或者,檢查程序配置...
        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 閉包的 $processIlluminate\Process\PendingProcess 的實例,而 $resultIlluminate\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 方法還接受一個閉包,該閉包將接收 PendingProcessProcessResult 的實例,允許您檢查程序配置的選項。如果此閉包返回 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...',
]);

// 返回虛擬回應...
Process::run('ls -la');

// 拋出異常...
Process::run('bash import.sh');