Process¶
A port-and-adapter layer for running shell processes. Process describes what to run;
ProcessRunner manages a queue of processes, controls concurrency, and handles failures.
SymfonyProcessRunner is the concrete adapter backed by symfony/process.
Application\Process
├── Process — Immutable process descriptor
├── ProcessBuilder — Fluent builder for Process
├── ProcessRunner (interface) — attach(), clear(), run()
├── ProcessErrorBehavior (enum: int) — EXCEPTION, IGNORE, RETRY
└── Exception\
├── ProcessException — extends SystemException
└── ProcessFailedException — extends ProcessException
Adapter\Process
└── Symfony\
└── SymfonyProcessRunner — Concurrent runner via symfony/process
Table of Contents¶
- Process
- ProcessBuilder
- ProcessRunner
- ProcessErrorBehavior
- SymfonyProcessRunner
- Symfony Configuration
- Usage Examples
Process Descriptor¶
Fight\Common\Application\Process\Process
An immutable value object that describes a process to be run. All fields beyond $command
are optional.
use Fight\Common\Application\Process\Process;
$process = new Process(
command: 'bin/console cache:clear',
directory: '/var/www/html',
environment: ['APP_ENV' => 'prod'],
input: null,
timeout: 60.0, // seconds; null = no timeout (default 60.0)
stdout: $stdoutFn, // callable(?string $data): void
stderr: $stderrFn, // callable(?string $data): void
outputDisabled: false
);
Fields¶
| Method | Returns | Description |
|---|---|---|
command() |
string |
Shell command string |
directory() |
?string |
Working directory (null = inherit) |
environment() |
?array<string, string> |
Additional env vars (null = inherit) |
input() |
mixed |
Stdin input (string, resource, or null) |
timeout() |
?float |
Timeout in seconds (null = unlimited) |
stdout() |
callable\|null |
Called for each chunk of stdout output |
stderr() |
callable\|null |
Called for each chunk of stderr output |
isOutputDisabled() |
bool |
Whether output capturing is disabled |
ProcessBuilder¶
Fight\Common\Application\Process\ProcessBuilder
A fluent builder for constructing Process descriptors. Accepts an optional initial argument
list in the constructor (or via create()), then assembles the shell command using
escapeshellarg so that arguments with spaces or special characters are always safe.
use Fight\Common\Application\Process\ProcessBuilder;
$process = ProcessBuilder::create('vendor/bin/phpunit')
->option('filter', 'test_my_feature')
->option('no-coverage')
->directory('/var/www/html')
->timeout(120)
->stdout(fn(string $data) => print($data))
->getProcess();
Building the Command¶
The builder distinguishes between a prefix (fixed leading tokens, e.g. the executable)
and arguments (variable tokens appended after the prefix). Both are escaped and joined
with spaces when getProcess() is called.
// Prefix + arguments
$process = ProcessBuilder::create()
->prefix(['php', 'artisan']) // fixed executable tokens
->arg('migrate') // positional argument
->option('force') // --force
->short('n') // -n
->getProcess();
// → 'php' 'artisan' 'migrate' '--force' '-n'
Methods¶
| Method | Description |
|---|---|
create(string\|array\|null $args) |
Static factory; accepts an initial argument string or list |
prefix(string\|array $prefix) |
Sets fixed leading tokens (replaces any previous prefix) |
arg(string $arg) |
Appends a positional argument; empty strings are ignored |
option(string $option, ?string $value) |
Appends a long option; -- added if absent |
short(string $option, ?string $value) |
Appends a short option; - added if absent |
clearArgs() |
Removes all positional arguments (prefix unaffected) |
directory(?string $dir) |
Sets working directory |
input(mixed $input) |
Sets stdin; accepts string, resource, or null |
timeout(int\|float\|null $s) |
Sets timeout in seconds (default 60.0); null = unlimited |
setEnv(string $name, string $value) |
Adds or overrides an environment variable |
stdout(callable\|null $fn) |
Sets callback for stdout chunks |
stderr(callable\|null $fn) |
Sets callback for stderr chunks |
disableOutput() |
Disables output capturing |
enableOutput() |
Re-enables output capturing |
getProcess() |
Returns the built Process; throws MethodCallException if empty |
ProcessRunner¶
Fight\Common\Application\Process\ProcessRunner
interface ProcessRunner
{
public function attach(Process $process): void;
public function clear(): void;
/**
* @throws ProcessException
*/
public function run(?ProcessErrorBehavior $errorBehavior = null): void;
}
Processes are queued via attach(), then all started when run() is called. run()
blocks until all queued processes complete and then clears the queue automatically.
Pass a ProcessErrorBehavior to control how failures are handled (defaults to
EXCEPTION).
ProcessErrorBehavior¶
Fight\Common\Application\Process\ProcessErrorBehavior
enum ProcessErrorBehavior: int
{
case EXCEPTION = 1; // Throw ProcessFailedException on non-zero exit (default)
case IGNORE = 2; // Continue silently when a process fails
case RETRY = 3; // Re-run failed processes up to the configured tries limit
}
SymfonyProcessRunner¶
Fight\Common\Adapter\Process\Symfony\SymfonyProcessRunner
A queue-based, concurrent process runner built on symfony/process. Processes are
started up to the concurrency limit; as they finish, the next queued process is launched.
use Fight\Common\Adapter\Process\Symfony\SymfonyProcessRunner;
$runner = new SymfonyProcessRunner(
logger: $logger, // ?LoggerInterface (default null)
maxConcurrent: 4, // max simultaneous processes; 0 = unlimited (default 1)
delay: 1000, // microseconds between polling ticks (default 1000)
tries: 3, // max attempts per process when using RETRY (default 3)
logLevel: LogLevel::DEBUG
);
Concurrency¶
Set maxConcurrent to control how many processes run at the same time:
$runner = new SymfonyProcessRunner(maxConcurrent: 8);
foreach ($jobs as $job) {
$runner->attach(new Process($job->command()));
}
$runner->run(); // runs up to 8 at a time, blocks until all complete
maxConcurrent: 0 disables the concurrency limit — all queued processes are started
immediately.
Retry¶
When ProcessErrorBehavior::RETRY is passed to run(), a failed process is re-enqueued
and started again, up to $tries total attempts:
$runner = new SymfonyProcessRunner(tries: 5);
$runner->attach(new Process('bin/flaky-script'));
$runner->run(ProcessErrorBehavior::RETRY);
// attempts up to 5 times before throwing ProcessFailedException
Logging¶
When a LoggerInterface is provided, the runner logs:
- Process started (at configured $logLevel)
- Process restarted after failure (at configured $logLevel)
- Process failed — includes exit code, exit code text, stdout, and stderr (at error)
Output Callbacks¶
Attach per-process callbacks to stream output in real time:
$process = ProcessBuilder::create('bin/long-running-task')
->stdout(fn(string $data) => $this->logger->info($data))
->stderr(fn(string $data) => $this->logger->error($data))
->getProcess();
Symfony Configuration¶
# config/packages/common_process.yaml
services:
_defaults:
autowire: true
autoconfigure: true
Fight\Common\Adapter\Process\Symfony\SymfonyProcessRunner:
arguments:
- '@logger'
- 4 # maxConcurrent
- 1000 # delay (µs)
- 3 # tries
Fight\Common\Application\Process\ProcessRunner:
alias: Fight\Common\Adapter\Process\Symfony\SymfonyProcessRunner
Usage Examples¶
Running a Single Process¶
use Fight\Common\Application\Process\ProcessBuilder;
use Fight\Common\Application\Process\ProcessRunner;
class CacheClearer
{
public function __construct(private ProcessRunner $runner) {}
public function clear(string $env): void
{
$this->runner->attach(
ProcessBuilder::create('bin/console')
->arg('cache:clear')
->directory('/var/www/html')
->setEnv('APP_ENV', $env)
->getProcess()
);
$this->runner->run();
}
}
Running Jobs in Parallel¶
$runner = new SymfonyProcessRunner(maxConcurrent: 4);
foreach ($files as $file) {
$runner->attach(
ProcessBuilder::create('bin/process-file')
->arg($file)
->getProcess()
);
}
$runner->run(); // 4 at a time until all complete
Ignoring Failures¶
$runner->attach(new Process('bin/optional-cleanup'));
$runner->run(ProcessErrorBehavior::IGNORE); // non-zero exit silently discarded
Retrying Flaky Processes¶
$runner = new SymfonyProcessRunner(tries: 3);
$runner->attach(
ProcessBuilder::create('curl')
->arg('https://api.example.com/sync')
->getProcess()
);
$runner->run(ProcessErrorBehavior::RETRY);
// retries up to 3 times; throws ProcessFailedException if all attempts fail