consolidation/site-process
Consolidation Site Process is a PHP process runner for local and remote site commands. It extends Symfony Process with site-alias support, enabling SSH, Docker, and other transports so tools like Drush can execute commands against configured environments safely and consistently.
consolidation/site-process is a library, not an application — you install it to use from your own command/tool code.
composer require consolidation/site-process
It pulls in symfony/process and pairs with consolidation/site-alias (the alias format is @site.env, typically stored in drush/sites/*.site.yml). There is no binary to run; the entry points are:
Consolidation\SiteProcess\SiteProcess — the Symfony\Component\Process\Process subclass. It is the object you call run(), start(), wait(), getOutput(), getExitCode() on.Consolidation\SiteProcess\SiteProcessFactory — creates a SiteProcess from a SiteAliasInterface, a command array, arguments, and options. Most applications never instantiate SiteProcess directly.Consolidation\SiteProcess\Transport\* — how a command reaches its target (LocalTransport, SshTransport, etc.). This is what makes a local alias run locally and a remote alias run through ssh.Consolidation\SiteProcess\ProcessManagerAwareInterface / ProcessManagerAwareTrait — the standard way to give a command class access to a factory.First use case — run drush status against an alias and let the package decide whether that means locally or over SSH:
use Consolidation\SiteAlias\SiteAliasManager;
use Consolidation\SiteProcess\SiteProcessFactory;
$aliasManager = new SiteAliasManager();
$aliasManager->addSearchPath(__DIR__ . '/drush/sites');
$alias = $aliasManager->get('@prod');
$factory = new SiteProcessFactory();
$process = $factory->create($alias, ['drush', 'status', '--fields=name,status']);
$process->run();
if (!$process->isSuccessful()) {
fwrite(STDERR, $process->getErrorOutput());
exit($process->getExitCode());
}
echo $process->getOutput();
If @prod has a host/user, the package builds an ssh invocation; if it is a local alias (or @self), it runs the command directly. Your calling code does not branch on that.
Look first at SiteProcess::create() / SiteProcessFactory::create() and at the transport selected for the alias — that is where 90% of surprises come from.
The core pattern is: never build ssh user@host 'cmd' yourself. Build a SiteProcess from an alias and let the transport do the wrapping.
$process = $factory->create($alias, ['git', 'status', '--porcelain']);
$process->run();
The alias supplies host, user, port, root/path, and any SSH options. This is also how you get consistent behavior across local and remote in tests: point the same code at @self and it runs locally.
For anything larger than a script, follow the Drush convention: your command class uses the trait, and a factory is injected once.
use Consolidation\SiteProcess\ProcessManagerAwareTrait;
class DeployCommand
{
use ProcessManagerAwareTrait;
public function deploy($aliasName)
{
$alias = $this->siteAliasManager()->get($aliasName);
$this->processManager()
->create($alias, ['composer', 'install', '--no-dev'])
->mustRun();
$this->processManager()
->create($alias, ['drush', 'cache:rebuild'])
->mustRun();
}
}
mustRun() throws Symfony\Component\Process\ProcessFailedException on non-zero exit — good default for steps that must succeed.
create() expects the command as an array (['ls', '-la', '/tmp']). Passing a single string tends to be interpreted as a shell command and reintroduces quoting problems, especially once it is wrapped in an SSH command. Use arrays and let Symfony/the transport escape each argument. Reserve a shell string for cases that genuinely need pipes or redirection.
Set behavior on the returned process object before running:
$process
->setWorkingDirectory('/var/www/app') // maps to cd on the remote side
->setTimeout(null) // long-running deploy
->setTty(true) // interactive / progress output
->setRealtime(true); // echo output as it arrives
setRealtime(true) is the one you want for streaming logs/rsync progress; combine with start() + wait() and a callback when you need to inspect lines as they arrive:
$process->start(function ($type, $buffer) {
echo $buffer;
});
$process->wait();
A typical workflow is: resolve the alias, run a sequence of commands, short-circuit on failure, and treat the whole thing as one unit.
foreach ([['git','pull'], ['composer','install'], ['drush','updb','-y']] as $cmd) {
$p = $factory->create($alias, $cmd);
$p->setTimeout(null)->run();
if (!$p->isSuccessful()) {
throw new \RuntimeException(sprintf(
"%s failed (exit %d): %s",
implode(' ', $cmd),
$p->getExitCode(),
$p->getErrorOutput()
));
}
}
If your environment is Docker, Kubernetes, or a jump host, that is the extension point: implement TransportInterface (the methods that build the command and/or feed stdin) and use it instead of the default SSH transport for the alias. This keeps your command code identical while the delivery mechanism changes.
new SiteAliasManager() won't find drush/sites/*.site.yml unless you addSearchPath() (or hand it a root). A missing alias usually shows up as an exception from get(), not a process failure.getErrorOutput() for ssh: connect/Permission denied before blaming the remote command.setWorkingDirectory() semantics differ by side. Locally it is Symfony's cwd. Remotely it becomes a cd in the remote shell. The path must exist on the remote host; there is no fallback.setEnv() affects the child. Remotely, you generally need to export them inside the command (['sh', '-c', 'export FOO=bar; ./deploy.sh']). Do not assume setEnv() is propagated.setTty(true) allocates a pseudo-terminal (and on the remote side, typically requires script/ssh -t). It merges stdout and stderr, which breaks clean output capture. Use it only for interactive commands; use plain capture for anything you parse.start()/wait(). Setting setRealtime(true) and then calling run() gives you buffered output; the streaming behavior is tied to the callback form.setTimeout(null) is often required. Deploys, composer install, and rsync routinely exceed Symfony's default timeout; otherwise you get a ProcessTimedOutException mid-run.$process->getCommandLine() shows the fully wrapped invocation the transport built. Copying that into a shell is the fastest way to reproduce a failure.setLogger() on the process gives you the constructed command and output in your application logs; extremely helpful when the failing step is buried in a multi-command workflow.Process API. On modern Symfony/PHP 8+, expect deprecations and possibly breakage; constrain symfony/process in your composer.json and run a smoke test (@self alias, ['echo', 'hi']) after upgrades.start() several processes, then wait()), and rollback logic in your own layer.@self and assert on exit codes and output without SSH. For fully hermetic tests, inject a fake factory that returns stub Process objects.How can I help you explore Laravel packages today?