jpbgomes/laravel-database-backup
A Laravel package for backing up your application database, with configurable storage destinations and Artisan commands you can schedule for automatic, recurring backups.
Install and confirm the version matrix first. v1.2.0 only widens the declared support window (Laravel 13 + Symfony Process 8, previous versions kept), so there is no migration work — but it is the moment to verify your own constraint set resolves cleanly:
composer require your-vendor/package:^1.2
composer why symfony/process
php artisan about
Check composer.lock afterwards: on PHP 8.4+ Composer will now happily pull symfony/process:^8.0, while a PHP 8.2/8.3 host stays on 7.x. Both are supported, but your CI matrix should exercise whichever one production actually resolves to (see Gotchas).
Where to look first, in order:
config/<package>.php after php artisan vendor:publish --tag=<package>-config — timeout, working directory, binary paths, and any allow/deny list of runnable commands live here.exitCode(), output(), errorOutput(), successful()) — this is the object you will type-hint and assert against in 90% of your code.First use case: replace an ad-hoc exec()/shell_exec() call with the package's runner so you get a typed result, a timeout, and a fakeable boundary for tests.
use YourVendor\Package\Facades\Process;
$result = Process::path(base_path())
->timeout(120)
->run(['php', 'artisan', 'migrate', '--force']);
if ($result->failed()) {
report(new RuntimeException($result->errorOutput()));
}
Note the array form: passing an array bypasses the shell entirely, which is both the safer and the more portable option. Reach for a string command only when you genuinely need shell features (pipes, globbing, env expansion).
Inject the runner, don't reach for the facade inside domain code. The facade is fine in Artisan commands, jobs, and controllers; anything you want to unit-test in isolation should take the manager via constructor injection (or a small RunsProcesses interface you bind in a service provider). That keeps the package swappable and lets you drop in a stub without touching Laravel's container.
Wrap every invocation in a value object. Long-lived codebases end up with dozens of process calls; centralising them means one place to change timeouts, binaries, and logging.
final class BackupDatabase
{
public function __construct(private ProcessManager $process) {}
public function __invoke(): ProcessResult
{
return $this->process
->path(config('backup.workdir'))
->env(['PGPASSWORD' => config('backup.password')])
->timeout(900)
->idleTimeout(60) // kill if nothing is printed for 60s
->run(['pg_dump', '-Fc', '-f', $this->target()]);
}
}
Stream output instead of buffering it. For long jobs (deploys, importers, npm ci) pass a callback so output goes to your log or a broadcast event as it arrives rather than sitting in memory until exit:
$result = Process::timeout(0)
->run($command, function (string $type, string $buffer) {
Log::channel('deploy')->info($buffer);
});
A timeout(0) disables the timeout entirely — acceptable only when something else (a queue worker's --timeout, Horizon's maxTime) is the real guard.
Push anything over a few seconds into a queued job. Combine with WithoutOverlapping middleware and a job-level timeout that is slightly larger than the process timeout, so the process is killed and its exit code recorded before the worker is SIGKILLed. Log both exit code and stderr on failure; a bare "job failed" is the most common debugging dead end with process execution.
Use the package's fake in tests and turn on stray-process prevention.
Process::fake([
'php artisan migrate*' => Process::result(output: 'ok', exitCode: 0),
'*' => Process::result(exitCode: 1, errorOutput: 'unexpected'),
]);
Process::preventStrayProcesses(); // fails the test on any unmatched call
preventStrayProcesses() is the highest-value line you can add: it stops a test from silently shelling out to a real git or rsync on a CI runner.
Integration tip — pair it with Laravel's scheduler and events. Emit a domain event when a result is failed(), and let a listener decide whether to retry, alert, or halt a pipeline. This keeps the calling code linear and testable.
Multi-step workflows: run steps sequentially, breaking out on the first failure, and pass state (artifacts, temp paths) between steps via a small context array rather than environment variables — env is inherited by children and is hard to reason about once you have three levels of nesting.
composer why symfony/process on the deployment image, not just locally; a dev machine on 8.4 and a container on 8.2 will resolve different majors, and Process 8 internals (options array shape, exception classes) can differ subtly from 7.x. Add a CI job pinned to each supported major if you support both.proc_open can be disabled. Shared hosts and hardened php.ini files (disable_functions = proc_open,exec,shell_exec) break the package in a way that surfaces as a confusing exception at runtime, not at boot. Fail fast: assert availability in a health check or service provider boot on non-local environments.node, git, and php resolve on your terminal and mysteriously 127 in a queued worker. Use absolute paths in config, or call PHP_BINARY for artisan invocations.errorOutput() as failure — check successful()/exitCode() first, and log stderr as context only.ProcessTimedOutException (distinguishing general from idle timeouts); an uncaught one inside a job burns a retry you may not want.timeout semantics, or POSIX paths needs a Windows-skipped test or an explicit "not supported" note in your README.escapeshellarg(); a user-supplied branch name in a git checkout string is a live injection vector.--no-interaction / -y flags everywhere, and set stdin to a closed or empty stream if the package exposes it.env([...]) in this package is typically additive to the inherited environment. If you need isolation, build the child env explicitly rather than assuming you've replaced it.Process::macro('forBackup', ...)) or swap the underlying Symfony Process subclass for one that pre-sets options. Bind them in a dedicated service provider rather than in AppServiceProvider.composer.json at ^1.2 but commit the lockfile for deploy tooling; a silent major bump of symfony/process under you is the most likely way this package breaks a pipeline without a code change.How can I help you explore Laravel packages today?