跳至内容

进程

简介

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

并发进程

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

在测试此路由时,我们可以指示 Laravel 通过在没有参数的情况下对 Process 门面调用 fake 方法,为每个调用的进程返回一个假的成功进程结果。此外,我们甚至可以 断言 某个进程已被“运行”。

<?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 闭包的 $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 方法也接受一个闭包,该闭包将接收一个进程实例和一个进程结果,允许您检查进程的配置选项。如果此闭包返回 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');