进程
介绍
Laravel 提供了一个简洁且富有表现力的 API,基于 【Symfony Process 组件】,使你能够方便地从 Laravel 应用中调用外部进程。Laravel 的进程功能专注于最常见的使用场景,并提供出色的开发者体验。
调用进程
要调用一个进程,你可以使用 Process
facade 提供的 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->successful(); // 检查进程是否成功
$result->failed(); // 检查进程是否失败
$result->exitCode(); // 获取进程的退出码
$result->output(); // 获取进程的标准输出
$result->errorOutput(); // 获取进程的错误输出
抛出异常
如果你有一个进程结果,并且希望在退出码大于零(表示失败)时抛出一个 Illuminate\Process\Exceptions\ProcessFailedException
实例,你可以使用 throw
和 throwIf
方法。如果进程没有失败,将返回该进程的结果实例:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
进程选项
当然,在调用进程之前,你可能需要自定义进程的行为。幸运的是,Laravel 允许你调整各种进程特性,例如工作目录、超时设置和环境变量。
工作目录路径
你可以使用 path
方法来指定进程的工作目录。如果没有调用此方法,进程将继承当前执行的 PHP 脚本的工作目录:
$result = Process::path(__DIR__)->run('ls -la');
超时
默认情况下,如果进程执行超过 60 秒,将抛出一个 Illuminate\Process\Exceptions\ProcessTimedOutException
实例。你可以通过 timeout
方法自定义这个行为:
$result = Process::timeout(120)->run('bash import.sh');
或者,如果你希望完全禁用进程超时,可以调用 forever
方法:
$result = Process::forever()->run('bash import.sh');
idleTimeout
方法可用于指定进程在没有返回任何输出的情况下最多可以运行多少秒:
$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
进程输出
如前所述,进程输出可以通过 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')) {
// ...
}
管道
有时你可能希望将一个进程的输出作为另一个进程的输入。这通常被称为将一个进程的输出 “管道” 传递给另一个进程。Laravel 的 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"');
})->start(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
方法,直到进程执行完成,并获取进程的结果实例:
$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
方法基于进程的输出停止等待。Laravel 会在传递给 waitUntil
方法的闭包返回 true
时停止等待进程完成:
$process = Process::start('bash import.sh');
$process->waitUntil(function (string $type, string $output) {
return $output === 'Ready...';
});
并发进程
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
方法返回一个可访问的数组对象,让你可以通过池中的每个进程的键来访问其进程结果实例:
$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();
测试
Laravel 提供了许多服务功能,帮助你轻松而富有表现力地编写测试,Laravel 的进程服务也不例外。Process
facade 的 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
facade 上调用 fake
方法,指示 Laravel 返回一个假的、成功的进程结果,模拟每次调用的进程。此外,我们甚至可以断言某个进程是否被 “运行”:
<?php
use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
test('process is invoked', function () {
Process::fake();
$response = $this->get('/import');
// Simple process assertion...
Process::assertRan('bash import.sh');
// Or, inspecting the process configuration...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
});
<?php
namespace Tests\Feature;
use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
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');
// Simple process assertion...
Process::assertRan('bash import.sh');
// Or, inspecting the process configuration...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}
正如之前讨论的,调用 Process
facade 上的 fake
方法将指示 Laravel 总是返回一个没有输出的成功进程结果。然而,你可以轻松地使用 Process
facade 的 result
方法来指定模拟进程的输出和退出码:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
伪造特定进程
正如你在之前的例子中可能注意到的,Process
facade 允许你通过向 fake
方法传递一个数组来为每个进程指定不同的模拟结果。
数组的键应该表示你想要模拟的命令模式及其关联的结果。*
字符可以用作通配符。任何没有被模拟的进程命令将会实际执行。你可以使用 Process
facade 的 result
方法为这些命令构造模拟结果:
Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);
如果你不需要定制模拟进程的退出码或错误输出,可能会发现将模拟结果直接指定为简单的字符串更方便:
Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);
伪造进程序列
如果你正在测试的代码调用了多个相同命令的进程,你可能希望为每次进程调用分配不同的模拟结果。你可以通过 Process
facade 的 sequence
方法来实现这一点:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
伪造异步进程生命周期
到目前为止,我们主要讨论了使用 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
facade 的 describe
方法:
Process::fake([
'bash import.sh' => Process::describe()
->output('First line of standard output')
->errorOutput('First line of error output')
->output('Second line of standard 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
方法也接受一个闭包,该闭包将接收一个进程实例和一个进程结果实例,允许你检查进程的配置选项。如果闭包返回 true
且进程被调用了指定的次数,则断言 “通过”:
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
防止孤立进程
如果你希望确保在单个测试或整个测试套件中所有调用的进程都被伪造(fake),可以调用 preventStrayProcesses
方法。调用此方法后,任何没有对应伪造结果的进程将抛出异常,而不是启动实际的进程:
use Illuminate\Support\Facades\Process;
Process::preventStrayProcesses();
Process::fake([
'ls *' => 'Test output...',
]);
// 返回伪造的响应...
Process::run('ls -la');
// 抛出异常...
Process::run('bash import.sh');