Skip to content
全部文档

进程

简介

Laravel 在 Symfony Process 组件 之上提供了一套富有表现力且精简的 API,便于你从 Laravel 应用中调用外部进程。Laravel 的进程功能聚焦于最常见的用例,并致力于出色的开发体验。

调用进程

要调用进程,可使用 Process Facade 提供的 runstart 方法。run 会调用进程并等待其执行完毕,而 start 用于异步执行。本文将分别介绍这两种方式。首先来看如何调用一个基本的同步进程并检查其结果:

php
use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

return $result->output();

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

php
$result = Process::run('ls -la');

$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();

抛出异常

若已有进程结果,并希望在退出码大于零(表示失败)时抛出 Illuminate\Process\Exceptions\ProcessFailedException,可使用 throwthrowIf 方法。若进程未失败,则返回 ProcessResult 实例:

php
$result = Process::run('ls -la')->throw();

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

进程选项

当然,在调用前你可能需要自定义进程行为。好在 Laravel 允许你调整多种进程特性,例如工作目录、超时与环境变量。

工作目录路径

可使用 path 方法指定进程的工作目录。若未调用该方法,进程将继承当前执行中 PHP 脚本的工作目录:

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

输入

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

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

超时

默认情况下,进程执行超过 60 秒后会抛出 Illuminate\Process\Exceptions\ProcessTimedOutException。不过,你可通过 timeout 方法自定义该行为:

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

或者,若希望完全禁用进程超时,可调用 forever 方法:

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

idleTimeout 方法可用于指定进程在无任何输出的情况下最多可运行的秒数:

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

环境变量

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

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

若希望从被调用进程中移除某个继承的环境变量,可将该环境变量的值设为 false

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

TTY 模式

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

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

进程输出

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

php
use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

echo $result->output();
echo $result->errorOutput();

不过,也可通过将闭包作为第二个参数传给 run 方法,实时收集输出。闭包会接收两个参数:输出「类型」(stdoutstderr)以及输出字符串本身:

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

Laravel 还提供 seeInOutputseeInErrorOutput 方法,可方便地判断给定字符串是否包含在进程输出中:

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

禁用进程输出

若进程会写入大量你不关心的输出,可通过完全禁用输出检索来节省内存。为此,在构建进程时调用 quietly 方法:

php
use Illuminate\Support\Facades\Process;

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

管道

有时你可能希望将一个进程的输出作为另一个进程的输入。这通常称为将进程输出「管道」到另一个进程。Process Facade 提供的 pipe 方法让此事变得简单。pipe 会同步执行管道中的进程,并返回管道中最后一个进程的结果:

php
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 方法传入命令字符串数组:

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

可通过将闭包作为第二个参数传给 pipe 方法,实时收集进程输出。闭包会接收两个参数:输出「类型」(stdoutstderr)以及输出字符串本身:

php
$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 方法的输出闭包,从而判断输出属于哪个进程:

php
$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 方法判断进程是否仍在运行:

php
$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
    // ...
}

$result = $process->wait();

如你所见,可调用 wait 方法等待进程执行完毕并获取 ProcessResult 实例:

php
$process = Process::timeout(120)->start('bash import.sh');

// ...

$result = $process->wait();

进程 ID 与信号

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

php
$process = Process::start('bash import.sh');

return $process->id();

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

php
$process->signal(SIGUSR2);

异步进程输出

异步进程运行时,可使用 outputerrorOutput 方法访问其当前全部输出;也可使用 latestOutputlatestErrorOutput 访问自上次获取输出以来新增的输出:

php
$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
    echo $process->latestOutput();
    echo $process->latestErrorOutput();

    sleep(1);
}

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

php
$process = Process::start('bash import.sh', function (string $type, string $output) {
    echo $output;
});

$result = $process->wait();

若不需要等到进程结束,可使用 waitUntil 方法根据进程输出停止等待。当传给 waitUntil 的闭包返回 true 时,Laravel 将停止等待进程结束:

php
$process = Process::start('bash import.sh');

$process->waitUntil(function (string $type, string $output) {
    return $output === 'Ready...';
});

并发进程

Laravel 也让管理并发异步进程池变得轻松,便于同时执行多项任务。首先调用 pool 方法,它接受一个接收 Illuminate\Process\Pool 实例的闭包。

