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

# Running external processes in packages and Process::fake

> Uses the Laravel 13 Process facade to explain external command arguments, timeouts, failure exceptions, fakes in tests and matching array commands.

When a package calls external commands such as `ffmpeg` or `git`, differences in the host application's environment and command failures become the package's behavior. Instead of calling `shell_exec()` or `exec()` directly, run commands through the **Process facade** and replace them with `Process::fake()` in tests.

> [!NOTE]
>
> This page targets Laravel 13 and checks the implementation of Framework `v13.42.0`. It assumes [processes](https://kawax.biz/en/processes.md) and [package testing](https://kawax.biz/en/advanced/package-testing.md). The array command matching covered here relies on how Symfony Process, which Process uses internally, converts arguments.

## Why to avoid direct execution

Direct function calls cannot be swapped out in tests, and each caller handles timeouts and exit codes differently. Going through the Process facade gives you one consistent way to record runs, fake them, apply timeouts and throw exceptions on failure.

```mermaid theme={null}
flowchart LR
    A["Package<br>wrapper class"] --> B["Process facade<br>PendingProcess"]
    B --> C{"Fake<br>registered?"}
    C -->|Yes| D["FakeProcessResult<br>no external command starts"]
    C -->|No| E["Start the external command"]
    E --> F["ProcessResult"]
    D --> F
    F --> G["throw() turns failures into exceptions"]
```

## Pass commands as arrays

A string command is interpreted by `sh`, so including user input opens the door to quoting problems and injection. Inside a package, pass arguments as an array.

```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();
    }
}
```

The default timeout of `PendingProcess` is `60` seconds, and the idle timeout is `null`. Set `timeout()` explicitly for long conversions, and add `idleTimeout()` for work whose output can stall.

## Exceptions and messages on failure

`ProcessResult::throw()` throws `ProcessFailedException` when the exit code is not successful. The message contains the command that ran, the exit code, and the standard output and standard error output.

> [!WARNING]
>
> If you put secrets in command arguments, they end up in the `ProcessFailedException` message and can appear in logs or on screen. Do not pass tokens as arguments. Pass them with `env()` or write them to the command's configuration file instead.

Timeouts are represented by `ProcessTimedOutException`, and idle timeouts by `ProcessIdleTimedOutException`. Document for your package's users which exceptions you throw and what they can expect when they catch them.

## Fake in tests

In tests, replace external commands with `Process::fake()`. `Process::result()` lets you specify the standard output, the standard error output and the exit code.

```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');
    }
}
```

When you pass a closure to `assertRan()`, you can check the `command`, `timeout`, `environment` and other properties of `PendingProcess`. Array commands are compared with `$process->command` by exact match, so tests detect changes to the order or values of arguments.

## Fake keys for array commands

Internally, Process matches the keys passed to `fake()` against the result of `getCommandline()` with `Str::is()`. Array commands are converted into a quoted string for the shell, so a string pattern such as `'ffmpeg*'` does not match. For tests of array commands, use one of the following.

| Approach | When to use it | Caveat |
| - | - | - |
| The `'*'` key | Return the same result regardless of the command | Hard to notice when a different command is called |
| `Process::assertRan()` | Verify arguments after the call | Prepare the fake response separately |
| A closure fake | Vary the result by argument | Logic gets complex, so keep the test's purpose narrow |

## Prevent unexpected external commands

To detect external commands that should not run in tests, use `preventStrayProcesses()`. Commands that do not match a fake throw a `RuntimeException` and never actually start.

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

    Process::preventStrayProcesses();
}
```

## When you use an injected Factory

If your package's constructor receives `Illuminate\Process\Factory`, check whether the `Process::fake()` state reaches the injected instance. The facade caches the resolved instance, and `swap()` replaces the instance in the container. Whether `Factory` is shared in the container, however, is not something you can rely on the framework to register explicitly, so verify it in a test.

If the fake does not reach the injected instance, swap the Factory explicitly in the test or design the package to go through the `Process` facade. In either case, confirming with `assertRan()` that the call was recorded lets you catch a missed swap.

## Checks to add for long-term maintenance

| Check | Purpose |
| - | - |
| A test that pins the order of array arguments | Notice when the position of `-y` or the input and output changes |
| Checking the exception and message on failure | Keep users' error handling unchanged |
| Checking the defaults of `timeout()` and `idleTimeout()` | Treat a change of defaults as a change in behavior for users |
| Enabling `preventStrayProcesses()` | Stop unexpected external commands in tests |
| Checking that no secrets are passed as arguments | Prevent leaks into exception messages and logs |

## Primary sources

* [Laravel docs: Processes](https://github.com/laravel/docs/blob/13.x/processes.md)
* [Laravel docs: Process::fake and assertRan](https://github.com/laravel/docs/blob/13.x/processes.md#faking-processes)
* [Process Factory: fake and assertRan](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/Factory.php)
* [PendingProcess: Default timeouts and fake matching](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/PendingProcess.php)
* [ProcessResult: Behavior of throw()](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/ProcessResult.php)
* [ProcessFailedException: Structure of the message](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Process/Exceptions/ProcessFailedException.php)
* [Facade: Caching and swap()](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Facades/Facade.php)


## Related topics

- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Processes](https://kawax.biz/en/processes.md)
- [Overriding and updating package views](https://kawax.biz/en/advanced/package-views.md)
- [Registering and caching package event listeners](https://kawax.biz/en/advanced/package-events.md)
- [WebSocket (Jetstream / Firehose)](https://kawax.biz/en/packages/laravel-bluesky/websocket.md)


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