跳转至内容

进程

简介

Laravel 基于 Symfony Process 组件 提供了一个富有表现力且精简的 API,让你可以方便地从 Laravel 应用程序中调用外部进程。Laravel 的进程功能专注于最常见的用例,旨在提供卓越的开发者体验。

调用进程

要调用进程,可以使用 Process 门面提供的 runstart 方法。run 方法会调用一个进程并等待其执行完毕,而 start 方法则用于异步执行进程。我们将在本章中探讨这两种方法。首先,让我们看看如何调用一个基础的同步进程并检查其结果。

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5return $result->output();

当然,run 方法返回的 Illuminate\Contracts\Process\ProcessResult 实例提供了一系列有用的方法,可用于检查进程结果。

1$result = Process::run('ls -la');
2 
3$result->command();
4$result->successful();
5$result->failed();
6$result->output();
7$result->errorOutput();
8$result->exitCode();

抛出异常

如果你想在进程结果的退出码大于零(即表示失败)时抛出 Illuminate\Process\Exceptions\ProcessFailedException 异常,可以使用 throwthrowIf 方法。如果进程未失败,则会返回 ProcessResult 实例。

1$result = Process::run('ls -la')->throw();
2 
3$result = Process::run('ls -la')->throwIf($condition);

进程选项

当然,你可能需要在调用进程之前自定义其行为。所幸,Laravel 允许你调整多种进程特性,例如工作目录、超时时间和环境变量。

工作目录路径

你可以使用 path 方法指定进程的工作目录。如果未调用此方法,进程将继承当前正在执行的 PHP 脚本的工作目录。

1$result = Process::path(__DIR__)->run('ls -la');

输入

你可以使用 input 方法通过进程的“标准输入”提供数据。

1$result = Process::input('Hello World')->run('cat');

超时

默认情况下,如果进程执行超过 60 秒,将抛出 Illuminate\Process\Exceptions\ProcessTimedOutException 实例。你可以通过 timeout 方法自定义此行为。

1$result = Process::timeout(120)->run('bash import.sh');

或者,如果你想彻底禁用进程超时,可以调用 forever 方法。

1$result = Process::forever()->run('bash import.sh');

idleTimeout 方法可用于指定进程在没有返回任何输出的情况下可以运行的最大秒数。

1$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');

环境变量

可以通过 env 方法为进程提供环境变量。调用的进程也将继承系统定义的所有环境变量。

1$result = Process::forever()
2 ->env(['IMPORT_PATH' => __DIR__])
3 ->run('bash import.sh');

如果你希望从调用的进程中移除某个继承的环境变量,可以将该环境变量的值设置为 false

1$result = Process::forever()
2 ->env(['LOAD_PATH' => false])
3 ->run('bash import.sh');

TTY 模式

tty 方法可用于为你的进程启用 TTY 模式。TTY 模式将进程的输入和输出连接到程序的输入和输出,从而允许你的进程打开像 Vim 或 Nano 这样的编辑器。

1Process::forever()->tty()->run('vim');

Windows 不支持 TTY 模式。

进程输出

正如前面所讨论的,可以使用进程结果上的 output (stdout) 和 errorOutput (stderr) 方法来访问进程输出。

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::run('ls -la');
4 
5echo $result->output();
6echo $result->errorOutput();

此外,通过将闭包作为 run 方法的第二个参数传递,也可以实时获取输出。该闭包将接收两个参数:输出的“类型”(stdoutstderr)以及输出字符串本身。

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

Laravel 还提供了 seeInOutputseeInErrorOutput 方法,它们提供了一种便捷的方式来确定给定的字符串是否包含在进程的输出中。

1if (Process::run('ls -la')->seeInOutput('laravel')) {
2 // ...
3}

禁用进程输出

如果你的进程产生了大量你并不需要的输出,你可以通过完全禁用输出获取来节省内存。为此,请在构建进程时调用 quietly 方法。

1use Illuminate\Support\Facades\Process;
2 
3$result = Process::quietly()->run('bash import.sh');

管道 (Pipelines)

有时你可能希望将一个进程的输出作为另一个进程的输入。这通常被称为将一个进程的输出“管道化 (piping)”到另一个进程。Process 门面提供的 pipe 方法可以轻松实现这一点。pipe 方法将同步执行管道中的进程,并返回最后一个进程的执行结果。

