> ## 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-TW/processes.md)與[套件的測試](https://kawax.biz/zh-TW/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-TW/advanced/index.md)
- [測試](https://kawax.biz/zh-TW/packages/laravel-bluesky/testing.md)
- [並行處理](https://kawax.biz/zh-TW/concurrency.md)
- [套件的 HTTP 用戶端與測試](https://kawax.biz/zh-TW/advanced/package-http-client.md)
- [套件的 Queue Job 與更新時的相容性](https://kawax.biz/zh-TW/advanced/package-queue-jobs.md)


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