跳至内容

Processes

介绍

Laravel 围绕Symfony Process 组件提供了简洁高效的 API ,让您能够轻松地从 Laravel 应用程序中调用外部进程。Laravel 的进程功能专注于最常见的用例,并提供卓越的开发者体验。

调用Processes

要调用Processes,您可以使用外观提供的run和方法方法将调用一个Processes并等待该Processes执行完成,而方法用于异步Processes执行。我们将在本文档中分别介绍这两种方法。首先,让我们研究一下如何调用一个基本的同步Processes并检查其结果:startProcessrunstart

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

当然,Illuminate\Contracts\Process\ProcessResult该方法返回的实例run提供了多种有用的方法,可用于检查处理结果:

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

抛出异常

如果您有一个Processes结果,并且希望在Illuminate\Process\Exceptions\ProcessFailedException退出代码大于零(从而表示失败)时抛出一个 实例,则可以使用throwthrowIf方法。如果Processes没有失败,则将返回Processes结果实例:

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

Processes选项

当然,您可能需要在调用进程之前自定义其行为。值得庆幸的是,Laravel 允许您调整各种进程特性,例如工作目录、超时和环境变量。

工作目录路径

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

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

输入

您可以使用该方法通过Processes的“标准输入”提供输入input

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

超时

Illuminate\Process\Exceptions\ProcessTimedOutException默认情况下,进程执行超过 60 秒后会抛出一个 实例。但是,你可以通过以下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');

过程输出

如前所述,可以使用output(stdout) 和errorOutput(stderr) 方法访问Processes结果的Processes输出:

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

Pipeline

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

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}

如果您不需要定制组成Pipeline的各个进程,您可以简单地将命令字符串数组传递给该pipe方法:

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

可以通过将闭包作为第二个参数传递给该方法来实时收集Processes输出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 还允许你通过 方法来为Pipeline中的每个进程分配字符串键as。该键还将传递给该pipe方法的 output 闭包,从而允许你确定输出属于哪个进程:

1$result = Process::pipe(function (Pipe $pipe) {
2 $pipe->as('first')->command('cat example.txt');
3 $pipe->as('second')->command('grep -i "laravel"');
4})->start(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方法来等待Processes执行完毕并检索Processes结果实例:

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

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

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方法的闭包返回以下内容时,Laravel 将停止等待进程完成true

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方法返回一个可访问数组的对象,该对象允许您通过其键访问池中每个进程的进程结果实例:

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 的Processes服务也不例外。FacadeProcessfake方法允许您指示 Laravel 在调用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});

fake测试此路由时,我们可以通过在外观层上调用不带参数的方法,指示 Laravel 为每个调用的Processes返回一个虚假的成功Processes结果Process。此外,我们甚至可以断言给定的Processes已“运行”:

1<?php
2 
3use Illuminate\Process\PendingProcess;
4use Illuminate\Contracts\Process\ProcessResult;
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\Process\PendingProcess;
6use Illuminate\Contracts\Process\ProcessResult;
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}

如上所述,调用Facadefake上的方法将指示 Laravel 始终返回成功的进程结果,不包含任何输出。但是,您可以使用Facade 的方法Process轻松地为伪造的进程指定输出和退出代码Processresult

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次数runningtrue

可用的断言

如前所述,Laravel 为你的功能测试提供了多个Processes断言。我们将在下面逐一讨论这些断言。

断言

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

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

assertRan方法还接受一个闭包,该闭包将接收一个Processes实例和一个Processes结果,以便检查该Processes的配置选项。如果此闭包返回true,则断言将“通过”:

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

传递$processassertRan闭包的 是 的一个实例Illuminate\Process\PendingProcess,而$result是 的一个实例Illuminate\Contracts\Process\ProcessResult

assertDidntRun

断言未调用给定的进程:

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

与方法类似assertRan,该assertDidntRun方法也接受一个闭包,该闭包将接收一个Processes实例和一个Processes结果,以便检查Processes的配置选项。如果此闭包返回true,则断言将“失败”:

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

断言运行时间

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

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

assertRanTimes方法还接受一个闭包,该闭包将接收一个Processes实例和一个Processes结果,以便检查Processes的配置选项。如果此闭包返回true,并且该Processes被调用了指定的次数,则断言将“通过”:

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