> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包中的外部进程执行与 Process::fake

> 使用 Laravel 13 的 Process facade，讲解外部命令的参数、超时、失败异常、测试中的 fake 以及数组命令的匹配。

包调用 `ffmpeg` 或 `git` 等外部命令时，使用方应用的环境差异和失败会直接变成包的行为。不要直接调用 `shell_exec()` 或 `exec()`，而是通过 **Process facade** 执行，并在测试中用 `Process::fake()` 替换。

> [!NOTE]
>
> 本页面向 Laravel 13，确认了 Framework `v13.42.0` 的实现。前提是已了解[进程](https://kawax.biz/zh-CN/processes.md)与[包的测试](https://kawax.biz/zh-CN/advanced/package-testing.md)。这里介绍的数组命令匹配，以 Process 内部使用的 Symfony Process 的参数转换为前提。

## 避免直接执行的理由

直接调用函数无法在测试中替换，超时和退出码的处理也会因调用方而各不相同。通过 Process facade，可以统一执行记录、fake、超时以及失败时的异常。

```mermaid theme={null}
flowchart LR
    A["包的<br>包装类"] --> B["Process facade<br>PendingProcess"]
    B --> C{"是否已<br>注册 fake?"}
    C -->|是| D["FakeProcessResult<br>不启动外部命令"]
    C -->|否| E["启动外部命令"]
    E --> F["ProcessResult"]
    D --> F
    F --> G["用 throw() 将失败转为异常"]
```

## 以数组传递命令

字符串命令会经过 `sh` 解析，如果包含用户输入，就会产生引号问题和注入的可能。在包内部请以数组传递参数。

```php theme={null}
<?php

namespace Vendor\Package\Support;

use Illuminate\Support\Facades\Process;

final class VideoConverter
{
    public function toMp4(string $source, string $target): void
    {
        Process::timeout(120)
            ->run(['ffmpeg', '-y', '-i', $source, $target])
            ->throw();
    }
}
```

`PendingProcess` 的默认超时为 `60` 秒，空闲超时为 `null`。耗时较长的转换请用 `timeout()` 明确指定；对于输出可能停滞的处理，请同时使用 `idleTimeout()`。

## 失败时的异常与消息

当退出码不表示成功时，`ProcessResult::throw()` 会抛出 `ProcessFailedException`。消息中包含所执行的命令、退出码，以及标准输出和标准错误输出。

> [!WARNING]
>
> 如果把机密信息放进命令参数，它会被包含在 `ProcessFailedException` 的消息中，可能出现在日志或页面上。不要把令牌等作为参数传递，而是通过 `env()` 传递，或写入命令的配置文件。

超时由 `ProcessTimedOutException` 表示，空闲超时由 `ProcessIdleTimedOutException` 表示。请向包的使用者明确说明会抛出哪些异常，以及捕获时可以期待什么。

## 在测试中 fake

在测试中用 `Process::fake()` 替换外部命令。`Process::result()` 可以指定标准输出、标准错误和退出码。

```php theme={null}
<?php

namespace Vendor\Package\Tests\Feature;

use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\Exceptions\ProcessFailedException;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
use Vendor\Package\Support\VideoConverter;
use Vendor\Package\Tests\TestCase;

class VideoConverterTest extends TestCase
{
    public function test_it_runs_ffmpeg_with_the_expected_arguments(): void
    {
        Process::fake([
            '*' => Process::result(output: 'converted'),
        ]);

        (new VideoConverter)->toMp4('/tmp/in.mov', '/tmp/out.mp4');

        Process::assertRan(
            fn (PendingProcess $process, ProcessResult $result) => $process->command === ['ffmpeg', '-y', '-i', '/tmp/in.mov', '/tmp/out.mp4']
        );
    }

    public function test_it_fails_when_ffmpeg_exits_with_an_error(): void
    {
        Process::fake([
            '*' => Process::result(errorOutput: 'Invalid data', exitCode: 1),
        ]);

        $this->expectException(ProcessFailedException::class);

        (new VideoConverter)->toMp4('/tmp/in.mov', '/tmp/out.mp4');
    }
}
```

向 `assertRan()` 传入闭包，可以确认 `PendingProcess` 的 `command`、`timeout`、`environment` 等。数组命令会与 `$process->command` 进行完全一致比较，因此参数顺序或值的变化会在测试中被检测出来。

## 数组命令的 fake 键

Process 内部使用 `Str::is()` 将 `fake()` 的键与 `getCommandline()` 的结果进行匹配。数组命令会被转换为面向 shell 的带引号字符串，因此 `'ffmpeg*'` 这样的字符串模式不会匹配。在数组命令的测试中，请使用以下方式之一。

| 方式 | 适用场景 | 注意点 |
| - | - | - |
| `'*'` 键 | 无论命令如何都返回相同结果 | 即使调用了其他命令也难以察觉 |
| `Process::assertRan()` | 在调用后验证参数 | fake 的响应需另行准备 |
| 闭包 fake | 根据参数改变结果 | 处理会变复杂，因此要收窄测试目的 |

## 防止意外的外部命令

要检测测试中不应执行的外部命令，请使用 `preventStrayProcesses()`。与 fake 不匹配的命令会抛出 `RuntimeException`，并不会实际启动。

```php theme={null}
protected function setUp(): void
{
    parent::setUp();

    Process::preventStrayProcesses();
}
```

## 使用注入的 Factory 时

如果包的构造函数接收 `Illuminate\Process\Factory`，请确认 `Process::fake()` 的状态能否到达注入目标。Facade 会缓存已解析的实例，`swap()` 会替换容器中的实例。另一方面，`Factory` 是否在容器中共享，无法依赖框架的显式注册，因此请在测试中实际确认。

如果无法到达注入目标，请在测试中显式替换 Factory，或将设计改为经由 `Process` facade。无论哪种情况，用 `assertRan()` 确认调用已被记录，都能发现遗漏的替换。

## 为长期维护追加的验证

| 验证 | 目的 |
| - | - |
| 固定数组参数顺序的测试 | 在 `-y` 或输入输出的位置发生变化时察觉 |
| 确认失败时的异常与消息 | 保持使用者的错误处理不变 |
| 确认 `timeout()` 与 `idleTimeout()` 的默认值 | 将默认值的变化视为使用者的行为变化 |
| 启用 `preventStrayProcesses()` | 在测试中阻止意外的外部命令启动 |
| 确认未把机密信息放进参数 | 防止泄露到异常消息和日志中 |

## 参考的一手资料

* [Laravel 官方文档：进程](https://github.com/laravel/docs/blob/13.x/processes.md)
* [Laravel 官方文档：Process::fake 与 assertRan](https://github.com/laravel/docs/blob/13.x/processes.md#faking-processes)
* [Process Factory：fake 与 assertRan](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/Factory.php)
* [PendingProcess：默认超时与 fake 的匹配](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/PendingProcess.php)
* [ProcessResult：throw() 的行为](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/ProcessResult.php)
* [ProcessFailedException：消息的构成](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/Exceptions/ProcessFailedException.php)
* [Facade：缓存与 swap()](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Facades/Facade.php)


## Related topics

- [进阶主题](https://kawax.biz/zh-CN/advanced/index.md)
- [进程](https://kawax.biz/zh-CN/processes.md)
- [测试](https://kawax.biz/zh-CN/packages/laravel-bluesky/testing.md)
- [并发处理](https://kawax.biz/zh-CN/concurrency.md)
- [测试入门](https://kawax.biz/zh-CN/testing.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.