Introduction
Laravel provides an expressive, minimal API around the Symfony Process component, allowing you to conveniently invoke external processes from your Laravel application. Laravel's process features are focused on the most common use cases and a wonderful developer experience.
Invoking Processes
To invoke a process, you may use the run
and
start
methods offered by the Process
facade.
The run
method will invoke a process and wait for the
process to finish executing, while the start
method is used
for asynchronous process execution. We'll examine both approaches within
this documentation. First, let's examine how to invoke a basic,
synchronous process and inspect its result:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
return $result->output();
Of course, the
Illuminate\Contracts\Process\ProcessResult
instance
returned by the run
method offers a variety of helpful
methods that may be used to inspect the process result:
$result = Process::run('ls -la');
$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();
Throwing Exceptions
If you have a process result and would like to throw an instance of
Illuminate\Process\Exceptions\ProcessFailedException
if the
exit code is greater than zero (thus indicating failure), you may use
the throw
and throwIf
methods. If the process
did not fail, the process result instance will be returned:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
Process Options
Of course, you may need to customize the behavior of a process before invoking it. Thankfully, Laravel allows you to tweak a variety of process features, such as the working directory, timeout, and environment variables.
Working Directory Path
You may use the path
method to specify the working
directory of the process. If this method is not invoked, the process
will inherit the working directory of the currently executing PHP
script:
$result = Process::path(__DIR__)->run('ls -la');
Input
You may provide input via the "standard input" of the process using
the input
method:
$result = Process::input('Hello World')->run('cat');
Timeouts
By default, processes will throw an instance of
Illuminate\Process\Exceptions\ProcessTimedOutException
after executing for more than 60 seconds. However, you can customize
this behavior via the timeout
method:
$result = Process::timeout(120)->run('bash import.sh');
Or, if you would like to disable the process timeout entirely, you
may invoke the forever
method:
$result = Process::forever()->run('bash import.sh');
The idleTimeout
method may be used to specify the
maximum number of seconds the process may run without returning any
output:
$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
Environment Variables
Environment variables may be provided to the process via the
env
method. The invoked process will also inherit all of
the environment variables defined by your system:
$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');
If you wish to remove an inherited environment variable from the
invoked process, you may provide that environment variable with a value
of false
:
$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');
TTY Mode
The tty
method may be used to enable TTY mode for your
process. TTY mode connects the input and output of the process to the
input and output of your program, allowing your process to open an
editor like Vim or Nano as a process:
Process::forever()->tty()->run('vim');
Process Output
As previously discussed, process output may be accessed using the
output
(stdout) and errorOutput
(stderr)
methods on a process result:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();
However, output may also be gathered in real-time by passing a
closure as the second argument to the run
method. The
closure will receive two arguments: the "type" of output
(stdout
or stderr
) and the output string
itself:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});
Laravel also offers the seeInOutput
and
seeInErrorOutput
methods, which provide a convenient way to
determine if a given string was contained in the process' output:
if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}
Disabling Process Output
If your process is writing a significant amount of output that you
are not interested in, you can conserve memory by disabling output
retrieval entirely. To accomplish this, invoke the quietly
method while building the process:
use Illuminate\Support\Facades\Process;
$result = Process::quietly()->run('bash import.sh');
Pipelines
Sometimes you may want to make the output of one process the input of
another process. This is often referred to as "piping" the output of a
process into another. The pipe
method provided by the
Process
facades makes this easy to accomplish. The
pipe
method will execute the piped processes synchronously
and return the process result for the last process in the pipeline:
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()) {
// ...
}
If you do not need to customize the individual processes that make up
the pipeline, you may simply pass an array of command strings to the
pipe
method:
$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);
The process output may be gathered in real-time by passing a closure
as the second argument to the pipe
method. The closure will
receive two arguments: the "type" of output (stdout
or
stderr
) and the output string itself:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});
Laravel also allows you to assign string keys to each process within
a pipeline via the as
method. This key will also be passed
to the output closure provided to the pipe
method, allowing
you to determine which process the output belongs to:
$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) {
// ...
});
Asynchronous Processes
While the run
method invokes processes synchronously,
the start
method may be used to invoke a process
asynchronously. This allows your application to continue performing
other tasks while the process runs in the background. Once the process
has been invoked, you may utilize the running
method to
determine if the process is still running:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
// ...
}
$result = $process->wait();
As you may have noticed, you may invoke the wait
method
to wait until the process is finished executing and retrieve the process
result instance:
$process = Process::timeout(120)->start('bash import.sh');
// ...
$result = $process->wait();
Process IDs & Signals
The id
method may be used to retrieve the operating
system assigned process ID of the running process:
$process = Process::start('bash import.sh');
return $process->id();
You may use the signal
method to send a "signal" to the
running process. A list of predefined signal constants can be found
within the PHP
documentation:
$process->signal(SIGUSR2);
Asynchronous Process Output
While an asynchronous process is running, you may access its entire
current output using the output
and
errorOutput
methods; however, you may utilize the
latestOutput
and latestErrorOutput
to access
the output from the process that has occurred since the output was last
retrieved:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
sleep(1);
}
Like the run
method, output may also be gathered in
real-time from asynchronous processes by passing a closure as the second
argument to the start
method. The closure will receive two
arguments: the "type" of output (stdout
or
stderr
) and the output string itself:
$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
$result = $process->wait();
Concurrent Processes
Laravel also makes it a breeze to manage a pool of concurrent,
asynchronous processes, allowing you to easily execute many tasks
simultaneously. To get started, invoke the pool
method,
which accepts a closure that receives an instance of
Illuminate\Process\Pool
.
Within this closure, you may define the processes that belong to the
pool. Once a process pool is started via the start
method,
you may access the collection of running
processes via the running
method:
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();
As you can see, you may wait for all of the pool processes to finish
executing and resolve their results via the wait
method.
The wait
method returns an array accessible object that
allows you to access the process result instance of each process in the
pool by its key:
$results = $pool->wait();
echo $results[0]->output();
Or, for convenience, the concurrently
method may be used
to start an asynchronous process pool and immediately wait on its
results. This can provide particularly expressive syntax when combined
with PHP's array destructuring capabilities:
[$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();
Naming Pool Processes
Accessing process pool results via a numeric key is not very
expressive; therefore, Laravel allows you to assign string keys to each
process within a pool via the as
method. This key will also
be passed to the closure provided to the start
method,
allowing you to determine which process the output belongs to:
$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 Process IDs & Signals
Since the process pool's running
method provides a
collection of all invoked processes within the pool, you may easily
access the underlying pool process IDs:
$processIds = $pool->running()->each->id();
And, for convenience, you may invoke the signal
method
on a process pool to send a signal to every process within the pool:
$pool->signal(SIGUSR2);
Testing
Many Laravel services provide functionality to help you easily and
expressively write tests, and Laravel's process service is no exception.
The Process
facade's fake
method allows you to
instruct Laravel to return stubbed / dummy results when processes are
invoked.
Faking Processes
To explore Laravel's ability to fake processes, let's imagine a route that invokes a process:
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
Process::run('bash import.sh');
return 'Import complete!';
});
When testing this route, we can instruct Laravel to return a fake,
successful process result for every invoked process by calling the
fake
method on the Process
facade with no
arguments. In addition, we can even assert that a given process was
"run":
<?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('/');
// 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;
});
}
}
As discussed, invoking the fake
method on the
Process
facade will instruct Laravel to always return a
successful process result with no output. However, you may easily
specify the output and exit code for faked processes using the
Process
facade's result
method:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
Faking Specific Processes
As you may have noticed in a previous example, the
Process
facade allows you to specify different fake results
per process by passing an array to the fake
method.
The array's keys should represent command patterns that you wish to
fake and their associated results. The *
character may be
used as a wildcard character. Any process commands that have not been
faked will actually be invoked. You may use the Process
facade's result
method to construct stub / fake results for
these commands:
Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);
If you do not need to customize the exit code or error output of a faked process, you may find it more convenient to specify the fake process results as simple strings:
Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);
Faking Process Sequences
If the code you are testing invokes multiple processes with the same
command, you may wish to assign a different fake process result to each
process invocation. You may accomplish this via the Process
facade's sequence
method:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
Faking Asynchronous Process Lifecycles
Thus far, we have primarily discussed faking processes which are
invoked synchronously using the run
method. However, if you
are attempting to test code that interacts with asynchronous processes
invoked via start
, you may need a more sophisticated
approach to describing your fake processes.
For example, let's imagine the following route which interacts with an asynchronous process:
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';
});
To properly fake this process, we need to be able to describe how
many times the running
method should return
true
. In addition, we may want to specify multiple lines of
output that should be returned in sequence. To accomplish this, we can
use the Process
facade's describe
method:
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),
]);
Let's dig into the example above. Using the output
and
errorOutput
methods, we may specify multiple lines of
output that will be returned in sequence. The exitCode
method may be used to specify the final exit code of the fake process.
Finally, the iterations
method may be used to specify how
many times the running
method should return
true
.
Available Assertions
As previously discussed, Laravel provides several process assertions for your feature tests. We'll discuss each of these assertions below.
assertRan
Assert that a given process was invoked:
use Illuminate\Support\Facades\Process;
Process::assertRan('ls -la');
The assertRan
method also accepts a closure, which will
receive an instance of a process and a process result, allowing you to
inspect the process' configured options. If this closure returns
true
, the assertion will "pass":
Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);
The $process
passed to the assertRan
closure is an instance of
Illuminate\Process\PendingProcess
, while the
$result
is an instance of
Illuminate\Contracts\Process\ProcessResult
.
assertDidntRun
Assert that a given process was not invoked:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');
Like the assertRan
method, the
assertDidntRun
method also accepts a closure, which will
receive an instance of a process and a process result, allowing you to
inspect the process' configured options. If this closure returns
true
, the assertion will "fail":
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);
assertRanTimes
Assert that a given process was invoked a given number of times:
use Illuminate\Support\Facades\Process;
Process::assertRanTimes('ls -la', times: 3);
The assertRanTimes
method also accepts a closure, which
will receive an instance of a process and a process result, allowing you
to inspect the process' configured options. If this closure returns
true
and the process was invoked the specified number of
times, the assertion will "pass":
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
Preventing Stray Processes
If you would like to ensure that all invoked processes have been
faked throughout your individual test or complete test suite, you can
call the preventStrayProcesses
method. After calling this
method, any processes that do not have a corresponding fake result will
throw an exception rather than starting an actual process:
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');