1use Illuminate\Process\Pipe;
2use Illuminate\Support\Facades\Process;
3 
4$result = Process::pipe(function (Pipe $pipe) {
5 $pipe->command('cat example.txt');
6 $pipe->command('grep -i "laravel"');
7});
8 
9if ($result->successful()) {
10 // ...
11}

如果你不需要自定义组成管道的各个进程,可以直接向 pipe 方法传递一个命令字符串数组。

1$result = Process::pipe([
2 'cat example.txt',
3 'grep -i "laravel"',
4]);

通过将闭包作为 pipe 方法的第二个参数传递,可以实时收集进程输出。闭包将接收两个参数:输出的“类型”(stdoutstderr)以及输出字符串本身。

1$result = Process::pipe(function (Pipe $pipe) {
2 $pipe->command('cat example.txt');
3 $pipe->command('grep -i "laravel"');
4}, function (string $type, string $output) {
5 echo $output;
6});

Laravel 还允许你通过 as 方法为管道中的每个进程指定字符串键。此键也将传递给传递给 pipe 方法的输出闭包,从而让你确定输出属于哪个进程。

1$result = Process::pipe(function (Pipe $pipe) {
2 $pipe->as('first')->command('cat example.txt');
3 $pipe->as('second')->command('grep -i "laravel"');
4}, function (string $type, string $output, string $key) {
5 // ...
6});

异步进程

虽然 run 方法同步调用进程,但 start 方法可用于异步调用进程。这允许你的应用程序在进程于后台运行时继续执行其他任务。进程调用后,你可以使用 running 方法来确定进程是否仍在运行。

1$process = Process::timeout(120)->start('bash import.sh');
2 
3while ($process->running()) {
4 // ...
5}
6 
7$result = $process->wait();

你可能已经注意到,你可以调用 wait 方法来等待进程执行完毕并获取 ProcessResult 实例。

1$process = Process::timeout(120)->start('bash import.sh');
2 
3// ...
4 
5$result = $process->wait();

进程 ID 与信号

id 方法可用于获取操作系统为该运行中进程分配的进程 ID。

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

你可以使用 signal 方法向正在运行的进程发送“信号”。预定义的信号常量列表可以在 PHP 文档 中找到。

1$process->signal(SIGUSR2);

异步进程输出

当异步进程运行时,你可以使用 outputerrorOutput 方法访问其全部当前输出;你也可以利用 latestOutputlatestErrorOutput 来访问自上次获取输出以来产生的最新输出。

1$process = Process::timeout(120)->start('bash import.sh');
2 
3while ($process->running()) {
4 echo $process->latestOutput();
5 echo $process->latestErrorOutput();
6 
7 sleep(1);
8}

run 方法类似,通过将闭包作为 start 方法的第二个参数传递,也可以从异步进程中实时获取输出。闭包将接收两个参数:输出的“类型”(stdoutstderr)以及输出字符串本身。

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

你无需等待进程完成,可以使用 waitUntil 方法根据进程的输出停止等待。当传递给 waitUntil 方法的闭包返回 true 时,Laravel 将停止等待。

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

异步进程超时

在异步进程运行时,你可以使用 ensureNotTimedOut 方法验证进程是否已超时。如果进程已超时,此方法将抛出 超时异常

1$process = Process::timeout(120)->start('bash import.sh');
2 
3while ($process->running()) {
4 $process->ensureNotTimedOut();
5 
6 // ...
7 
8 sleep(1);
9}

并发进程

Laravel 还让管理并发的异步进程池变得轻而易举,让你能够轻松同时执行多个任务。要开始使用,请调用 pool 方法,该方法接收一个接收 Illuminate\Process\Pool 实例的闭包。

在此闭包内,你可以定义属于该池的进程。一旦通过 start 方法启动进程池,你就可以通过 running 方法访问运行中进程的 集合

1use Illuminate\Process\Pool;
2use Illuminate\Support\Facades\Process;
3 
4$pool = Process::pool(function (Pool $pool) {
5 $pool->path(__DIR__)->command('bash import-1.sh');
6 $pool->path(__DIR__)->command('bash import-2.sh');
7 $pool->path(__DIR__)->command('bash import-3.sh');
8})->start(function (string $type, string $output, int $key) {
9 // ...
10});
11 
12while ($pool->running()->isNotEmpty()) {
13 // ...
14}
15 
16$results = $pool->wait();

