简介

Laravel 围绕 Symfony Process 组件 提供了富有表现力且精简的 API,让你可以方便地从 Laravel 应用程序调用外部进程。Laravel 的进程功能专注于最常见的用例和出色的开发人员体验。

调用进程

要调用进程,你可以使用 php Process 门面提供的 php runphp start 方法。 php run 方法将调用进程并等待进程完成执行,而 php start 方法用于异步进程执行。我们将在本文档中研究这两种方法。首先,让我们研究如何调用基本的同步进程并检查其结果:

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

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

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

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

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

流程选项

当然,在调用进程之前,你可能需要自定义进程的行为。刚好 Laravel 允许你调整各种进程功能,例如工作目录、超时和环境变量。

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

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

输入
你可以使用 php input 方法通过进程的「标准输入」提供输入:

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

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

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

或者,你可以调用 php forever 方法完全禁用进程超时:

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

php idleTimeout 方法可用于指定进程在不返回任何输出的情况下运行的最大秒数:

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

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

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

如果你想从调用的进程中删除继承的环境变量,可以为该环境变量提供 php false 值:

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

TTY 模式
php tty 方法可用于为进程启用 TTY 模式。TTY 模式将进程的输入和输出连接到程序的输入和输出,允许进程以进程形式打开 Vim 或 Nano 等编辑器:

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

进程输出

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

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

但是,也可以通过将闭包作为第二个参数传递给 php run 方法,实时收集输出。闭包将接收两个参数:输出的 「类型」(php stdoutphp stderr)和输出字符串本身:

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

Laravel 还提供了 php seeInOutputphp seeInErrorOutput 方法,它们提供了一种方便的方法来确定给定的字符串是否包含在进程的输出中:

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

禁用流程输出
如果你的进程正在写入大量你不感兴趣的输出,你可以通过完全禁用输出检索来节省内存。为此,请在构建进程时调用 php quietly 方法:

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

管道

有时候你希望将一个进程的输出作为另一个进程的输入。这通常称为将一个进程的输出「管道化」到另一个进程中。php Process 门面提供的 php pipe 方法可以轻松实现此目的。php pipe 方法将同步执行管道进程并返回管道中最后一个进程的进程结果:

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

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

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

通过将闭包作为第二个参数传递给 php pipe 方法,可以实时收集进程输出。闭包将接收两个参数:输出的 「类型」 (php stdoutphp 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 还允许你通过 php as 方法为管道中的每个进程分配字符串键。此键还将传递给 php 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. });

异步进程

虽然 php run 方法同步调用进程,但可以使用 php start 方法异步调用进程。这使得你的应用程序能够在进程后台运行时继续执行其他任务。一旦进程被调用,你可以使用 php running 方法来判断进程是否仍在运行:

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

如你所料,你可以调用 php wait 方法来等待进程执行完毕并获取进程结果实例

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

进程 ID 和信号

可以使用 php id 方法来获取正在运行的进程的操作系统分配的进程 ID:

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

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

  1. $process->signal(SIGUSR2);

异步过程输出

当异步进程运行时,你可以使用 php outputphp errorOutput 方法访问其当前的全部输出;也可以使用 php latestOutputphp latestErrorOutput 来访问上一次获取输出以来发生的进程输出:

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

php run 方法类似,也可以通过将闭包作为第二个参数传递给 php start 方法,实时收集异步进程的输出。闭包将接收两个参数:输出的 「类型」 (php stdoutphp stderr)和输出字符串本身:

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

并发进程

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

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

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

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

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

或者,为了方便,可以使用 php 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. echo $first->output();

命名进程池中的进程

通过数字键访问进程池结果不太具有表达性,因此 Laravel 允许你通过 php as 方法为池中的每个进程分配字符串键。这个键也将传递给提供给 php 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. $results = $pool->wait();
  9. return $results['first']->output();

进程池进程 ID 和信号

