lhpalacio/backup-manager-bundle
Backup Manager Bundle simplifies creating, scheduling, and restoring database and file backups in Symfony/PHP applications. Configure backup sources and destinations, run backups from services or commands, and keep recovery workflows flexible and maintainable.
First, adjust expectations: you cannot composer require this into a Laravel app. lhpalacio/backup-manager-bundle is a Symfony2 bundle — it registers via AppKernel, wires services through the Symfony DI container, and exposes Symfony Console commands. There is no service provider, no config/backup.php stub, no Artisan namespace, and no support for Laravel's scheduler or queue. The repository is archived, targets EOL PHP (5.x/early 7.x), and has zero dependents. On a modern Laravel (9+) the code will not even install cleanly, let alone boot.
So the "getting started" is a reading path, not an install path:
src/ in this order — the DI extension (config schema: sources, destinations, options), then the command classes (backup, restore, list), then the service wiring. That's the whole bundle; it's thin. Roughly an hour.backup-manager/backup-manager. This is the part with actual substance: dumpers, compressors, and storage adapters. The bundle adds nothing you can't call directly.Your first real use case, in Laravel, is one of two paths:
spatie/laravel-backup. It is the direct Laravel-native equivalent of everything this bundle does (scheduled dumps, multi-destination via Laravel filesystem disks, retention, notifications) and is maintained.backup-manager/backup-manager directly and write the ~150-line Laravel wrapper yourself: a config file, a singleton binding in a service provider, one Artisan command, and a scheduled entry. You own that surface either way — the difference is whether you also own a forklifted Symfony2 bundle.Skip entirely if backups are a production or compliance obligation. An archived, 13-star, zero-dependent dependency holding your database credentials and object-storage keys is a non-starter in a security review, and backup tooling fails silently by nature.
Everything below assumes the "read it and re-implement the shape in Laravel" path.
Pattern 1 — Config-first, mirroring the bundle's schema. The bundle's one genuinely good idea is that sources and destinations are named, declared once, and composed. Do the same in config/backup.php:
return [
'sources' => [
'primary' => ['driver' => 'mysql', 'connection' => 'mysql'],
'uploads' => ['driver' => 'files', 'paths' => [storage_path('app/public')]],
],
'destinations' => [
's3' => ['disk' => 'backups-s3', 'prefix' => 'db/'],
'nas' => ['disk' => 'backups-nas', 'prefix' => 'db/'],
],
];
Then config('backup.sources.primary') is the contract your service consumes. This is the piece worth copying.
Pattern 2 — One command, thin, no business logic. The bundle's commands are thin wrappers over the service; keep yours the same. php artisan backup:run primary --to=s3. The command resolves the source/destination pair, delegates, and returns an exit code. Nothing else. This makes the command trivially testable with a faked service.
Pattern 3 — Programmatic access via the container. The bundle's other useful idea: backups are callable from your own services, not just cron. In Laravel, bind a singleton in a service provider and inject it:
$this->app->singleton(BackupRunner::class, fn ($app) => new BackupRunner(
config('backup.sources'), config('backup.destinations')
));
Then any controller, job, or listener can call $runner->run('primary', 's3') — useful for "backup before migration" or "export this tenant's data on request" flows.
Pattern 4 — Fan-out to multiple destinations. The bundle supports N destinations per source; so should you. Loop destinations inside the runner and fail loudly per-destination rather than aborting the whole run — a dead FTP target should not prevent the S3 copy from landing. Collect per-destination results and report them together.
Pattern 5 — Schedule with withoutOverlapping and onOneServer. This is where the bundle is weakest and Laravel is strongest. The bundle expects you to wire cron yourself; Laravel gives you:
Schedule::command('backup:run primary --to=s3')
->dailyAt('02:00')
->withoutOverlapping(120)
->onOneServer()
->runInBackground();
onOneServer() requires a shared cache driver (Redis, database) — not file. withoutOverlapping needs the same. Get this right, or you'll get concurrent dumps writing to the same temp file.
Pattern 6 — Notifications on failure, from day one. The bundle has no notification story, which is precisely the failure mode that matters. In Laravel, implement ShouldQueue on the command's failure path or use Notification::route('mail', ...) in the failed() handler. Better: a heartbeat model — a backup_runs table row per run plus an external dead-man's-switch check. Do not trust the tool's own success log; verify freshness independently.
Pattern 7 — Queue long-running backups. Never run a multi-GB dump synchronously in a web request or in a schedule:run that shares a worker with other jobs. Dispatch a job with $timeout set above your worst-case dump duration, and set retryUntil. Note the scheduler's runInBackground still occupies a PHP process; the queue is cleaner.
Pattern 8 — Post-backup verification. The bundle lists and restores, but does not verify. Add a separate weekly command that (a) downloads the newest artifact, (b) restores it into a throwaway database/schema, (c) runs CHECKSUM TABLE or compares row counts against the source, and (d) alerts on mismatch. This is the single highest-value thing you'll write, and it's the thing the bundle never had.
Pattern 9 — Retention as an explicit prune command. Filesystem-driven retention (just keep writing until the disk fills) is what you get by default. Implement backup:prune --keep-days=14 --keep-count=30 as its own scheduled command against the destination disk, driven by a policy in config — not by whatever the adapter happens to do.
Pattern 10 — Test against faked disks. Laravel's Storage::fake('backups-s3') plus a small fixture database makes the whole runner testable in milliseconds. If you fork anything from this bundle, you lose that; another reason not to.
DependencyInjection/Configuration.php class as-is — rewrite the tree as a plain PHP config array with validation in a service provider's register()/boot().composer.json pins PHP 5.x/early 7.x and Symfony 2.x. On PHP 8.1+ you'll hit removed functions and incompatible transitive deps. Even composer require --ignore-platform-reqs gets you a class that won't autoload under Laravel.league/flysystem-aws-s3-v3 etc.), which are maintained.mysqldump invocation is the sharp edge. Expect to deal with: binary path resolution (which mysqldump differs by OS/base image), escapeshellarg on every interpolated value, and credentials. Prefer --defaults-extra-file or MYSQL_PWD over --password= on the command line, which leaks into ps and logs.--single-transaction for InnoDB (consistent dump without locking). It does nothing for MyISAM — those tables still lock. On MySQL 8 you'll also need --no-tablespaces unless the user has PROCESS, otherwise the dump fails.No space left on device waiting to happen. Set a configurable temp path on a volume with headroom, and always clean up in a finally block — an orphaned 40 GB dump is a real incident.exec() returns the shell exit; proc_open returns 0 even when the child failed unless you check proc_close's return. The command can exit 0 having written nothing. Always assert the artifact exists and has non-zero size before declaring success.env() outside config files returns null under config:cache. Standard Laravel trap, but it bites hard here because backup credentials are exactly the sort of thing people reach for env('AWS_KEY') for in a service class. Put them in config/filesystems.php, never in the runner.withoutOverlapping mutex stuck from a killed process — all produce "no backups" with zero signal. Monitor freshness of artifacts, not command exit status. A heartbeat/dead-man's-switch is worth more than any logging you add.withoutOverlapping locks leak. If a PHP process is SIGKILLed (OOM during a large dump), the lock persists for the full withoutOverlapping window. Set it explicitly and log when a run is skipped due to an existing lock — otherwise you'll silently skip days.Storage::disk('backups-s3')->put(...), fakes for testing, and a maintained SDK — the thing the bundle was trying to abstract is already abstracted upstream.How can I help you explore Laravel packages today?