在该闭包中,可定义属于该池的进程。通过 start 方法启动进程池后,可使用 running 方法访问正在运行的进程集合

php
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 实例:

php
$results = $pool->wait();

echo $results[0]->output();

或者,为方便起见,可使用 concurrently 方法启动异步进程池并立即等待其结果。与 PHP 的数组解构结合时,语法尤其富有表现力:

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 方法的闭包,从而判断输出属于哪个进程:

php
$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:

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

此外,为方便起见,可在进程池上调用 signal 方法,向池中每个进程发送信号:

php
$pool->signal(SIGUSR2);

测试

许多 Laravel 服务都提供便于你轻松、富有表现力地编写测试的功能,进程服务也不例外。Process Facade 的 fake 方法可指示 Laravel 在调用进程时返回桩 / 虚拟结果。

伪造进程

为了解 Laravel 伪造进程的能力,假设有一个会调用进程的路由:

php
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
<?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
<?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 方法轻松指定伪造进程的输出与退出码:

php
Process::fake([
    '*' => Process::result(
        output: 'Test output',
        errorOutput: 'Test error output',
        exitCode: 1,
    ),
]);

伪造特定进程

如你在前面示例中所见,Process Facade 允许通过向 fake 方法传入数组,为每个进程指定不同的伪造结果。

数组的键应表示希望伪造的命令模式及其关联结果。* 字符可用作通配符。未被伪造的进程命令仍会被实际调用。可使用 Process Facade 的 result 方法为这些命令构造桩 / 伪造结果:

php
Process::fake([
    'cat *' => Process::result(
        output: 'Test "cat" output',
    ),
    'ls *' => Process::result(
        output: 'Test "ls" output',
    ),
]);

若无需自定义伪造进程的退出码或错误输出,将伪造进程结果指定为简单字符串可能更方便:

php
Process::fake([
    'cat *' => 'Test "cat" output',
    'ls *' => 'Test "ls" output',
]);

伪造进程序列

若被测代码会用相同命令多次调用进程,你可能希望为每次调用分配不同的伪造结果。可通过 Process Facade 的 sequence 方法实现:

php
Process::fake([
    'ls *' => Process::sequence()
        ->push(Process::result('First invocation'))
        ->push(Process::result('Second invocation')),
]);

伪造异步进程生命周期

到目前为止,我们主要讨论了使用 run 同步调用的进程伪造。但若要测试与通过 start 调用的异步进程交互的代码,可能需要更精细的方式来描述伪造进程。

例如,假设有以下与异步进程交互的路由:

php
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 方法:

php
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),
]);

来看上面的例子。使用 outputerrorOutput 方法可指定按顺序返回的多行输出。exitCode 方法用于指定伪造进程的最终退出码。最后,iterations 方法用于指定 running 方法应返回多少次 true

可用断言

前文所述,Laravel 为功能测试提供了多种进程断言。下面将逐一介绍。

assertRan

断言给定进程已被调用:

php
use Illuminate\Support\Facades\Process;

Process::assertRan('ls -la');

assertRan 方法也接受闭包,闭包会接收进程实例与进程结果,便于检查进程的配置选项。若闭包返回 true,断言将「通过」:

php
Process::assertRan(fn ($process, $result) =>
    $process->command === 'ls -la' &&
    $process->path === __DIR__ &&
    $process->timeout === 60
);

传给 assertRan 闭包的 $processIlluminate\Process\PendingProcess 实例,而 $resultIlluminate\Contracts\Process\ProcessResult 实例。

assertDidntRun

断言给定进程未被调用:

php
use Illuminate\Support\Facades\Process;

Process::assertDidntRun('ls -la');

assertRan 类似,assertDidntRun 也接受闭包,闭包会接收进程实例与进程结果,便于检查配置选项。若闭包返回 true,断言将「失败」:

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

assertRanTimes

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

php
use Illuminate\Support\Facades\Process;

Process::assertRanTimes('ls -la', times: 3);

assertRanTimes 方法也接受闭包,闭包会接收进程实例与进程结果,便于检查进程的配置选项。若闭包返回 true 且进程被调用了指定次数,断言将「通过」:

php
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');