> ## 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

> 說明如何使用 Laravel 的 Process facade 執行外部 shell 指令，簡潔地處理同步、非同步與並行處理。

## 前言

Laravel 的 Process facade 是 [Symfony Process 元件](https://symfony.com/doc/current/components/process.html) 的輕量包裝。
提供從 Laravel 應用程式呼叫 shell 指令並處理結果的簡潔 API。

```mermaid theme={null}
flowchart LR
    A["Laravel 應用"] --> B["Process<br>facade"]
    B --> C["shell 指令"]
    C --> D["ProcessResult<br>(output / exitCode)"]
    D --> A
```

## 啟動 process

使用 `Process::run()` 可同步執行指令並取得結果。

```php theme={null}
use Illuminate\Support\Facades\Process;

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

return $result->output();
```

`ProcessResult` 實例提供了各種確認結果的方法。

```php theme={null}
$result = Process::run('ls -la');

$result->command();       // 已執行的指令
$result->successful();    // 是否成功（結束碼 0）
$result->failed();        // 是否失敗（結束碼非 0）
$result->output();        // 標準輸出（stdout）
$result->errorOutput();   // 標準錯誤輸出（stderr）
$result->exitCode();      // 結束碼
```

### 失敗時擲出例外

若要在結束碼大於 0 時擲出 `Illuminate\Process\Exceptions\ProcessFailedException`，可用 `throw()` 或 `throwIf()`。

```php theme={null}
$result = Process::run('ls -la')->throw();

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

## Process 選項

### 工作目錄

可用 `path()` 指定工作目錄。省略時會使用目前 PHP 腳本的工作目錄。

```php theme={null}
$result = Process::path(__DIR__)->run('ls -la');
```

### 標準輸入

可用 `input()` 將資料傳給 process 的標準輸入。

```php theme={null}
$result = Process::input('Hello World')->run('cat');
```

### 逾時（timeout）

預設為 60 秒。可用 `timeout()` 變更。逾時時 process 會被中斷，並擲出 `ProcessTimedOutException`。

```php theme={null}
$result = Process::timeout(120)->run('bash import.sh');
```

也可使用 `CarbonInterval` 輔助函式。

```php theme={null}
use function Illuminate\Support\minutes;

$result = Process::timeout(minutes(2))->run('bash import.sh');
```

若要停用逾時，可用 `forever()`。

```php theme={null}
$result = Process::forever()->run('bash import.sh');
```

也能設定閒置逾時（沒有輸出的持續秒數）。

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

### 環境變數

可用 `env()` 新增或覆寫環境變數。系統的環境變數會自動被繼承。

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

若要排除繼承的變數，指定 `false`。

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

### 停用輸出

若不需要大量輸出，可用 `quietly()` 抑制記憶體消耗。

```php theme={null}
$result = Process::quietly()->run('bash import.sh');
```

### 即時輸出

在 `run()` 的第 2 個引數傳入 closure，可即時接收輸出。

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

## Pipeline

使用 `Process::pipe()` 可以把某個指令的輸出作為下一個指令的輸入。

```php theme={null}
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()) {
    // ...
}
```

也可以傳入指令字串的陣列。

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

用 `as()` 為每個 process 加上 key，就能在輸出 closure 中判別是哪個 process 的輸出。

```php theme={null}
$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) {
    // $key 為 'first' 或 'second'
});
```

## 非同步 process

使用 `Process::start()` 可非同步啟動 process。process 執行期間，應用程式可繼續其他處理。

```php theme={null}
$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
    // 其他處理
}

$result = $process->wait();
```

### Process ID 與 signal

可用 `id()` 取得執行中 process 的作業系統 process ID。

```php theme={null}
$process = Process::start('bash import.sh');

return $process->id();
```

以 `signal()` 送出 signal 到 process。

```php theme={null}
$process->signal(SIGUSR2);
```

### 非同步 process 的輸出

執行中使用 `latestOutput()` 與 `latestErrorOutput()` 可取得自上次取得以來的差量輸出。

```php theme={null}
$process = Process::timeout(120)->start('bash import.sh');

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

    sleep(1);
}
```

要等到特定輸出出現時使用 `waitUntil()`。

```php theme={null}
$process = Process::start('bash import.sh');

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

### 非同步 process 的逾時確認

在迴圈中呼叫 `ensureNotTimedOut()`，若已逾時會擲出例外。

```php theme={null}
$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
    $process->ensureNotTimedOut();

    sleep(1);
}
```

## 並行 process

使用 `Process::pool()` 可以並行執行多個 process。

```php theme={null}
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();
```

使用 `concurrently()` 可用一行寫立即啟動 pool 並等待結果的處理。

```php theme={null}
[$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();
```

### 為 process 命名

以 `as()` 為每個 process 加上字串 key，可讓結果取得更清楚。

```php theme={null}
$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();
```

也可對整個 pool 送出 signal。

```php theme={null}
$pool->signal(SIGUSR2);
```

## 測試

### Process 的偽造（fake）

用 `Process::fake()` 可以在不實際執行 shell 指令的情況下進行測試。

```php theme={null}
use Illuminate\Support\Facades\Process;

Process::fake();

$response = $this->get('/import');

Process::assertRan('bash import.sh');
```

也能指定輸出與結束碼。

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

### 偽造特定指令

可用萬用字元或指令字串作為 key 設定個別的 fake。

```php theme={null}
Process::fake([
    'cat *'    => Process::result(output: 'file contents'),
    'ls -la'   => Process::result(output: 'file listing'),
]);
```

### 序列（sequence）的偽造

當同一個指令被呼叫多次時，可指定回傳結果的順序。

```php theme={null}
Process::fake([
    'ls *' => [
        Process::result('first time'),
        Process::result('second time'),
    ],
]);
```

### 斷言

| 方法                                      | 說明                 |
| --------------------------------------- | ------------------ |
| `Process::assertRan('command')`         | 驗證指令已執行            |
| `Process::assertNotRan('command')`      | 驗證指令未執行            |
| `Process::assertRan(fn)`                | 以 closure 詳細驗證執行內容 |
| `Process::assertRanTimes('command', 3)` | 驗證指令執行了指定次數        |

### 防止非預期的 process

呼叫 `Process::preventStrayProcesses()` 時，若執行了未設定 fake 的指令便會擲出例外。

```php theme={null}
Process::preventStrayProcesses();

Process::fake([
    'ls *' => Process::result('file listing'),
]);

// 例外：'bash import.sh' 未設定 fake
Process::run('bash import.sh');
```

## 相關頁面

<Columns cols={2}>
  <Card title="Artisan Console" icon="terminal" href="/zh-TW/artisan">
    建立自訂指令來呼叫 process
  </Card>

  <Card title="Queue" icon="list" href="/zh-TW/queues">
    與非同步處理的比較
  </Card>
</Columns>


## Related topics

- [Artisan Console](/zh-TW/artisan.md)
- [Laravel Reverb](/zh-TW/reverb.md)
- [並行處理](/zh-TW/concurrency.md)
- [Queue Job 的執行控制](/zh-TW/advanced/queue-job-control.md)
- [測試入門](/zh-TW/testing.md)
