进程
简介
Laravel 围绕 Symfony Process 组件 提供了一个富有表现力、极简的 API,允许你方便地从 Laravel 应用程序中调用外部进程。Laravel 的进程功能专注于最常见的用例和出色的开发者体验。
调用进程
要调用一个进程,你可以使用 Process 门面提供的 run 和 start 方法。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->command();
$result->successful();
$result->failed();
$result->output();
$result->errorOutput();
$result->exitCode();抛出异常
如果你有一个进程结果,并且如果退出码大于零(表示失败)想要抛出一个 Illuminate\Process\Exceptions\ProcessFailedException 实例,可以使用 throw 和 throwIf 方法。如果进程没有失败,ProcessResult 实例将被返回:
$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');timeout 和 idleTimeout 方法也接受 CarbonInterval 实例:
use function Illuminate\Support\minutes;
$result = Process::timeout(minutes(2))->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');WARNING
Windows 不支持 TTY 模式。
进程输出
如前所述,可以使用进程结果上的 output(stdout)和 errorOutput(stderr)方法来访问进程输出:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();然而,也可以通过将一个闭包作为第二个参数传递给 run 方法来实时收集输出。该闭包将接收两个参数:输出的“类型”(stdout 或 stderr)和输出字符串本身:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});Laravel 还提供了 seeInOutput 和 seeInErrorOutput 方法,它们提供了一种方便的方法来确定给定字符串是否包含在进程的输出中:
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 方法来实时收集进程输出。该闭包将接收两个参数:输出的“类型”(stdout 或 stderr)和输出字符串本身:
$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"');
}, 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 方法来等待进程完成执行并检索 ProcessResult 实例:
$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);异步进程输出
当异步进程正在运行时,你可以使用 output 和 errorOutput 方法访问其当前的全部输出;但是,你可以使用 latestOutput 和 latestErrorOutput 方法来访问自上次检索输出以来进程产生的输出:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
sleep(1);
}与 run 方法一样,可以通过将一个闭包作为第二个参数传递给 start 方法来从异步进程实时收集输出。该闭包将接收两个参数:输出的“类型”(stdout 或 stderr)和输出字符串本身:
$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
$result = $process->wait();你可以使用 waitUntil 方法根据进程的输出停止等待,而不是等待进程完成。当传递给 waitUntil 方法的闭包返回 true 时,Laravel 将停止等待进程完成:
$process = Process::start('bash import.sh');
$process->waitUntil(function (string $type, string $output) {
return $output === 'Ready...';
});异步进程超时
当异步进程正在运行时,你可以使用 ensureNotTimedOut 方法来验证进程是否尚未超时。如果进程已超时,此方法将抛出一个超时异常:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
$process->ensureNotTimedOut();
// ...
sleep(1);
}并发进程
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 方法返回一个可访问的对象,允许你通过其键访问池中每个进程的 ProcessResult 实例:
$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!';
});在测试此路由时,我们可以通过调用 Process 门面上的 fake 方法(不带参数)来指示 Laravel 为每个被调用的进程返回一个虚拟的、成功的进程结果。此外,我们甚至可以断言某个给定的进程被“运行”了:
<?php
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
test('进程被调用', function () {
Process::fake();
$response = $this->get('/import');
// 简单的进程断言...
Process::assertRan('bash import.sh');
// 或者,检查进程配置...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
});<?php
namespace Tests\Feature;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
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');
// 简单的进程断言...
Process::assertRan('bash import.sh');
// 或者,检查进程配置...
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: '测试输出',
errorOutput: '测试错误输出',
exitCode: 1,
),
]);模拟特定进程
正如你可能在前面的示例中注意到的,Process 门面允许你通过向 fake 方法传递一个数组来为每个进程指定不同的模拟结果。
数组的键应代表你想要模拟的命令模式及其相关结果。* 字符可用作通配符。任何未被模拟的进程命令将实际被调用。你可以使用 Process 门面的 result 方法为这些命令构建存根/虚拟结果:
Process::fake([
'cat *' => Process::result(
output: '测试 "cat" 输出',
),
'ls *' => Process::result(
output: '测试 "ls" 输出',
),
]);如果你不需要自定义模拟进程的退出码或错误输出,你会发现将模拟进程结果指定为简单字符串可能更方便:
Process::fake([
'cat *' => '测试 "cat" 输出',
'ls *' => '测试 "ls" 输出',
]);模拟进程序列
如果你正在测试的代码使用相同的命令调用了多个进程,你可能希望为每个进程调用分配一个不同的模拟进程结果。你可以通过 Process 门面的 sequence 方法来实现:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('第一次调用'))
->push(Process::result('第二次调用')),
]);模拟异步进程生命周期
到目前为止,我们主要讨论了模拟使用 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('第一行标准输出')
->errorOutput('第一行错误输出')
->output('第二行标准输出')
->exitCode(0)
->iterations(3),
]);让我们深入分析上面的示例。使用 output 和 errorOutput 方法,我们可以指定将按顺序返回的多行输出。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 闭包的 $process 是一个 Illuminate\Process\PendingProcess 实例,而 $result 是一个 Illuminate\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 方法也接受一个闭包,该闭包将接收一个 PendingProcess 实例和一个 ProcessResult 实例,允许你检查进程的配置选项。如果此闭包返回 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 *' => '测试输出...',
]);
// 返回模拟响应...
Process::run('ls -la');
// 抛出异常...
Process::run('bash import.sh');