由于进程池的 php running 方法提供了一个包含池中所有已调用进程的集合,因此你可以轻松地访问基础池进程的 ID:

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

为了方便,您可以在进程池上调用 php signal 方法向池中的每个进程发送信号:

  1. $pool->signal(SIGUSR2);

测试

许多 Laravel 服务都提供功能,以帮助你轻松、有表达力地编写测试,Laravel 的进程服务也不例外。php Process 门面的 php fake 方法允许你指示 Laravel 在调用进程时返回存根 / 伪造结果。

伪造进程

在探索 Laravel 的伪造进程能力时,让我们假设一下调用进程的路由:

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

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

  1. <?php
  2. use Illuminate\Process\PendingProcess;
  3. use Illuminate\Contracts\Process\ProcessResult;
  4. use Illuminate\Support\Facades\Process;
  5. test('process is invoked', function () {
  6. Process::fake();
  7. $response = $this->get('/import');
  8. // 简单的进程断言...
  9. Process::assertRan('bash import.sh');
  10. // 或者,检查进程配置...
  11. Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
  12. return $process->command === 'bash import.sh' &&
  13. $process->timeout === 60;
  14. });
  15. });
  1. <?php
  2. namespace Tests\Feature;
  3. use Illuminate\Process\PendingProcess;
  4. use Illuminate\Contracts\Process\ProcessResult;
  5. use Illuminate\Support\Facades\Process;
  6. use Tests\TestCase;
  7. class ExampleTest extends TestCase
  8. {
  9. public function test_process_is_invoked(): void
  10. {
  11. Process::fake();
  12. $response = $this->get('/import');
  13. // 简单的进程断言...
  14. Process::assertRan('bash import.sh');
  15. // 或者,检查进程配置...
  16. Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
  17. return $process->command === 'bash import.sh' &&
  18. $process->timeout === 60;
  19. });
  20. }
  21. }

如上所述,在 php Process 门面上调用 php fake 方法将指示 Laravel 始终返回没有输出的成功进程结果。然而,你可以使用 php Process 门面的 php result 方法为伪造的进程指定输出和退出代码:

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

伪造特定进程

如之前的示例中所提到的 php Process 门面允许您通过传递一个数组到 php fake 方法来为不同的进程指定不同的假结果。

数组的键应该表示你想伪造的命令模式及其相关结果。星号 php * 字符可用作通配符,任何未被伪造的进程命令将会被实际执行。你可以使用 php Process 门面的 php result 方法为这些命令构建 stub / fake 结果:

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

如果您不需要自定义伪造进程的退出代码或错误输出,使用简单的字符串指定伪造的进程结果可能会更加方便:

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

伪造进程序列

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

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

伪造异步进程的生命周期

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

例如,让我们假设以下路由,它与一个异步进程交互:

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

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

  1. Process::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. ]);

让我们深入研究上面的示例。使用 php output and php errorOutput 方法,我们可以指定顺序返回的多行输出。php exitCode 方法可用于指定伪造进程的最终退出码。最后, php iterations 方法可用于指定 php running 方法应返回 php true 的次数。

可用的断言

如前所述, Laravel 为你的功能测试提供了几个进程断言。我们将在下面讨论每个断言。

assertRan
断言调用了给定的进程:

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

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

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

传递给 php assertRan 闭包的 php $processphp Illuminate\Process\PendingProcess的一个实例,而 php $resultphp Illuminate\Contracts\Process\ProcessResult的一个实例。

assertDidntRun
断言没有调用给定的进程 :

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

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

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

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

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

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

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

防止杂散进程

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

  1. use Illuminate\Support\Facades\Process;
  2. Process::preventStrayProcesses();
  3. Process::fake([
  4. 'ls *' => 'Test output...',
  5. ]);
  6. // 返回了虚假响应...
  7. Process::run('ls -la');
  8. // 引发异常...
  9. Process::run('bash import.sh');