正如你所见,你可以等待池中所有进程执行完毕,并通过 wait 方法解析它们的结果。wait 方法返回一个数组可访问对象,允许你根据键访问池中每个进程的 ProcessResult 实例。

1$results = $pool->wait();
2 
3echo $results[0]->output();

或者,为了方便起见,可以使用 concurrently 方法启动一个异步进程池并立即等待其结果。当与 PHP 的数组解构功能结合使用时,这可以提供特别富有表现力的语法。

1[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
2 $pool->path(__DIR__)->command('ls -la');
3 $pool->path(app_path())->command('ls -la');
4 $pool->path(storage_path())->command('ls -la');
5});
6 
7echo $first->output();

为进程池命名

通过数字键访问进程池结果并不直观;因此,Laravel 允许你通过 as 方法为池中的每个进程分配字符串键。此键也将传递给传递给 start 方法的闭包,从而让你确定输出属于哪个进程。

1$pool = Process::pool(function (Pool $pool) {
2 $pool->as('first')->command('bash import-1.sh');
3 $pool->as('second')->command('bash import-2.sh');
4 $pool->as('third')->command('bash import-3.sh');
5})->start(function (string $type, string $output, string $key) {
6 // ...
7});
8 
9$results = $pool->wait();
10 
11return $results['first']->output();

进程池 ID 与信号

由于进程池的 running 方法提供了池中所有已调用进程的集合,你可以轻松访问底层的池进程 ID。

1$processIds = $pool->running()->each->id();

为了方便起见,你可以在进程池上调用 signal 方法,向池中的每个进程发送信号。

1$pool->signal(SIGUSR2);

测试

许多 Laravel 服务提供了帮助你轻松且富有表现力地编写测试的功能,Laravel 的进程服务也不例外。Process 门面的 fake 方法允许你指示 Laravel 在调用进程时返回存根(stubbed)/模拟结果。

伪造进程 (Faking Processes)

为了探索 Laravel 伪造进程的能力,让我们设想一个调用进程的路由。

1use Illuminate\Support\Facades\Process;
2use Illuminate\Support\Facades\Route;
3 
4Route::get('/import', function () {
5 Process::run('bash import.sh');
6 
7 return 'Import complete!';
8});

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

1<?php
2 
3use Illuminate\Contracts\Process\ProcessResult;
4use Illuminate\Process\PendingProcess;
5use Illuminate\Support\Facades\Process;
6 
7test('process is invoked', function () {
8 Process::fake();
9 
10 $response = $this->get('/import');
11 
12 // Simple process assertion...
13 Process::assertRan('bash import.sh');
14 
15 // Or, inspecting the process configuration...
16 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
17 return $process->command === 'bash import.sh' &&
18 $process->timeout === 60;
19 });
20});
1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Contracts\Process\ProcessResult;
6use Illuminate\Process\PendingProcess;
7use Illuminate\Support\Facades\Process;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 public function test_process_is_invoked(): void
13 {
14 Process::fake();
15 
16 $response = $this->get('/import');
17 
18 // Simple process assertion...
19 Process::assertRan('bash import.sh');
20 
21 // Or, inspecting the process configuration...
22 Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
23 return $process->command === 'bash import.sh' &&
24 $process->timeout === 60;
25 });
26 }
27}

如前所述,在 Process 门面上调用 fake 方法将指示 Laravel 始终返回一个没有输出的成功进程结果。但是,你可以使用 Process 门面的 result 方法轻松指定伪造进程的输出和退出码。

1Process::fake([
2 '*' => Process::result(
3 output: 'Test output',
4 errorOutput: 'Test error output',
5 exitCode: 1,
6 ),
7]);

伪造特定进程

正如你在之前的示例中可能注意到的,Process 门面允许你通过向 fake 方法传递数组,为不同的进程指定不同的伪造结果。

数组的键应该代表你想要伪造的命令模式,其对应的值为结果。* 字符可用作通配符。任何未被伪造的进程命令将实际被执行。你可以使用 Process 门面的 result 方法为这些命令构造存根/伪造结果。

1Process::fake([
2 'cat *' => Process::result(
3 output: 'Test "cat" output',
4 ),
5 'ls *' => Process::result(
6 output: 'Test "ls" output',
7 ),
8]);

