Laravel 提供了一个富有表现力、极简的 API,围绕着 Symfony Process 组件,让您可以方便地从 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);Of course, you may need to customize the behavior of a process before invoking it. Thankfully, Laravel allows you to tweak a variety of process features, such as the working directory, timeout, and environment variables.
You may use the path method to specify the working directory of the process. If this method is not invoked, the process will inherit the working directory of the currently executing PHP script:
$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');或者,如果您想完全禁用进程超时,您可以调用 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 模式将进程的输入和输出连接到程序的输入和输出,从而允许您的进程以进程的形式打开诸如 Vim 或 Nano 之类的编辑器:
Process::forever()->tty()->run('vim');[!WARNING]
TTY 模式在 Windows 上不受支持。
正如之前讨论的,进程输出可以使用进程结果上的 output (标准输出) 和 errorOutput (标准错误) 方法来访问:
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:
$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 方法,从异步进程中实时收集。该闭包将接收两个参数:输出的 "type"(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...';
});当异步进程运行时,您可以使用 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();由于进程池的 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 facade 上的 fake 方法,指示 Laravel 为每个被调用的进程返回一个伪造的、成功的进程结果。此外,我们甚至可以断言某个进程被“运行”了:
<?php
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
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\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');
// 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 门面允许您通过将一个数组传递给 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 门面的 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 为你的功能测试提供了若干进程断言。我们将在下文讨论这些断言中的每一个。
断言给定进程已被调用:
use Illuminate\Support\Facades\Process;
Process::assertRan('ls -la');The assertRan method also accepts a closure, which will receive an instance of a process and a process result, allowing you to inspect the process' configured options. If this closure returns true, the assertion will "pass":
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 的一个实例。
断言给定进程未被调用:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');与 assertRan 方法类似,assertDidntRun 方法也接受一个闭包,该闭包将接收一个进程实例和一个进程结果,以便你检查该进程的配置选项。如果此闭包返回 true,则断言将“失败”:
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);断言某个给定进程被调用了给定次数:
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 *' => 'Test output...',
]);
// Fake response is returned...
Process::run('ls -la');
// An exception is thrown...
Process::run('bash import.sh');