进程
简介
Laravel 提供了一个富有表现力且极简的 API,围绕 Symfony Process 组件,允许您方便地从 Laravel 应用程序中调用外部进程。Laravel 的进程功能专注于最常见的用例和出色的开发者体验。
调用进程
要调用进程,您可以使用 Process
外观模式提供的 run
和 start
方法。run
方法将调用进程并等待进程完成执行,而 start
方法用于异步进程执行。我们将在本文档中检查这两种方法。首先,让我们检查如何调用基本的同步进程并检查其结果
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();
抛出异常
如果您有一个进程结果,并且希望在退出代码大于零(因此表示失败)时抛出 Illuminate\Process\Exceptions\ProcessFailedException
的实例,则可以使用 throw
和 throwIf
方法。如果进程未失败,则将返回进程结果实例
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');
进程输出
如前所述,可以使用进程结果上的 output
(stdout) 和 errorOutput
(stderr) 方法访问进程输出
1use Illuminate\Support\Facades\Process;2 3$result = Process::run('ls -la');4 5echo $result->output();6echo $result->errorOutput();
但是,也可以通过传递闭包作为 run
方法的第二个参数来实时收集输出。闭包将接收两个参数:输出的“类型”(stdout
或 stderr
)和输出字符串本身
1$result = Process::run('ls -la', function (string $type, string $output) {2 echo $output;3});
Laravel 还提供了 seeInOutput
和 seeInErrorOutput
方法,这些方法提供了一种方便的方式来确定给定的字符串是否包含在进程的输出中
1if (Process::run('ls -la')->seeInOutput('laravel')) {2 // ...3}
禁用进程输出
如果您的进程正在写入大量您不感兴趣的输出,则可以通过完全禁用输出检索来节省内存。要实现此目的,请在构建进程时调用 quietly
方法
1use Illuminate\Support\Facades\Process;2 3$result = Process::quietly()->run('bash import.sh');
管道
有时您可能希望将一个进程的输出作为另一个进程的输入。这通常称为将一个进程的输出“管道”到另一个进程中。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
方法的第二个参数来实时收集进程输出。闭包将接收两个参数:输出的“类型”(stdout
或 stderr
)和输出字符串本身
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})->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
方法来等待进程完成执行并检索进程结果实例
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);
异步进程输出
当异步进程正在运行时,您可以使用 output
和 errorOutput
方法访问其整个当前输出;但是,您可以使用 latestOutput
和 latestErrorOutput
来访问自上次检索输出以来进程发生的输出
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
方法的第二个参数,从异步进程中实时收集输出。闭包将接收两个参数:输出的“类型”(stdout
或 stderr
)和输出字符串本身
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});
并发进程
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 的进程服务也不例外。Process
外观模式的 fake
方法允许您指示 Laravel 在调用进程时返回存根/虚拟结果。
伪造进程
为了探索 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});
在测试此路由时,我们可以指示 Laravel 通过在不带参数的情况下调用 Process
外观模式上的 fake
方法,为每个调用的进程返回伪造的成功进程结果。此外,我们甚至可以 断言 给定的进程已被“运行”
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 TestCase11{12 public function test_process_is_invoked(): void13 {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]);
让我们深入研究上面的示例。使用 output
和 errorOutput
方法,我们可以指定将按顺序返回的多行输出。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 === 605);
传递给 assertRan
闭包的 $process
是 Illuminate\Process\PendingProcess
的实例,而 $result
是 Illuminate\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
方法还接受一个闭包,该闭包将接收进程实例和进程结果,从而使您可以检查进程的配置选项。如果此闭包返回 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');