如果你不需要自定义伪造进程的退出码或错误输出,你会发现将伪造进程结果指定为简单的字符串会更方便。

1Process::fake([
2 'cat *' => 'Test "cat" output',
3 'ls *' => 'Test "ls" output',
4]);

伪造进程序列

如果你测试的代码使用相同的命令多次调用进程,你可能希望为每次进程调用分配不同的伪造进程结果。你可以通过 Process 门面的 sequence 方法来实现这一点。

1Process::fake([
2 'ls *' => Process::sequence()
3 ->push(Process::result('First invocation'))
4 ->push(Process::result('Second invocation')),
5]);

伪造异步进程生命周期

到目前为止,我们主要讨论了使用 run 方法同步调用的进程伪造。但是,如果你正在尝试测试与使用 start 调用的异步进程交互的代码,你可能需要一种更复杂的方法来描述你的伪造进程。

例如,让我们设想以下与异步进程交互的路由。

1use Illuminate\Support\Facades\Log;
2use Illuminate\Support\Facades\Route;
3 
4Route::get('/import', function () {
5 $process = Process::start('bash import.sh');
6 
7 while ($process->running()) {
8 Log::info($process->latestOutput());
9 Log::info($process->latestErrorOutput());
10 }
11 
12 return 'Done';
13});

为了正确伪造此进程,我们需要能够描述 running 方法应该返回 true 的次数。此外,我们可能想要指定将按顺序返回的多行输出。为此,我们可以使用 Process 门面的 describe 方法。

1Process::fake([
2 'bash import.sh' => Process::describe()
3 ->output('First line of standard output')
4 ->errorOutput('First line of error output')
5 ->output('Second line of standard output')
6 ->exitCode(0)
7 ->iterations(3),
8]);

让我们深入了解上面的示例。使用 outputerrorOutput 方法,我们可以指定将按顺序返回的多行输出。exitCode 方法可用于指定伪造进程的最终退出码。最后,iterations 方法可用于指定 running 方法应该返回 true 的次数。

可用断言

正如 前面讨论的那样,Laravel 为你的功能测试提供了几种进程断言。我们将在下面讨论这些断言。

assertRan

断言给定的进程已被调用。

1use Illuminate\Support\Facades\Process;
2 
3Process::assertRan('ls -la');

assertRan 方法还接受一个闭包,该闭包将接收一个进程实例和一个进程结果,允许你检查进程的配置选项。如果此闭包返回 true,断言将“通过”。

1Process::assertRan(fn ($process, $result) =>
2 $process->command === 'ls -la' &&
3 $process->path === __DIR__ &&
4 $process->timeout === 60
5);

传递给 assertRan 闭包的 $processIlluminate\Process\PendingProcess 的一个实例,而 $resultIlluminate\Contracts\Process\ProcessResult 的一个实例。

assertDidntRun

断言给定的进程未被调用。

1use Illuminate\Support\Facades\Process;
2 
3Process::assertDidntRun('ls -la');

assertRan 方法一样,assertDidntRun 方法也接受一个闭包,该闭包将接收一个进程实例和一个进程结果,允许你检查进程的配置选项。如果此闭包返回 true,断言将“失败”。

1Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
2 $process->command === 'ls -la'
3);

assertRanTimes

断言给定的进程被调用了指定的次数。

1use Illuminate\Support\Facades\Process;
2 
3Process::assertRanTimes('ls -la', times: 3);

assertRanTimes 方法也接受一个闭包,该闭包将接收 PendingProcessProcessResult 的实例,允许你检查进程的配置选项。如果此闭包返回 true 且进程被调用了指定的次数,断言将“通过”。

1Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
2 return $process->command === 'ls -la';
3}, times: 3);

防止意外进程

如果你想确保在整个测试或测试套件中所有被调用的进程都已被伪造,可以调用 preventStrayProcesses 方法。调用此方法后,任何没有相应伪造结果的进程都将抛出异常,而不是启动实际进程。

1use Illuminate\Support\Facades\Process;
2 
3Process::preventStrayProcesses();
4 
5Process::fake([
6 'ls *' => 'Test output...',
7]);
8 
9// Fake response is returned...
10Process::run('ls -la');
11 
12// An exception is thrown...
13Process::run('bash import.sh');